Tutorial 13: Sprite Sheets and Frame Animation
What Is a Sprite Sheet?
A sprite sheet (also called a texture atlas or sprite strip) is a single image file that contains multiple animation frames arranged in a grid. Instead of loading one file per frame, you load one PNG and select different rectangular regions of it to draw each frame.
Benefits:
- Fewer GPU texture switches —
SpriteBatchbatches draws to the same texture more efficiently. - Fewer file I/O operations — one asset load instead of dozens.
- Easier asset management — all frames of an animation live in one file.
A typical 4-frame walk cycle sprite sheet (each frame 64×64) looks like:
// Sheet layout (256 x 64 pixels total):
// +--------+--------+--------+--------+
// | frame0 | frame1 | frame2 | frame3 |
// | 64x64 | 64x64 | 64x64 | 64x64 |
// +--------+--------+--------+--------+
// x=0 x=64 x=128 x=192
Source Rectangle (Rectangle Struct)
Rectangle defines a rectangular region in screen or texture coordinates with four integer members: X, Y, Width, Height.
#include "Microsoft/Xna/Framework/Rectangle.hpp"
Rectangle r(10, 20, 64, 64); // left=10, top=20, width=64, height=64
// Access members
int x = r.X; // left edge
int y = r.Y; // top edge
int w = r.Width;
int h = r.Height;
// Derived properties
int right = r.getRight(); // X + Width
int bottom = r.getBottom(); // Y + Height
Point centre = r.getCenter();
// Collision / containment
bool inside = r.Contains(Point(50, 50));
bool overlap = r.Intersects(Rectangle(0, 0, 100, 100));
To draw frame 2 (x=128) from a 64×64 sprite sheet:
Rectangle frame2src(128, 0, 64, 64); // x=128, y=0, w=64, h=64
spriteBatch_->Draw(*sheetTex_, position_, frame2src, Color::White);
Frame-Based Animation
Animation works by advancing the source rectangle over time. Given a sheet with N frames of width W arranged in a single horizontal row:
const int frameWidth = 64;
const int frameHeight = 64;
const int frameCount = 4; // 4-frame walk cycle
float frameDuration = 0.12f; // seconds per frame (about 8fps)
int currentFrame_ = 0;
float frameTimer_ = 0.0f;
// In Update():
void MyGame::Update(GameTime& gameTime) {
float dt = static_cast<float>(gameTime.getElapsedGameTime().getTotalSeconds());
frameTimer_ += dt;
if (frameTimer_ >= frameDuration) {
frameTimer_ -= frameDuration;
currentFrame_ = (currentFrame_ + 1) % frameCount; // loop
}
}
// In Draw():
void MyGame::Draw(const GameTime&) override {
Rectangle src(currentFrame_ * frameWidth, 0, frameWidth, frameHeight);
spriteBatch_->Draw(*sheetTex_, position_, src, Color::White);
}
This advances one frame every frameDuration seconds and wraps back to frame 0 after the last frame.
AnimationPlayer Pattern
When your game has multiple animations (idle, walk, run, jump, attack), encapsulate the animation logic in a reusable class. Here is a complete AnimationPlayer:
Animation.hpp
#pragma once
#include "Microsoft/Xna/Framework/Rectangle.hpp"
struct Animation {
int frameWidth;
int frameHeight;
int frameCount;
int sheetRow; // row in the sprite sheet (y = sheetRow * frameHeight)
float frameDuration; // seconds per frame
bool loop = true; // loop or play once
// Get the source rectangle for a given frame index
Rectangle GetSourceRect(int frame) const {
return Rectangle(
frame * frameWidth,
sheetRow * frameHeight,
frameWidth,
frameHeight
);
}
};
AnimationPlayer.hpp
#pragma once
#include "Animation.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteEffects.hpp"
class AnimationPlayer {
public:
void Play(const Animation& anim) {
if (&anim != currentAnim_) {
currentAnim_ = &anim;
currentFrame_ = 0;
frameTimer_ = 0.0f;
finished_ = false;
}
}
void Update(float dt) {
if (!currentAnim_ || finished_) return;
frameTimer_ += dt;
if (frameTimer_ >= currentAnim_->frameDuration) {
frameTimer_ -= currentAnim_->frameDuration;
currentFrame_++;
if (currentFrame_ >= currentAnim_->frameCount) {
if (currentAnim_->loop) {
currentFrame_ = 0;
} else {
currentFrame_ = currentAnim_->frameCount - 1;
finished_ = true;
}
}
}
}
void Draw(SpriteBatch& batch, Texture2D& sheet,
Vector2 position, SpriteEffects effects = SpriteEffects::None) {
if (!currentAnim_) return;
Rectangle src = currentAnim_->GetSourceRect(currentFrame_);
batch.Draw(sheet, position, src, Color::White,
0.0f, Vector2::Zero, 1.0f, effects, 0.0f);
}
bool IsFinished() const { return finished_; }
private:
const Animation* currentAnim_ = nullptr;
int currentFrame_ = 0;
float frameTimer_ = 0.0f;
bool finished_ = false;
};
Timer-Based Frame Switching
Here is a complete game that loads a sprite sheet and plays a walk animation using AnimationPlayer:
#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteEffects.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Animation.hpp"
#include "AnimationPlayer.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;
class WalkDemo final : public Game {
public:
WalkDemo() : graphics_(this) {
graphics_.setPreferredBackBufferWidth(800);
graphics_.setPreferredBackBufferHeight(600);
}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Sprite sheet: 256x128, two rows of 4 frames each (64x64 per frame)
// Row 0: walk right
// Row 1: walk left (or use SpriteEffects::FlipHorizontally on row 0)
sheet_ = std::make_unique<Texture2D>("assets/character.png",
getGraphicsDeviceProperty());
// Define animations (row in sheet, number of frames, fps)
walkAnim_ = Animation{ 64, 64, 4, 0, 0.10f, true }; // 10fps walk cycle
idleAnim_ = Animation{ 64, 64, 2, 1, 0.40f, true }; // slow idle blink
// Start with idle
player_.Play(idleAnim_);
}
void Update(GameTime& gameTime) override {
float dt = static_cast<float>(gameTime.getElapsedGameTime().getTotalSeconds());
KeyboardState kb = Keyboard::GetState();
Vector2 input(0.0f, 0.0f);
bool moving = false;
SpriteEffects flip = SpriteEffects::None;
if (kb.IsKeyDown(Keys::A) || kb.IsKeyDown(Keys::Left)) {
input.X -= 1.0f;
moving = true;
flip = SpriteEffects::FlipHorizontally; // face left
}
if (kb.IsKeyDown(Keys::D) || kb.IsKeyDown(Keys::Right)) {
input.X += 1.0f;
moving = true;
}
if (kb.IsKeyDown(Keys::W) || kb.IsKeyDown(Keys::Up)) {
input.Y -= 1.0f;
moving = true;
}
if (kb.IsKeyDown(Keys::S) || kb.IsKeyDown(Keys::Down)) {
input.Y += 1.0f;
moving = true;
}
// Switch animation based on movement
if (moving) {
player_.Play(walkAnim_);
} else {
player_.Play(idleAnim_);
}
flip_ = flip;
// Move
if (input.Length() > 0.0f) input = Vector2::Normalize(input);
const float speed = 180.0f;
pos_ += input * speed * dt;
// Clamp to screen
auto& vp = getGraphicsDeviceProperty().getViewport();
pos_.X = std::clamp(pos_.X, 0.0f, static_cast<float>(vp.getWidth()) - 64.0f);
pos_.Y = std::clamp(pos_.Y, 0.0f, static_cast<float>(vp.getHeight()) - 64.0f);
// Advance animation
player_.Update(dt);
if (kb.IsKeyDown(Keys::Escape)) Exit();
}
void Draw(const GameTime&) override {
auto& device = getGraphicsDeviceProperty();
device.Clear(Color::CornflowerBlue);
spriteBatch_->Begin();
player_.Draw(*spriteBatch_, *sheet_, pos_, flip_);
spriteBatch_->End();
device.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> sheet_;
Animation walkAnim_;
Animation idleAnim_;
AnimationPlayer player_;
Vector2 pos_{368.0f, 268.0f};
SpriteEffects flip_ = SpriteEffects::None;
};
The demo:
- Uses WASD or arrow keys to move the character.
- Switches to the walk animation when moving, idle animation when still.
- Flips the sprite horizontally when moving left.
- Clamps the character inside the screen boundaries.
- The
AnimationPlayerhandles frame timing and looping automatically.
Multi-row sprite sheets
Extend the pattern to handle sheets with multiple animation rows by specifying sheetRow:
// Sheet layout:
// Row 0 (y=0): idle — 2 frames
// Row 1 (y=64): walk — 4 frames
// Row 2 (y=128): run — 6 frames
// Row 3 (y=192): jump — 3 frames (one-shot)
Animation idleAnim_{ 64, 64, 2, 0, 0.5f, true };
Animation walkAnim_{ 64, 64, 4, 1, 0.10f, true };
Animation runAnim_ { 64, 64, 6, 2, 0.07f, true };
Animation jumpAnim_{ 64, 64, 3, 3, 0.15f, false }; // plays once
// Play jump on spacebar press, auto-return to walk when finished
if (jumpPressed) player_.Play(jumpAnim_);
if (player_.IsFinished()) player_.Play(walkAnim_);
Congratulations — you have completed the CNA tutorial series! You now know how to open a window, use the game lifecycle, move objects with delta time, render sprites and text, handle keyboard and mouse input, and animate sprite sheets. From here, explore the documentation for audio, 3D rendering, and advanced effects.