Tutorial 12: Moving Sprites and Basic Animation

CNA Tutorial Series  ·  Intermediate

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:

ConstantValueDegrees
MathHelper::Pi3.14159...180°
MathHelper::TwoPi6.28318...360°
MathHelper::PiOver21.5707...90°
MathHelper::PiOver40.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.