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:
- if the player performs an attack, set
left_sto the attack animation "cooldown" value - do not allow other entities to move or attack if an attack animation is playing
- in
Entity.render, if an attack animation is playing (ifleft_sis 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:
- checking if the entity state is
Attacking - if it is, get a tile from the "big atlas" by
- switching on the entity type
- inspecting how much time is left in
left_srelative 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.