By jlnr (dev)
Date 2010-08-22 00:40
Edited 2010-08-22 00:42
The original Delphi version of Gosu used to be able to do this, and you neither need threads nor continuations, but support from within Gosu. The boring trick to it is recursion, just like Win32 does it actually, basically calling Window::show() from within its own callbacks.
From a (kind of) functional point of view, imagine mainloop() as being a function that takes all the callbacks and runs the main loop. Let's just assume there's only draw, update and button_down. Then the main game would run mainloop(game_update_proc, game_draw_proc, game_button_down_proc). At some time, the game_update_proc decides that the user needs to answer a modal confirmation dialog and calls "user_response = query_dialog('really?')".
query_dialog(...) just calls mainloop(msgbox_update_proc, msgbox_draw_proc, msgbox_button_down_proc). While this call to show_window is running, the outer show_window call would just wait while the inner one would consume all system events. The message box gets closed in msgbox_button_down_proc, and breaks out of the loop, e.g. by throwing a symbol, and query_dialog would return which button was pressed. The outer call just resumes the stream of system events.
There are two things that are tricky here imho. First, the interface. Gosu really doesn't need a Window class with callbacks. Already one might argue that all good games should use states all the time. Why not just use a delegate (state) for the callbacks altogether?
Basically we need 1) some way to specify what kind of window Gosu should open (resolution, fullscreen yes/no, caption...), 2) a runloop() function and 3) a way to break out of it. The runloop() function could be turned into State#run. We could also add State#stop(return_value=nil) for breaking out, similar to Window#close now.
The other thing is the implications for what is safe. Having an in-game msgbox() function that blocks is certainly cool. Now what happens when there are two button_down calls, the first triggering the msgbox()? Should the second one be delivered to the recursed function or to the original loop later on? Is it easy to corrupt anything else when in the middle of button_down, you actually might be calling the same button_down function again? Yay, suddenly things might be reentrant. :)
The original Delphi approach was to supply the system-level handleMessage(); function directly in Gosu, the one you can still peek at in WinUtility.hpp (I think). The dialog box might then do "while not result_stored_somewhere do handleMessage()", effectively nesting the main loop. But that approach easily ruins all abstraction.
Yeah, I suppose I should just get used to doing things asynchronously. It's the wave of the future or something. :) It's just that some things are difficult to do that way.
When it comes time, I'll probably run high level game logic in a second thread and use a message queue to act on it, but I won't be interacting with Gosu directly then. This is gonna be fun.
Time to viciously rip the threads from my code! I'll keep the Worker/Task classes I created, though. :D
By jlnr (dev)
Date 2010-08-22 02:49
Oh, I never meant to point to multi-threading. That is something I avoid until the tiniest target device has at least four cores or something. I think the core control flow should be as easily understandable as possible, and threads won't help with that. :)
Nah, I don't mean it like that. Think about RAD systems for games and how they tend to have some kind of scripting language that interacts asynchronously with your game objects.
Take RPG Maker '98 or 2000, because a lot of people know them: the event editor allowed you to, say, move the player character ten squares north, rotate counterclockwise, move three squares west and then face south, and you could either wait for that action to complete or start moving another character while that action is still in progress.
That's what I mean, and it won't come 'till much later if it ever does. Don't you worry your heart about me getting mired in threads. :D
Just remembered that you pointed me to this thread...
Implementing proper continuation support would be rather tempting for my framework, as it would actually be pretty easy. But on the other hand, I share a lot of the doubts.
I mean, what exactly is the class of situations where this useful? The only somewhat neat example I could come up with would be something like this:
myGame = do
intro
level1
level2
credits
Could even support loops or detours. Yet with the glaring problem that it's highly nontrivial to serialize this kind of game state :)
To me, this seems like a bad idea on many levels. Wouldn't really want to make it easier.
Would also be useful for (as Julian's example above) implementing dialogs that can be called synchronously. That was the main thing I wanted to do with it, actually. Something like this wholly fictional piece of code:
def button_down id
if user_clicked_set_input_button?
@button_map = call_state SetInputState
else
@button_map.do_stuff id
end
end
Yes, I kind-of get the synchronous window part. Such compatible game states must have:
* no side effects,
* ideally no requirements concerning the "pushed up" other game states and
* should never ever get serialised.
That's pretty hefty requirements. Just about anything bigger than a simple dialog is probably going to violate that. I mean, even showing the frozen prior game state as a background is already potentially unsafe in that it needs the background layer to be in a somewhat consistent state.
At least the first two problems could be solved by doing everything in delayed callbacks that get executed from some defined "safe" environment. If you had that, I could even make the following work:
bombActivate bomb = inContinuationStyle $ do
res <- pushState $ myMessageBox "Blow this up?"
guard (res = YES)
blowUp bomb
(I hope this is roughly understandable even when using Haskell syntax ;) )
"inContinuationStyle" would here register a callback that gets called after "update" is done - which would then push the message-box state and register a continuation for the rest, again as a callback. This would seem like less of a hack to me, and you could even think about some nice extensions, like having a "wait" command that delays the callback by a few ticks or seconds.
Yet the problem is again serialisation - and that the programmer needs to decide where exactly "safe" is. We wouldn't want any of such routines to issue callbacks while the main menu is showing, for instance.
I think I get the gist of what you're doing, but I'm only vaguely familiar with Haskell so it's not totally transparent. My understanding is that you're doing the waiting in the callback and the inContinuationStyle function otherwise returns immediately, correct?
The no serialisation requirement wouldn't be a particular problem for me (at least not as I envision it currently) since I don't plan to serialise game states directly. The game state is created anew and it loads a map object (and later an entity list) from there.
On the other hand, I'm pursuing another route right now because continuations and two-way thread communication truly are headache fuel. I'm certainly interested to hear your ideas, though. :)
By Maytsh
Date 2010-09-01 22:20
> On the other hand, I'm pursuing another route right now because continuations and two-way thread communication truly are headache fuel. I'm certainly interested to hear your ideas, though. :)
And I'd find it interesting to hear what your "other route" is ;)
Right now I can't see anything that I could build into the framework, because every solutions is either specific to a very special problem - or could be built in user code just as well. I wouldn't like having "pushState" really change the "top-most" control, for example - that sounds unsafe and I feel like you could quickly want something else.
Instead I would propose that the top control passes down a function with which other controls can "push" their desired states on a stack, which then later take priority. The programmer might have to pass this function around a bit, but to me this feels like the design where the programmer has more control over what actually happens.
But okay, I guess I will have to try it sometime. Would also be interesting to know more about what you're trying to do ;)
I've just remembered that Ruby 1.9 has Fibers, which are kind of like the old green threads but more awesome. Fiber#yield
and Fiber#resume
seem to do exactly what I wanted my push_state
and pop_state
methods to do. Hmmmmm, I might have to poke around.
I doubt I'll end up using this, but who knows. :)