Feb 6 2023

2D Sprite Animation using ECS

2D Sprite Animation using an Entity-Component-System Architecture


Background

I began working on a 3-D game Engine a while back and it slowly began to grow unwieldy. There was no (sensible) architecture and it really just fell apart. It was a fairly peculiar engine, there was some animation but it was all LERP or Simulation (Gerstner Wave) based. After realizing I was a bit beyond my ability, I decided to tackle a much smaller 2-D game to wrap my head around game engines, their structure, and how they work. After listening to probably one too many Mike Acton talks, I decided to look into Entity Component Systems. It’s a pattern I’ve heard mentioned everywhere and never really understood, and figured what better way to understand it than implement it. If you would like a good background on the general ECS problem, here is a good video from Catherine West at RustConf 2018 explaining it.

After setting up the general ECS architecture, OpenGL backend, etc. animation was the first interesting problem that I ran into that I’ve never encountered before, and here’s a documentation of my solution.

Problem Description

In 2-D sprite based art, you have a sprite sheet and if it’s animated, then some sprites are meant to be played sequentially at a certain speed. Like pictured:

The above picture is only the sprite indices for the running state of the player. The player can have states for Idle, Walking, Running, Attacking, etc. How can you switch between these states and animate through their respective sprite indices?

Solution

Data

Since we’re talking about ECS, it’s only fitting for us to describe the nature of our data first and foremost. It is the most important part. We will come to the Component/System code after. Already mentioned above is the sprite sheet with the RUN state of the player. It’s 6 sprite indices (we will call frames from here on out) are [8, 9, 10, 11, 12, 13]. Also an important note on the data: the animation loops When frame 13 is exhausted, it should restart at frame 8. For each of the following frames we also need to maintain how much time has passed since rendering that frame. If we let the animation system continuosly run through the frames on every GameState update, it would be too fast, so we need to limit that by keeping track of how long that frame has been rendered.

The following AnimationFrame struct is how we are going to store the data for a specific Animation frame:

typedef struct AnimationFrame
{
	float    elapsed_time;
	uint32_t sprite_index;
} AnimationFrame;

It is composed of the 2 properties:

  1. elapsed_time: Everytime we update our animation system, we will increment this elapsed time by the Game Engine’s delta time dt variable. This defaults to 0.
  2. sprite_index: The Index into our sprite sheet.

Here is the JSON data we have that maps directly to the AnimationFrame struct above:

"STATE": "RUN",
"frame_list": [
    {
        "elapsed_time": 0.0,
        "sprite_index": 8
    },
    {
        "elapsed_time": 0.0,
        "sprite_index": 9
    },
    {
        "elapsed_time": 0.0,
        "sprite_index": 10
    },
    {
        "elapsed_time": 0.0,
        "sprite_index": 11
    },
    {
        "elapsed_time": 0.0,
        "sprite_index": 12
    },
    {
        "elapsed_time": 0.0,
        "sprite_index": 13
    }
]

Component

Now we need a way to store an animation STATE and it’s associated AnimationFrame List. This is where the AnimationComponent comes in.

typedef struct AnimationComponent
{
	float animation_speed;
	size_t current_frame;
	std::string current_state;

	std::unordered_map<std::string, std::vector<AnimationFrame>> state_map; 

	void transition_state(std::string new_state);
} AnimationComponent;

The AnimationComponent is essentially a State Machine for whatever Entity it belongs to! Here are the following properties:

  1. animation_speed: This is how long each frame lasts. If a frame’s elapsed_time goes over the Animation Speed, we go to the next frame!
  2. current_frame: The index into the frame list of the current frame
  3. current_state: The current state represented by a string. E.g. “RUN”
  4. state_map: The mapping of state to frame lists. E.g. state_map["RUN"] = { /* frame_list from JSON above */ };
  5. We can ignore the transition_state(…) method.

System

The System is where most of the logic happens and here is the animation_system.hpp file:

typedef struct AnimatedEntity {
	AnimationComponent& animation_component;
	SpriteComponent& sprite_component;
} AnimatedEntity;

typedef struct AnimationSystem {
	void update(EntityManager& entity_manager, float dt);
} AnimationSystem;

void update_sprite(AnimatedEntity entity, float dt);

The struct itself is fairly simple, it only has one update() method that takes in an entity_manager and a dt. Outside of that are some utility Structs/Functions that help us in the implementation. Which can be seen here:

void AnimationSystem::update(EntityManager& entity_manager, float dt)
{
	tag_t animation_tag = (flags::animation | flags::sprite);

	size_t num_entities = entity_manager.num_entities();
	for (entity e = 0; e < num_entities; e++)
	{
		if ((entity_manager.tags[e] & animation_tag) != animation_tag) { continue; }

		AnimatedEntity entity =
		{
			entity_manager.animation_components[e].value(),
			entity_manager.sprite_components[e].value()
		};
		update_sprite(entity, dt);
	}
}

I won’t delve into the details of the update method, as it’s fairly irrelevant and just doing some iterations over the entities and grabbing the relevant Components if that entity conforms to the required composition. The real animation work is being done in the update_sprite(...) method below:

void update_sprite(AnimatedEntity entity, float dt)
{
	// 1.  Get references to the Entity Components
	AnimationComponent& anim_comp = entity.animation_component;
	SpriteComponent& sprite = entity.sprite_component;

	// 2. Get current Animation state List and relevant frame
	size_t& frame_index = anim_comp.current_frame;
	std::vector<AnimationFrame>& frame_list = anim_comp.state_map[anim_comp.current_state];

	// 3. Restart if we are over the list size
	if (frame_index >= frame_list.size())
		frame_index = 0;

	// 4. Get Current Frame
	AnimationFrame& current_frame = frame_list[frame_index];

	// 5. Update values for current State
	current_frame.elapsed_time += dt;
	sprite.sprite_index = current_frame.sprite_index;

	// 6. If we have animated over the current frame's time, go to the next animation step, and reset the frame's elapsed time
	if (current_frame.elapsed_time >= anim_comp.animation_speed) {
		frame_index++;
		current_frame.elapsed_time = 0.0f;
	}
}

I will now lay out the relevant steps here:

  1. We are simply unpacking the struct into it’s components to make it a bit easier.
  2. We are grabbing the current Animation Frame Index and Animation Frame List of the current state. The frame_index is the index of the frame_list.
  3. If the Frame is at the end of the list, simply restart at the beginning!
  4. Now we want to get the current Frame from the Frame list so we can update/grab any values in it (the elapsed_time or sprite_index properties).
  5. Add the delta time dt variable to the elapsed_time and update the Sprite’s Index from the Frame. The sprite/sprite_index are used by the renderer to index the texture, that’s all.
  6. Here is where we check the current_frame.elapsed_time against the Component’s animation speed. If the current frame has been rendered over the speed limit, then we know we must go to the next frame in our frame list. So we increment the index, and reset it’s elapsed_time so that next go around, we start right at the beginning of the frame.

Result

Here’s the final result in a gif!