Tutorial 12: Moving Sprites and Basic Animation

CNA Tutorial Series  ·  Intermediate

What you’ll learn

  • Storing position and velocity as Vector2 and 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 Draw call.

Before you startTutorial 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:

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_->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.