LoginLogin
Nintendo shutting down 3DS + Wii U online services, see our post

Has anybody implemented an event queue before?

Root / Programming Questions / [.]

raimondzCreated:
Hi, I want to know if anybody has implemented event queue on smilebasic. I did that before for my megaman battle network clone but I want to know if there is a better implementation. Here is my code.(Warning: this include Lowerdash syntax)
Spoiler
MODULE EVENTS

VAR curFrame
VAR FRAME[0]
VAR FUN$[0]
VAR ARG$[0]

'Function triggered when a new screen is created.
STATIC DEF onCreate_
 EVENTS_reset
END

'Function triggered when the screen is destroyed.
STATIC DEF onDestroy_
 EVENTS_reset
END

STATIC DEF update_
 INC _.curFrame
 WHILE LEN(_.FRAME)>0 && _.FRAME[0]<_.curFrame
  EVENTS_exec
 WEND
END

'Reset the values of this module
STATIC DEF reset
 _.curFrame=0
 _.FRAME=UTIL_malloc(0,"")
 _.FUN$=UTIL_malloc(0,"$")
 _.ARG$=UTIL_malloc(0,"$")
END

'put a value on the argument variable
STATIC DEF put ARG$,V
 VAR T=typeOF(V)
 VAR Q$=CHR$(34) 'Q$="
 
 IF LEN(ARG$)>0 THEN INC ARG$,","
'IF T==2 THEN V is a string. Else V is a number.
 IF T==2 THEN
  INC ARG$,Q$+V+Q$
 ELSE
   INC ARG$,STR$(V)
 ENDIF
END

'Add an event in the queue
STATIC DEF add F,F$,A$
  PUSH _.Frame,F + _.curFrame
  PUSH _.FUN$,F$
  PUSH _.ARG$,A$
 SORT _.Frame,_.FUN$,_.ARG$
END

'Use the page 1 to execute an event.
STATIC DEF exec
 VAR F$,V$
 EAT SHIFT(_.Frame)
 F$=SHIFT(_.FUN$)
 A$=SHIFT(_.ARG$)

 PRGEDIT 1
 PRGDEL -1
 PRGSET F$+" "+A$
 EXEC 1
END
Usage
 EVENTS_onCreate_
 
'Print hello world after 100 frames
 VAR ARG$=""
 EVENTS_put ARG$,"hello world"
 EVENTS_add 100,""PRINT",ARG$

'Make a sound after 200 frames
 ARG$=""
 EVENTS_put ARG$,10
 EVENTS_add 200,"BEEP",ARG$

' "game loop"
 WHILE 1
  EVENTS_update_
  VSYNC
 WEND

My Smisp system has 'threads', in which the call stack is an explicit data structure: the central engine checks each of the threads, if there is processing to be done, one step is done (basically, as much as can be done before requiring a call to another function or procedure; it is not called directly, but the call stack is updated). I'm currently writing code for asynchronous communications between threads. Nothing fancy. An 'inbox' string array for each thread. To transmit, PUSH onto the appropriate inbox(es). To receive, check the length of the inbox, and SHIFT if it's not empty. EDIT: Looking at your system, I see a potential flaw. It seems only element number 0 of the array _.FRAME is checked for its timer expiring, and EVENTS_add always PUSHes to the end of the array. So, say, at time T=0, event A is scheduled for (now)+100 and event B is scheduled for (now)+200. Say, event A schedules a new event C, at (now)+50. After A terminates, element number 0 of the array will be event B, to be executed at T=200, and element number 1 of the array will be event C, supposed to be executed at T=150. Maybe I'm misreading the code, or I'm missing something, but you might want to check for this kind of circumstance. EDIT 2: I think you should also drop the assumption that EVENTS_update_ will be called precisely once per frame. There is a system variable which increments once per frame, reliably... MAINCNT. Why not use it? Or even use MILLISEC.

I think they call EVENTS_update_ once per frame because they want their events to be tied to the in-game frames. That way if the game slows down, it won't trigger events at the normal speed which could break things.

@SquareFingers If forgot to add SORT on the function "ADD" to handle that issue. As 12Me21 said, I use the update_(and curFrame) to keep the events tied to the in-game frames. Also, that's helpful if I want to pause the game. What I want to improve is the way events are called. Right now, It write a function and arguments on other page but I want to know if there is another way which doesn't use an extra page.

