I am something of a roguelike traditionalist (you can find me calling out non-roguelikes in a hopefully non-obnoxious manner on reddit) but while I do love my ASCII, I also think looking nice is important. Once I started building ranged weapons into my game I realized: I need animation.
In most programming languages, animation is straightforward. Start an animation, wait, continue where you left off. In javascript, this is less straight forward, because javascript is a single-threaded non-blocking language. The appropriate way to do delays in javascript is to use setTimeout. However, if your code looks like this:
player.shoot(monster, weapon)
game.drawAnimation(player, weapon)
game.damage(monster)
if monster.is_dead() then
game.kill(monster)
Then your game will not work like you think it will. As your drawAnimation function starts firing off setTimeout events and drawing (for example) the path of the shot across the screen with 20ms delays, the javascript machine continues chugging along. This means while your player is watching their bullets fly across the screen, javascript has already moved on and is now calling your damage() and potentially kill() functions. Even worse, depending on how you write your code, it will keep going and eventually start moving other monsters, potentially even the one that just got shot.
Despite the cries of anguish from javascript purists, my first idea was to implement a delay() function, forcing javascript to wait until my animation drawing was done before continuing. Javascript purists hate this because you will be blocking the entire browser (or tab?) while this is happening. I figured when programming a game, this was OK, because it's not like anything else is happening on the page other than the game.
Unfortunately if you use this method ROT.js will not ever get a chance to run it's display handling code, and your canvas will not update until after your terrible delays are over.
Using setTimeout is a pain in the ass. It makes coding very messy because you have to use callbacks, something like this:
player.shoot(monster, weapon)}
game.drawAnimation(player, weapon, function () {
game.damage(monster)
if monster.is_dead() then
game.kill(monster)
Using this method I was still running into asynchronous event issues, and gods help you if you want to nest your animations (like a shooting rocket causing an explosion).
After a lot of messing about, I came up with a relatively elegant solution. It might be obvious to some, but no amount of googling on my part led me to anything similar.
You have to treat your animations as game actors. They need to have turns just like the player and monsters. They use the same drawing routines, screen updating routines, etc.
Now my game loop looks something like this (in CoffeeScript, but you should get the idea):
endPlayerTurn: () ->
@nextTurn()
nextTurn: () ->
if @hasAnimations()
first_animation = @animations[0]
@animationTurn(first_animation)
return
next_actor = @scheduler.next()
if next_actor.group == "player"
@finishEndPlayerTurn()
return
if next_actor.objtype == "monster"
monster = next_actor
@intel.doMonsterTurn(monster)
@finishEndPlayerTurn()
@nextTurn()
return
animationTurn: (animation) ->
animation.runTurn(@, @ui, @my_level)
if not animation.active
@removeAnimation(animation)
@finishEndPlayerTurn()
setTimeout(=>
@nextTurn()
50)
return
So, now my code is much more readable. Players and monsters take turns as per usual, using the ROT.js scheduler. Animations are independent actors that always take their turns first, if any are still alive. Within each animation there is a runTurn() method that calls the relevant game code: draw a rocket, draw a flash of light, move an expanding circle outwards, etc. The setTimeout is there but feels much less intrusive to me this way.
The code exits out to the main ROT.js loop elegantly, and does not hold up any operation or freeze the browser. The last step is to just make sure the player can't move or start a new turn until the animation is over (this last part is somewhat optional, in my experience roguelikes are not that fast-paced anyways).
The tricky thing is thinking of animations as game actors. The player may launch a rocket or a laser, but it will be the actual laser actor that calls the attack() or damage() functions. It is a little tricky to get used to, but I find it much more simple and easy to code than worrying about callbacks.
Your mileage may vary, but I wanted to put this out there so the next developer trying to solve this problem can save a few days of refactoring hell.
Thanks so much for writing this! I'm about to run into this issue myself in my own JS + ROT.js roguelike and have been trying to figure out how to handle it...I eventually landed on a similar solution, but mine involved keeping track of a list of animations to play on the player's turn, and then sequentially running through them. I realized that this would probably not work because all the other actors would move, then animations would play and might look goofy since some of the actors might not be there any more. Your solution takes care of that. Thanks for sharing your ideas!
ReplyDelete