Tutorial 13: Sprite Sheets and Frame Animation
What you’ll learn
- Cutting frames out of one texture with the source-rectangle
Drawoverload. - Advancing frames on a timer to produce an animation.
- The reusable animation-player pattern.
Before you start — Tutorial 08: Loading and Drawing Textures for the Draw overloads and Tutorial 12: Moving Sprites and Basic Animation for the per-frame timing this builds on.
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.getRightProperty(); // X + Width
int bottom = r.getBottomProperty(); // Y + Height
Point centre = r.getCenterProperty();
// 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.getElapsedGameTimeProperty().getTotalSecondsProperty());
frameTimer_ += dt;
if (frameTimer_ >= frameDuration) {
frameTimer_ -= frameDuration;
currentFrame_ = (currentFrame_ + 1) % frameCount; // loop
}
}
// In Draw():
void MyGame::Draw(const GameTime&) {
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"
// CNA's types live in namespaces; a using-declaration keeps the code below short.
// (In a larger project, put your own types in a namespace and qualify instead.)
using Microsoft::Xna::Framework::Rectangle;
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"
using Microsoft::Xna::Framework::Color;
using Microsoft::Xna::Framework::Vector2;
using Microsoft::Xna::Framework::Graphics::SpriteBatch;
using Microsoft::Xna::Framework::Graphics::SpriteEffects;
using Microsoft::Xna::Framework::Graphics::Texture2D;
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 <algorithm>
#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/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_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Sprite sheet: 256x128, two rows (64x64 per frame): row 0 has the
// 4-frame walk cycle, row 1 a 2-frame idle blink. Facing left is done
// with SpriteEffects::FlipHorizontally instead of a second set of frames.
// (A relative path is resolved against the working directory.)
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.getElapsedGameTimeProperty().getTotalSecondsProperty());
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().getViewportProperty();
pos_.X = std::clamp(pos_.X, 0.0f, static_cast<float>(vp.getWidthProperty()) - 64.0f);
pos_.Y = std::clamp(pos_.Y, 0.0f, static_cast<float>(vp.getHeightProperty()) - 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);
// The PNG is loaded straight from disk, so it has straight (not
// premultiplied) alpha: draw it with NonPremultiplied blending (Tutorial 22).
spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::NonPremultiplied);
player_.Draw(*spriteBatch_, *sheet_, pos_, flip_);
spriteBatch_->End();
// Game::EndDraw() presents the frame after Draw() returns.
}
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 core 2D track! 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. The next tutorial adds sound; after that the series continues with content, 2D techniques, 3D rendering and more — see the tutorial index, or explore the documentation for audio, 3D rendering, and advanced effects.