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:
-
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. - 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:
-
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! - current_frame: The index into the frame list of the current frame
- current_state: The current state represented by a string. E.g. “RUN”
-
state_map: The mapping of state to frame lists. E.g.
state_map["RUN"] = { /* frame_list from JSON above */ };
- 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:
- We are simply unpacking the struct into it’s components to make it a bit easier.
-
We are grabbing the current Animation Frame Index and Animation Frame List of the current state. The
frame_index
is the index of theframe_list
. - If the Frame is at the end of the list, simply restart at the beginning!
-
Now we want to get the current Frame from the Frame list so we can update/grab any values in it (the
elapsed_time
orsprite_index
properties). -
Add the delta time
dt
variable to theelapsed_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. -
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’selapsed_time
so that next go around, we start right at the beginning of the frame.
Result
Here’s the final result in a gif!