Unlike rendering, I went into this jam with no clue how to do animation. Fortunately, it turns out to be fairly simple (in part thanks to my per-entity rendering setup.)
First, a brief bit of background, for anyone unfamiliar with rendering:
Each vertex drawn has three properties: position, color, and texture coordinates. The rest of the pixels draw are determined by linear interpolation between connected vertices. Which vertices are connected depends on the primitive type defined when we send the vertex buffer to the GPU. In Majo, everything is rendered as Quads (4 consecutive vertices create a single face.) Finally, by convention, the elements of the position vector are referred to by the familiar (x, y, z), but texture coordinates are labeled (u, v, w).
So, the following code renders a 16×16 square starting at the origin (top-left of screen), textured with a 32×32 block of the current texture, starting at the top-left of the texture image:
void Foo::render(sf::Vertex * quad) const
quad.position = sf::Vector2f(0, 0);
quad.position = sf::Vector2f(16, 0);
quad.position = sf::Vector2f(16, 16);
quad.position = sf::Vector2f(0, 16);
quad.texCoords = sf::Vector2f(0, 0);
quad.texCoords = sf::Vector2f(32, 0);
quad.texCoords = sf::Vector2f(32, 32);
quad.texCoords = sf::Vector2f(0, 32);
(There’s no requirement for texture size and actual rendered size to be related, but direct multiples prevent squashing and stretching.)
What this means is we can place multiple sprites on a single texture image (a spritesheet), and slice out the portion we want to render. Which means that *animating* these sprites is just a matter of changing which slice of the spritesheet we render.
A single frame of animation, then, is simply a rectangle indicating what part of the spritesheet to render:
sf::Vector2f uv; // Offset into the texture.
sf::Vector2f spriteSize; // Size of the sprite on the texture.
And an animation consists of a list of frames, with some bookkeeping regarding when to switch:
/* Container for a list of animation frames. */
std::vector<const AnimationFrame> m_frames;
// Constructors / Accessors elided.
/* Start the animation. */
/* Update call. Advances the animation. */
* Render the texture coords to quad.
* Does not touch position -- that is to be set by the entity.
void render(sf::Vertex * quad) const;
The render method allows the owning entity to pass a quad to render the appropriate uv coordinates (while still rendering position and color itself.)
We’re not done yet!
The problem is that this class is hefty: a four-frame animation weighs in at 76 bytes, plus overhead incurred by the vector class. Copying this class as a member of every Entity that uses an animation adds up to a lot of memory moving around. (The issue is less the memory usage, and more that allocating, copying, and deallocating that memory is not free.)
The obvious solution is to cache the Animation, and let each entity store a pointer to it. However, we now have a different problem: the timing information and current frame are shared between all entities using a given animation. (Since each entity calls Animation::update, this leads to undesired behavior.)
This leads to the core lesson I learned during this jam: data and state are different things. Separate them.
Our animation frames are data: we load them into memory once, and only read from them. On the other hand, the timing information is *state*, modified on each update. These are two separate concepts, so let’s carve out the stateful bits into their own structure:
// Methods elided
With a bit of trivial setup, each animated Entity now owns an AnimationState, as well as a pointer to an Animation (20 + 4 bytes per Entity on x86). Now we can cache and share Animations all we want, as they are constant data. Animation::render has its signature changed to accept an AnimationState passed in:
void render(sf::Vertex * quad, const AnimationState& state) const;
Over 1000 entities, this system saves ~54.6kB of memory. That’s not a lot. However, by enabling caching of Animations, we reduce the amount of initialization code involved, as well as the number of runtime copies/moves neccessary. Decreased memory usage is just a nice bonus.
Next time: design, scope, and some miscellaneous issues encountered over the week.