2026-01-19

09:14:17, atom feed.

This last week I focused on implementing basic animations.

Here is the curated commit log from last week:

2026-01-13
feat: simple animation impl: timers and sprite-select logic

2026-01-14
refactor: combine common elements of Cooldown and Animation to Timer

2026-01-14
refactor: destroy resources with defer in main.jai

2026-01-14
refactor: don't pass by pointer when we don't mutate the arg

2026-01-14
fix: repeated player actions no longer infinitely stall enemies

This introduces a player action cooldown that is set after the player
performs an action. If the player move cooldown is less than the
animation timer cooldown, the player can move faster than all other
entities and can repeatedly perform an action which triggers an
animation, causing enemies to repeatedly skip their turn while the
animation plays.

2026-01-15
feat: seed SDL's random number generator

2026-01-15
feat: add basic enemy attack; provide player ent. view in enemy updates

2026-01-15
feat: add enemy attack animations

Additionally:
- split passes through the ECS into timing/state updates and attack/move
  updates
- iterate across all entities when rendering animations
- compute time since last frame only once; remove perf count from timers
- remove cooldown and animation from components; replace with
  action_timer and animation_timer

For my first attempt at animations, I drew a couple sprites in Aseprite for a basic player attack and then introduced an animation struct that looked like

Animation :: struct {
  last_perf_count: u64;
  left_s: float;
}

where left_s was the number of seconds left in the animation timer and last_perf_count was the last performance counter value used to determine the delta-time to subtract from left_s, essentially the performance counter value captured in timer-update logic in the last frame. I obtain the performance counter value from SDL_GetPerformanceCounter.

My Entity.update_positions procedure evolved into Entity.update_states. This procedure is now responsible for updating multiple entity properties in the ECS. When implementing the ECS, I was forcing myself to minimize the number of times I looped through entities, but this resulted in loops having complicated branching and some branches using outer-scope variables updated by other branches, for example to determine whether the player had performed an action this frame. The logic for whether the player had performed an action also grew in complexity as I extended what an "action" meant--initially it was just movement, but now it is a simple attack coupled with an animation timer.

I had the obvious realization that I ought to just loop through the entities multiple times and update components (properties) more granularly. I searched around to see if this was the whole idea of ECS, and indeed, making multiple passes through the entities for more granular updates is one of the core tenets. My current organization--struct-of-arrays, the way I organize my components, does not leverage cache alignment well, and this seems to be a common pitfall of ECS implementations. The idea is that all data for a single property of all entities is contiguous in memory, so iterating over and updating that data is efficient because data locality means we won't have many cache misses. The reality is that updating some property P1 often depends on some other property P2, a simple example being updating an entity's position by performing some computation using an entity's instantaneous velocity, maybe taking into account some special item property, etc. You start to chase pointers here and there, and now you're benefiting less from cache locality of a single property.

Others have of course come to the same realization. Here is such a post. I agree with the author's general idea that structs for specific entity types instead of a ton of dynamic behavior are easier to debug and can take a project across the finish line. I haven't profiled the cost of creating one of my entity views, but the view idea does seem to address the difficult-to-debug nuance of the way entities are composed in memory.

Back to animations, I used the Animation struct as follows:

  1. if the player performs an attack, set left_s to the attack animation "cooldown" value
  2. do not allow other entities to move or attack if an attack animation is playing
  3. in Entity.render, if an attack animation is playing (if left_s is greater than 0), render an attack animation sprite; select the sprite to render based on how much time is left in the animation timer

This approach was before splitting looping through the entities into finer-grained passes. Two issues are immediately obvious: the player update must always come first (which I think is okay), but then when it comes time for an enemy to attack, how does the player or another enemy know when an attack is being performed? And, at render time, how do we render attack animations for anyone but the player?

The solution was an introduction of a state property to all entities and a new pass through the entities that updates timers and entity states. Any timer with time left in left_s gets its value reduced by time-since-last-frame, and in this loop we also set some outer-scope variables such as tracking whether an entity has a live animation timer and is thus attacking. If an attack animation is playing, the entire position-update pass through the entities is skipped.

I eventually got rid of Animation and just introduced Timer which is

Timer :: struct {
  left_s: float;
}

Instead of polling a new performance counter value for each timer on each frame, Game.update now determines the time since it was last called and stores this value in its game state so that other per-frame update logic can use it without recomputing it.

Now that passes through the entities for timer/state and movement are decoupled, it is also possible--and straightforward--to render animations for any entity, not just the player. When rendering, we select the animation tile to render, if any, by:

  1. checking if the entity state is Attacking
  2. if it is, get a tile from the "big atlas" by
    1. switching on the entity type
    2. inspecting how much time is left in left_s relative to some attack animation constant set elsewhere for the specific entity type

I've also added idle animations using similar logic.

The animations are not beautiful, just placeholder programmer art. You can see them here:

As mentioned in a previous post, pathfinding does support diagonal movement, and thus enemies can also attack diagonally, but I have not implemented support for diagonal facing directions. Thus, enemey attack animations will only be displayed in up-down-left-right directions, which you can see in the video. Enemy attack animation is just a single sprite with its opacity modulated as time left on the animation timer approaches zero. The player has a 2-sprite attack animation with opacity modulation, and both are controlled by time left on the animation timer. Rotation of the attack sprite for both entity types is done via SDL_RenderTextureRotated.

Lastly, I made some code quality updates. Instead of destroying resources at the end of some scope, I leveraged Jai's defer keyword, which I had forgotten about. I also seeded the random number generator, at least for development, so that I have reproducible entity-spawn positions. And finally, I removed many instances of pass-by-pointer in my signatures when this was not necessary. Passing by pointer in Jai indicates that you intend to modify the argument. Jai will automatically pass by pointer (to "const", in C++ parlance) behind the scenes if the argument is above some (internally-determined) size threshold.

From here, it's time for me to work on game mechanics and the general game design. There are many quality-of-life improvements to make as well, but I will probably leave lots of polishing for when the game matures more and it is time to do proper art.