The Zone.js - Animating stuff

| 3 min read

On the spirit of not writing posts every bunch of months with a ton of updates I will be focusing this one on a rather critical but small update I have been working on in the past few days. Introducing animations into the game.

The problem

They way I had the game setup, the game loop was completely dependent on the users input. Something along this lines:

pre animation

Whenever an input was detected, the game moved forward one turn and the magic happened. In between time was frozen, a dead world awaiting its master's orders.

And how do you make things move in a frozen world? You can't, that's how 🤷🏻‍♂️.

Decoupling rendering

If we want stuff to move around, we needed a lively display doing it's thing no matter how turns pass. Instead of freezing time. We need reality to keep going as expected but our minions to keep waiting for our orders to do stuff. A.k.a we need this to happen:

animation

First of all we need to untie the render from the game logic itself. Which proved to be pretty easy (and gave me a sudden spark of hope that the code was somewhat well designed).

Every time an input is pressed a turn passes and the Game moves a step forward:

bus.subscribe(EVENTS.TURN_PASSED, (action) => {
game.runMainLoop(action);
});

The renderer reads the Game's computed state and paints pretty things on the screen:

export const render = (game) => {
TileRenderer.run(game.state);
ui.update(game.state);
};

No matter what the games does, this last thing will happen continuously. I'm capping this little guy to 30 FPS because I don't need more.

const FPS_CAP = 30;

const runFrame = () => {
render(game);

setTimeout(() => {
requestAnimationFrame(runFrame);
}, 1000 / FPS_CAP);
};

First task accomplished! Everything works like before but we have a render clock to play with completely separate from the games main logic loop.

Spawning animations

Now that we have the ability to paint stuff on the screen let's make use of our new acquired powers!

First of all we need to know when something animation worthy happened. So we let our Combat system emit an event whenever someone is hit, with the information of where it happened:

static _attack(target, bus) {
// combat stuff
bus.emit(EVENTS.HIT, {x: target.position.x, y: target.position.y})
//
}

Our trusty bus is listening to this information (outside of the Game class) and adds a new animation to the queue whenever something worthy happens:

bus.subscribe(EVENTS.HIT, ({ x, y }) => {
animations.add(new HitAnimation(x, y));
});

And this is where stuff gets interesting...

The animation queue

The animation queue is the vital piece of all of this system. It takes care of computing the necessary tiles that must be painted on screen for every given frame.

pre animation

It has this shape:

export class AnimationQueue {
animations: Array<Animation>;

add(animation) {
// add animation to queue
}

composeNextFrame(): Matrix {
// iterate over each animation
// if animation finished, remove it from the queue
// if it is still unfinished, retrieve current frame
// compose the current frame of all animations
// return the result of all combined frames as a Matrix
}
}

It basically goes over each animation and asks for its current frame. Each animation in turn is a series of Matrixes containing the desired tiles in sequence. Then it is just a matter of letting the display know that that tile must be combined with the rest of the background, entities or whatever tiles already in place.

pre animation

For example, the Hit animation places a "hit" tile at a given position for a single frame.

export class HitAnimation extends Animation {
frames: Matrix[];

constructor(x: number, y: number) {
super();
this.frames = [new Matrix().setValue(x, y, TILES.hit)];
}
}

When the animation queue processes it, it will return the hit tile for the current frame. In the following frame it will be removed, thus resulting in the "blink" animation that you can see here:

hit

Such a lengthy and technical post for a pixel blinking, anticlimatic ain't it?!? 😄

Resources

I was completely lost at the start of making this. Couldn't have done it without the hint from this r/roguelike dev comment:

The modern way to do animations is basically to write your core game loop as if your game was real-time. Set a frame rate like 30 or 60 fps, update the screen at that pace, and run the idle animations every frame as long as the game is running. Then you put the turn-basedness on top of that by making all the game logic that matters stand still while waiting for player input.

So big props to the author!


  • 🐙 You can see the code here
  • 👾 You can play the latest build here

All code in this post has been simplified for clarity. You can check the full code at the time of writing this article in this commit

Vilva.