Unless there's a deliberate 'slow-motion' mode or such, if 'in-game frames' deviates from system frames, then there's something wrong with the code using EVENTS - that's not a good reason to have something wrong with the EVENTS code itself. You make a strong point with pausing the game, but I'd still find another way to do it. Another issue I see with your code is you don't sanitize string arguments. If EVENTS_add has a V which is a string containing CHR$(34), things will go badly wrong, or containing CHR$(10) or CHR$(13) (I think either of these will be trouble, but perhaps it's just one of them). I am critical, but I do hope you know it's not malicious: I want to see you succeed as best as possible, I don't want it to be an unpleasant surprise and a tricky debugging experience for you when the system fails because there's a " in a string argument. You could use CALL, instead of writing to a new page. CALL has the disadvantage that the number of parameters needs to be fixed, but there are workarounds that get you mostly there. You could use a single string parameter, like you use ARG$, and have each function extract the intended parameter values from that one actual string parameter. You could have a string array parameter and a numeric array parameter, so e.g. DEF X A$,B%,C$,D$,E#,F%,G$ becomes DEF X SARRAY$,NARRAY#, and use SARRAY$[0] instead of A$, etc.. Every integer-type value can be precisely represented in the floating-point type, so they both can be put together. This gives string references to the function, so INC SARRAY$[0],"Z" will have an effect outside the function, which your current implementation cannot do. Even my suggestion cannot handle references to actual arrays, though, so a PUSH will not have an effect outside of the function (unless it is on a string). Another possibility is global variables. There are problems with globals, but in some instances, they can be the right tool, so it's something to consider. EDIT: I haven't tested it, but you might be able to use VAR, and the names of variables as regular string values, to implement pass-by-reference. This way, you can pass arrays, and you can essentially implement OUT parameters. EDIT 2 Another thing, STR$ does not always generate a string which can be interpreted as a numeric literal... particularly inf and nan. It also does not keep precision.

Yeah, using PRGEDIT and EXEC each time you call an event would probably be extremely slow, since PRGEDIT commands aren't that fast and SB would have to recompile the slot every time. It's definitely possible to use MAINCNT for timing events, but I just don't see any advantage to doing this, or anything wrong with using your own timer. If you make all your event functions have the same number of arguments, it would probably be faster than always passing arrays. For example:
DEF FUNC1 A,B,C,D
 PRINT A+B*C/D
END

DEF FUNC2 A,B,C,D
 PRINT A
END

DEF FUNC3 A,B,C,D
 PRINT A-B
END

CALL "FUNC3",7,4,,

It's definitely possible to use MAINCNT for timing events, but I just don't see any advantage to doing this, or anything wrong with using your own timer.
Right, there's no advantage. Unless you want your clocks to, like, measure time, reliably.

It's definitely possible to use MAINCNT for timing events, but I just don't see any advantage to doing this, or anything wrong with using your own timer.
Right, there's no advantage. Unless you want your clocks to, like, measure time, reliably.
But this is an event queue, not a clock. And it should measure time reliably as long as the game is running at the proper speed.

But this is an event queue, not a clock. And it should measure time reliably as long as the game is running at the proper speed.
Right. It is not a clock. It is an event queue. More specifically, an event queue with a clock. I'm glad you got that off your chest. As to what it 'should' do, I think it 'should' give the best chance that the game does run at proper speed, and keeps time, rather than make it more likely the game does not, so that when it does not, its failure to keep time has an 'excuse'. 'Proper speed' can hardly be used in an argument for choosing less accuracy in your clock. I may be wrong.

Yeah, using PRGEDIT and EXEC each time you call an event would probably be extremely slow, since PRGEDIT commands aren't that fast and SB would have to recompile the slot every time. It's definitely possible to use MAINCNT for timing events, but I just don't see any advantage to doing this, or anything wrong with using your own timer. If you make all your event functions have the same number of arguments, it would probably be faster than always passing arrays. For example:
DEF FUNC1 A,B,C,D
 PRINT A+B*C/D
END

DEF FUNC2 A,B,C,D
 PRINT A
END

DEF FUNC3 A,B,C,D
 PRINT A-B
END

CALL "FUNC3",7,4,,
I did that before but that method was not very flexible. In one of my games I ended up with 6 "if" in the exec function to handle different kind of functions (Mostly because, at the time I wasn't aware that I could use the same parameter like a string, array or number).
... You could use CALL, instead of writing to a new page. CALL has the disadvantage that the number of parameters needs to be fixed, but there are workarounds that get you mostly there. You could use a single string parameter, like you use ARG$, and have each function extract the intended parameter values from that one actual string parameter. You could have a string array parameter and a numeric array parameter, so e.g. DEF X A$,B%,C$,D$,E#,F%,G$ becomes DEF X SARRAY$,NARRAY#, and use SARRAY$[0] instead of A$, etc.. Every integer-type value can be precisely represented in the floating-point type, so they both can be put together. This gives string references to the function, so INC SARRAY$[0],"Z" will have an effect outside the function, which your current implementation cannot do. Even my suggestion cannot handle references to actual arrays, though, so a PUSH will not have an effect outside of the function (unless it is on a string). ...
I like that idea. This remind me to Android on how you need to pass data with bundle(Map of variables) instead of passing direct arguments for an activity(This is a new window) or listener. I have a module that do something similar. I use it to store strings, number and arrays on an array to handle data files. I'll try to do something similar and see how this work.