Not logged inGosu Forums
Forum back to libgosu.org Help Search Register Login
Up Topic Gosu / Gosu Exchange / What I learned about callcc, threads and Gosu...
- - By erisdiscord Date 2010-08-21 23:05
Well, I figured out this clever trick for game states. Think Chingu if you aren't sure what I mean by game states.

I wanted to have push_state(state) stop execution of the current state right there and let the new state take over immediately. Then when I call pop_state(return_value), the original push_state call would return whatever was passed to pop_state to the original game state. Everybody still with me? :)

I hacked something together using callcc to save a continuation and then throw an exception to get out of the current state and move on to the next tick. After popping the top state back off the stack, it would call the continuation and do its magic. It worked beautifully.

Except that it causes a deadlock after the next tick ends because it tries to unlock a mutex that isn't locked. This is on OS X; it might work on other platforms, but I doubt it. So much for that brilliant idea! Well, callcc is dangerous and yucky anyway.

Then inspiration struck yet again: I would implement it by spawning a new thread and having push_state block until that thread exited. The code to do it this way ended up being much cleaner and didn't rely on attaching hidden things to my game states that could get lost. I know that drawing on a non-main thread with Gosu causes things to get a little weird, so I made sure that my draw methods would always be called on the main thread. Nobody needs to switch states in draw and they shouldn't.

This worked beautifully as well. In fact, I was able to load fonts and images in my new threads and do all sorts of neat stuff. Except, actually, no, I can't load images in secondary threads either. My first game state was the only one loading images and the code that does that was being called on the main thread after all. Oops. So, I just got a segfault after modifying one of my examples in a way that caused images to be loaded on one of those other threads.

Damn and curses—my hopes and dreams are shattered. Hey Julian, any way you could make image loading thread safe? :) I'd try to hack at it myself, but I could never get the Ruby extension to build.
Parent - - 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.
Parent - - By erisdiscord Date 2010-08-22 01:18
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
Parent - - 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. :)
Parent - By erisdiscord Date 2010-08-22 04:12
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
Parent - - By Maytsh Date 2010-08-26 21:04
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.
Parent - - By erisdiscord Date 2010-08-27 15:57
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
Parent - - By Maytsh Date 2010-08-27 20:29
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.
Parent - - By erisdiscord Date 2010-08-27 21:00
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. :)
Parent - 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 ;)
Parent - By erisdiscord Date 2010-08-29 03:45
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. :)
Up Topic Gosu / Gosu Exchange / What I learned about callcc, threads and Gosu...

Powered by mwForum 2.29.7 © 1999-2015 Markus Wichitill