twpage

twpage

Roguelike Animations using Javascript and ROT.js

I am using javascript (CoffeeScript, actually) and the excellent ROT.js library to build the next version of RoboCaptain. ROT.js is great; it makes easy stuff really easy and hard stuff much more straightforward. It allows you to break away from messing around finely tuning your Field Of View algorithm or your AStar pathfinding routines and spend time building actual game content.

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

RoboCaptain: That's levitation, Holmes

Jetpacks! Chasms to fly over! Also melee weapons with special attack patterns! Also flying enemies! Also ammo-based guns!

Melee weapons are a bit different from systems. They take up a coveted slot, but you can carry them around when they aren’t in use. When activated, the current weapons all have special attack patterns (ala brogue). The blade will pierce multiple enemies, the flail will damage all surrounding enemies, and the hammer will temporarily stun your target.

Coming soon: radiation, fire, exploding barrels, and upgrades. And recharging. And rockets. And…

PS – I will need beta (alpha?) testers at some point this month. Shoot me a comment or e-mail (twpage AT gmail) if you are curious and want to help.

RoboCaptain 2.0

Just wondering if anyone still actually has this blog in a dusty corner of their RSS reader?

I dusted off my old 2011 7DRL RoboCaptain a few months ago and have made steady progress. The new engine utilizes the wonderful rot.js and is fully re-coded in CoffeeScript.

It looks kind of like this:


For the first time today I actually spent time “playing” the game for more than testing purposes. There are already some interesting tactics arising from having to utilize different systems. There is a lot of work to be done on balancing this mechanic. I think the original 7DRL struck a chord with lots of people, but having to constantly switch between systems in order to recharge them seemed very fiddly to me by the end.

The current mechanic modifies the original one. You don’t have to worry (as much) about recharging your systems, but instead you need to pick which one is currently ACTIVE. 

Some systems function whether or not they are active, but being activated makes them better. Active lasers will have 2x change of critical hits. Active shields will recharge over time. Active melee weapons will perform special attacks like shoving enemies back or piercing through multiple enemies (thanks Brogue).

Other systems only function while active. The cloak is an obvious one. The promised jetpack also makes an appearance (jet-punch is a must-have feature). When activated, the Armor system makes your melee attacks do double damage and allows you to smash through walls — very satisfying.

I’m still working out how to balance the two different kinds of systems with things like melee weapons, limited ammunition weapons, and rockets. I think melee weapons will become systems. Since you are limited to 5 or 6 systems, this will become a strategic choice. I may also add ammo-based weapons that are designed to be disposable, but have special features when compared to the default laser. In theory you could even drop your lasers and do (for example) a melee/cloak-only build. Or even leave your shields behind and stack your robo-suit with 4 weapons systems.

Currently I decided that you can’t “carry” systems around to install later, so the strategic choices around which 5 (or 6) to keep installed is an important one.

Anyways, if you are still listening, drop me a line. I will need beta testers soon enough.

Cheers.

HF

Finally!! I Beat Brogue

YEEEEAAAAH!!

After 2 months of obsession intense focus, I finally beat brogue. (Not so coincidentally only two days after Andrew Doull managed a win.)

Brogue is by far my favorite roguelike, and in the running for favorite “game” overall these days. The current version (1.6.4) really hits the sweet spot in terms of difficulty, and showing you “cool stuff” in the game. Basically that means that you are always just barely getting farther than you have before, and always seeing something cool and new, which in turn drives you forward.

After a particularly grueling pair of almost-wins, my actual victory game turned out to be (mostly) a breeze. I spent the first 12 levels with no weapon but the starting dagger, and no pet.

This was pretty embarassing…

On level 13 I found:

  1. A sword
  2. A captive ogre
  3. A +3 Ring of Awareness
  4. A cage zoo
I (ab)used my wand of beckoning that I had been carrying around to free a dar priestess and a troll. Now I was set. The ogre didn’t last long, but the dar priestess survived, and learned flight. I had been waiting for a good moment to use my wand of plenty that I had been carrying around all game, and then I had 2 flying dar priestesses. The troll later turned invisible which is surprisingly NOT useful when you have other pets that cannot see it… with little no regret I polymorphed the troll into a toad and left him to wander the dungeon.
About level 18 or 19 I found a golem. Between the golem and the two priestesses I basically had the perfect tank-healer-dps combination. After I (we?) plowed through two tentacled Horrors in a row, I knew I had a lock on victory… assuming I didn’t do anything stupid!’
For the rest of the game it was just a matter of aggressively destroying any discord-casting enemies (Lich, dar battlemage, pixie) and letting my pets do the rest. Eventually I found a war pike and was able to deal some decent damage of my own. Tentacled Horrors, Dragons, and Golems all fell quite easily. Revenants and Liches are much easier to kill when one of your allies can cast negate. The staff of turret destruction tunneling, which I had attained on level 1 or 2, was incredibly helpful in getting through the rest of the levels quickly and safely.
The rest of the game went something like this
A couple of other things I learned along the way:
  • Plate armor really is worth it. I never knew!
  • Some pets are not worth rescuing (dar battlemage, salamander)
  • Centaurs are easily foiled by doors
  • A staff of tunneling is surprisingly useful
  • You don’t have to pick up every key!
  • Sometimes it is OK to just take the stairs without exploring every last room…
My thanks to Pender for creating such an amazing game. Now I can relax and return to my regularly scheduled development work!

Adventures in UI Design

Traditional roguelike games are restricted in what they can display graphically. Restrictions are part of what make roguelikes great (this is definitely true for the original rogue), but graphics no longer have to be one of them. While troubleshooting the firing logic for robocaptain I found myself stuck in a “traditional” mode of thinking.

In the original 7DRL version of the game, firing took two steps: ‘f’ to confirm target, and ‘f’ again to fire. Alternatively, once ‘f’ was pressed the first time, the player could hit ‘g’ to cycle between targets. This is mostly fine, but can be improved. While I mostly play with the mouse, I want the keyboard controls to be just as good.

The thing that ‘unstuck’ me was realizing I wasn’t limited to conventional roguelike graphical restrictions. The restriction being ‘1 tile, 1 character’. Why not just add another indicator over top of the existing character?

I came up with a simple ‘targeting’ overlay that constantly tracks the closest (or last hit) enemy. This targeting overlay can be cycled at any time by hitting ‘g’. Hitting ‘f’ will automatically shoot at the currently targeted enemy.

This kind of ‘passive tracking’ saves the player an extra keystroke, since most of the time you are shooting the only/closest enemy. The rest of the time you can use ‘g’ to cycle between targets just as before. The game will remember the last enemy you shot at and always keep them as the default target.