Tutorial 12: Moving Sprites and Basic Animation
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.getElapsedGameTime().getTotalSeconds());
// Position += velocity * delta_time
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.getElapsedGameTime().getTotalSeconds());
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_ += 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().getViewport();
float screenW = static_cast<float>(vp.getWidth());
float screenH = static_cast<float>(vp.getHeight());
// 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_->getWidth() / 2.0f, tex_->getHeight() / 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_->getWidth() / 2.0f, tex_->getHeight() / 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.getTotalGameTime().getTotalSeconds();
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_.setPreferredBackBufferWidth(800);
graphics_.setPreferredBackBufferHeight(600);
setIsFixedTimeStep(true);
setTargetElapsedTime(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.getElapsedGameTime().getTotalSeconds());
pos_ += vel_ * dt;
const float r = 32.0f; // ball radius (half of 64px texture)
auto& vp = getGraphicsDeviceProperty().getViewport();
float w = static_cast<float>(vp.getWidth());
float h = static_cast<float>(vp.getHeight());
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.