I'm back, and now only slightly sleep deprived! Yes, my Steam Deck is awesome (and Baby Mk II is doing great).
To celebrate, I decided to add some animations to player and enemy characters. They actually run and jump now!
Quality Signals
On the one hand, animations don't solve the most pressing problem this game has: the lack of a highly-compelling gameplay loop.
But on the other hand, characters "running" with all the smoothness of a floating statue makes it difficult to take this game seriously:
So working on gameplay is more important ultimately, but I do need to meet some minimum bar to get folks to play at all. 1
Animation for Monkey Brains
In 3D land, animation requires figuring out keyframes, interpolation types, root motion, animation blending, Blender's improved-but-still-tricky controls, constantly fighting with the viewport camera to get it repositioned just right, and god knows what else.
In 2D-pixel-art land, you draw some pictures and cycle between them, like a flipbook:
So, to animate a character, you just:
- Draw some images.
- Define how rapidly those images need to be swapped out. 2 3
- Write some code to define which sequence of images should be displayed when a character runs, jumps or falls.
Animating Rollback Networking
Except, we also have rollback networking4, which adds two complications:
- Our game's gameplay logic can't actually directly interact with our game's drawing logic (because the gameplay logic has to be able to run without drawing when fast-forwarding after a rollback).
- When rolling back to a past game state, the animations also should be rewound, to avoid playing the wrong animation.
I solved that by processing each frame of animation to be displayed for a certain number of ticks, and in the game state each animated character only keeps track of its current animation name5 and how many ticks6 the character has been in that animation state.
Then, for each character, the drawing logic of the game can use the stored animation name and tick count to choose the right frame to draw - which makes the drawing logic blissfully oblivious to any rollbacks. 7
Anyway, that was dangerously close to talking about code (ewww), so here's what it looks like:
Somehow my ladder animations make player characters look cute and chubby - but at least you can tell that they're climbing up or sliding down.
Attack animations are still a work in progress: those need to have wind-up and reload animations associated with them, which the game doesn't support yet. 8 9
Playable web build
Try running around as an unintentionally-cute-and-marshmallow-esque animated player character:
FYI: I've disabled online web multiplayer here just so I don't have to worry about maintaining backwards- or forwards-compatibility in the matchmaking server.
Having animations in the game will also help me to launch a "Coming Soon" page on Steam for my game that's only "fairly embarrassing" rather than "entirely mortifying".
If you're using Aseprite (which I recommend) then you can define this in-app as the number of milliseconds to display each frame, from which you can derive a frames per second number.
Surprisingly there's no real consensus on the "right animation speed" for pixel art. I ended up animating most animations at around 12 frames per second, which feels pretty smooth.. but maybe it's a bit too high to stick with (higher frames per second means more animating work!). Strategically altering the duration of certain frames is apparently also a thing, which I played with in the animation above.
For those just joining us, rollback networking is where you predict what all the other (remote, far away over the internet) players are doing; if you mis-predict, you rewind ("roll back") the entire state of the world to when you made your mistake, factor in the corrected action from the remote player, then play the game state in fast forward to get back to the present time.
Actually, storing the full animation names is wasteful because they're heap-allocated strings, so each time the game world is copied (60 times a second) we'd be re-allocating all those strings too (or increasing a reference counter if using reference counted strings).
Instead, I take advantage of the fact that animation names aren't going to be removed ever* once they are loaded: I pre-process the animations to intern each string name as part of loading asset data, so that the game state can store a number representing each string instead.
(* well, animation names can be removed at runtime via hot-reloading assets, which would cause a crash. But that's not a problem for my players.)
A tick is a single forward-step of the game world.
Although it does mean that frames of animations are skipped if the game logic needs to run multiple times for a render (as tends to happen if rendering is the bottleneck), which is not so great.
Maybe someday I can find a best-of-both-worlds approach.
Currently the logic for selecting the correct animation is chosen "instantaneously" each tick, based on the movement state of a character.. which doesn't work when a character is standing still specifically because it's preparing to attack.
(This is also why none of the enemy animations have any Anticipation, like crouching down slightly before a jump.)
Also it's quite painful to synchronize a frame-by-frame attack animation with actually creating an (e.g.) in-game arrow at the right time and the right location, so I'm working on a better approach to that too.