Tutorial 12: Moving Sprites and Basic Animation
What you’ll learn
- Storing position and velocity as
Vector2and integrating them each frame. - Multiplying velocity by delta time so speed does not depend on frame rate.
- Keeping a sprite inside the window with boundary checks.
- Adding rotation and scale to a
Drawcall.
Before you start — Tutorial 08: Loading and Drawing Textures (a texture to move), Tutorial 10: Handling Keyboard Input (to steer it) and Tutorial 05: The Game Loop (for delta time).
Storing Position with Vector2
Vector2 is the standard type for storing 2D positions, velocities, and sizes in CNA. It matches the XNA 4.0 Vector2 exactly.
#include "Microsoft/Xna/Framework/Vector2.hpp"
// Create a position
Vector2 position(400.0f, 300.0f); // x, y
// Access components
float x = position.X;
float y = position.Y;
// Modify
position.X += 10.0f;
position.Y -= 5.0f;
// Arithmetic
Vector2 velocity(100.0f, 50.0f);
Vector2 newPos = position + velocity * dt; // dt = delta time in seconds
// Common constants
Vector2 zero = Vector2::Zero; // (0, 0)
Vector2 one = Vector2::One; // (1, 1)
Vector2 unitX = Vector2::UnitX; // (1, 0)
Vector2 unitY = Vector2::UnitY; // (0, 1)
Vector2 operations you will use frequently:
float length = velocity.Length(); // magnitude
float dist = Vector2::Distance(posA, posB); // distance between points
Vector2 norm = Vector2::Normalize(velocity); // unit vector
Vector2 lerp = Vector2::Lerp(posA, posB, 0.5f); // midpoint
float dot = Vector2::Dot(dirA, dirB); // dot product
Velocity-Based Movement
The fundamental pattern for moving any game object:
// Class members:
Vector2 position_{400.0f, 300.0f}; // pixels
Vector2 velocity_{150.0f, 100.0f}; // pixels per second
// In Update():
void MyGame::Update(GameTime& gameTime) {
float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
// Position = Position + velocity * delta_time
position_ = position_ + velocity_ * dt;
}
This gives you exactly the right units: if velocity_.X = 150.0f, the object moves 150 pixels per second horizontally, regardless of frame rate.
Player-controlled movement
void MyGame::Update(GameTime& gameTime) {
float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
KeyboardState kb = Keyboard::GetState();
Vector2 input(0.0f, 0.0f);
if (kb.IsKeyDown(Keys::Left) || kb.IsKeyDown(Keys::A)) input.X -= 1.0f;
if (kb.IsKeyDown(Keys::Right) || kb.IsKeyDown(Keys::D)) input.X += 1.0f;
if (kb.IsKeyDown(Keys::Up) || kb.IsKeyDown(Keys::W)) input.Y -= 1.0f;
if (kb.IsKeyDown(Keys::Down) || kb.IsKeyDown(Keys::S)) input.Y += 1.0f;
// Normalize diagonal movement so you don't move faster diagonally
if (input.Length() > 0.0f) input = Vector2::Normalize(input);
const float speed = 200.0f;
position_ = position_ + input * speed * dt;
}
Boundary Checking
Keep the sprite within the screen bounds. You need the sprite size (texture width/height) to avoid partial clipping at the edges:
// Sprites are 64x64 pixels
const float spriteW = 64.0f;
const float spriteH = 64.0f;
auto& vp = getGraphicsDeviceProperty().getViewportProperty();
float screenW = static_cast<float>(vp.getWidthProperty());
float screenH = static_cast<float>(vp.getHeightProperty());
// Clamp so the sprite stays fully inside the screen
position_.X = std::clamp(position_.X, 0.0f, screenW - spriteW);
position_.Y = std::clamp(position_.Y, 0.0f, screenH - spriteH);
Bouncing off edges
// Check horizontal edges
if (position_.X < 0.0f) {
position_.X = 0.0f;
velocity_.X = std::abs(velocity_.X); // bounce right
}
if (position_.X + spriteW > screenW) {
position_.X = screenW - spriteW;
velocity_.X = -std::abs(velocity_.X); // bounce left
}
// Check vertical edges
if (position_.Y < 0.0f) {
position_.Y = 0.0f;
velocity_.Y = std::abs(velocity_.Y); // bounce down
}
if (position_.Y + spriteH > screenH) {
position_.Y = screenH - spriteH;
velocity_.Y = -std::abs(velocity_.Y); // bounce up
}
Smooth Movement with Delta Time
A common need is smoothly interpolating from one position to a target — for camera follow, enemy AI, and menu animations:
// Exponential decay (feels smooth and physical)
// Higher lerpSpeed = snappier, lower = slower/floaty
float lerpSpeed = 5.0f;
float t = 1.0f - std::exp(-lerpSpeed * dt); // frame-rate-independent lerp
position_ = Vector2::Lerp(position_, targetPosition_, t);
This is sometimes called "smooth follow" or "exponential smoothing". Unlike a constant lerp amount, exp(-speed * dt) is frame-rate-independent.
Rotation
Rotation in CNA (and XNA) is in radians, clockwise. Pass the rotation to SpriteBatch::Draw() along with an origin point.
// Class members:
float rotation_ = 0.0f;
float rotationSpeed_ = MathHelper::Pi; // 180 degrees per second
// In Update():
rotation_ += rotationSpeed_ * dt;
// Wrap to [0, 2pi] to avoid floating-point drift
if (rotation_ > MathHelper::TwoPi) rotation_ -= MathHelper::TwoPi;
// In Draw():
// Origin at texture centre so the sprite rotates around its own centre
Vector2 origin(tex_->getWidthProperty() / 2.0f, tex_->getHeightProperty() / 2.0f);
spriteBatch_->Draw(*tex_,
position_, // position of origin point (centre of sprite)
std::nullopt, // source rect (whole texture)
Color::White,
rotation_, // radians
origin, // pivot in texture space
1.0f, // scale
SpriteEffects::None,
0.0f // depth
);
Useful angle constants from MathHelper:
| Constant | Value | Degrees |
|---|---|---|
MathHelper::Pi | 3.14159... | 180° |
MathHelper::TwoPi | 6.28318... | 360° |
MathHelper::PiOver2 | 1.5707... | 90° |
MathHelper::PiOver4 | 0.7853... | 45° |
Convert degrees to radians: MathHelper::ToRadians(90.0f) returns Pi / 2.
Scaling
Scale a sprite by passing a float (uniform) or Vector2 (non-uniform) to SpriteBatch::Draw():
// Class members:
float scale_ = 1.0f;
float targetScale = 2.0f;
float scaleSpeed = 1.5f; // scale units per second
// Uniform scale — grows toward targetScale
scale_ = std::min(scale_ + scaleSpeed * dt, targetScale);
// In Draw():
Vector2 origin(tex_->getWidthProperty() / 2.0f, tex_->getHeightProperty() / 2.0f);
spriteBatch_->Draw(*tex_, position_, std::nullopt, Color::White,
0.0f, origin, scale_, SpriteEffects::None, 0.0f);
Pulsing scale effect (using a sine wave):
// In Draw():
double t = gameTime.getTotalGameTimeProperty().getTotalSecondsProperty();
float pulseScale = 1.0f + 0.2f * std::sin(static_cast<float>(t * 4.0)); // pulse ±20%
spriteBatch_->Draw(*tex_, position_, std::nullopt, Color::White,
0.0f, origin, pulseScale, SpriteEffects::None, 0.0f);
Full bouncing ball demo
class BouncingBall final : public Game {
public:
BouncingBall() : graphics_(this) {
graphics_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
setIsFixedTimeStepProperty(true);
setTargetElapsedTimeProperty(TimeSpan::FromSeconds(1.0 / 60.0));
}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Load a circle sprite from assets/ball.png (64x64)
ballTex_ = std::make_unique<Texture2D>("assets/ball.png",
getGraphicsDeviceProperty());
}
void Update(GameTime& gameTime) override {
float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
pos_ = pos_ + vel_ * dt;
const float r = 32.0f; // ball radius (half of 64px texture)
auto& vp = getGraphicsDeviceProperty().getViewportProperty();
float w = static_cast<float>(vp.getWidthProperty());
float h = static_cast<float>(vp.getHeightProperty());
if (pos_.X - r < 0) { pos_.X = r; vel_.X = std::abs(vel_.X); }
if (pos_.X + r > w) { pos_.X = w - r; vel_.X = -std::abs(vel_.X); }
if (pos_.Y - r < 0) { pos_.Y = r; vel_.Y = std::abs(vel_.Y); }
if (pos_.Y + r > h) { pos_.Y = h - r; vel_.Y = -std::abs(vel_.Y); }
rotation_ += 2.0f * dt; // spin while moving
if (rotation_ > MathHelper::TwoPi) rotation_ -= MathHelper::TwoPi;
if (Keyboard::GetState().IsKeyDown(Keys::Escape)) Exit();
}
void Draw(const GameTime&) override {
auto& device = getGraphicsDeviceProperty();
device.Clear(Color::Black);
Vector2 origin(32.0f, 32.0f); // centre of 64x64 texture
spriteBatch_->Begin();
spriteBatch_->Draw(*ballTex_, pos_, std::nullopt, Color::White,
rotation_, origin, 1.0f, SpriteEffects::None, 0.0f);
spriteBatch_->End();
device.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> ballTex_;
Vector2 pos_{400.0f, 300.0f};
Vector2 vel_{220.0f, 180.0f}; // pixels per second
float rotation_ = 0.0f;
};
The ball bounces around the screen, spinning as it moves. Position and velocity use delta time for frame-rate independence. The origin is set to the texture centre so rotation looks natural.
In Tutorial 13 you will learn how to animate a sprite by cycling through frames of a sprite sheet.