Tutorial 17: 2D Camera and Viewport

Graphics  ·  Matrix  ·  SpriteBatch  ·  Viewport

XNA does not have a built-in 2D camera class, but SpriteBatch::Begin() accepts an optional transformMatrix parameter that makes implementing one straightforward. By passing a translation, scale, and rotation matrix you can pan, zoom, and rotate the entire world without touching any individual sprite position.

What is a 2D camera?

A 2D camera is simply a mathematical transform that maps world-space coordinates to screen-space coordinates. If the camera is at world position (400, 300), then an object at world (400, 300) should appear at the centre of the screen. You achieve this by translating the entire scene by -cameraPosition (and optionally scaling and rotating before or after).

In XNA/CNA the transform is expressed as a Matrix and passed to SpriteBatch::Begin(). SpriteBatch multiplies every sprite's position by this matrix before rendering. This means all world coordinates stay unchanged — only the rendering shifts.

Matrix.CreateTranslation for scroll

For a pure pan (no zoom or rotation), a translation matrix is all you need:

#include "Microsoft/Xna/Framework/Matrix.hpp"

// Camera looks at world position (camX, camY)
// We translate the scene by the negative of that position
Matrix transform = Matrix::CreateTranslation(-camX, -camY, 0.0f);

spriteBatch_->Begin(SpriteSortMode::Deferred,
                    nullptr,   // BlendState — nullptr = AlphaBlend default
                    nullptr,   // SamplerState
                    nullptr,   // DepthStencilState
                    nullptr,   // RasterizerState
                    nullptr,   // Effect
                    transform);

An object at world position (500, 300) will now appear at screen position (500 - camX, 300 - camY).

SpriteBatch::Begin() transformMatrix parameter

The full Begin() signature in CNA is:

void SpriteBatch::Begin(
    SpriteSortMode          sortMode        = SpriteSortMode::Deferred,
    BlendState*             blendState      = nullptr,
    SamplerState*           samplerState    = nullptr,
    DepthStencilState*      depthStencil    = nullptr,
    RasterizerState*        rasterizerState = nullptr,
    Effect*                 effect          = nullptr,
    std::optional<Matrix>  transformMatrix = std::nullopt
);

Passing a Matrix in the last argument is the standard hook for 2D cameras, parallax scrolling, and screen shake. The matrix is applied uniformly to all sprites drawn between this Begin()/End() pair.

Viewport struct

Viewport describes the portion of the back buffer that receives rendering output. It also provides helper methods for converting between screen and world space:

#include "Microsoft/Xna/Framework/Graphics/Viewport.hpp"

Viewport vp = getGraphicsDeviceProperty().getViewport();

// Viewport fields
intcs x      = vp.X;        // left edge in pixels
intcs y      = vp.Y;        // top edge in pixels
intcs width  = vp.Width;    // width in pixels
intcs height = vp.Height;   // height in pixels
Single minZ  = vp.MinDepth; // usually 0.0f
Single maxZ  = vp.MaxDepth; // usually 1.0f

// Screen centre
Vector2 centre((Single)vp.Width / 2.0f, (Single)vp.Height / 2.0f);

Camera2D class pattern

A reusable camera class encapsulates position, zoom, rotation, and produces a transform matrix on demand:

// Camera2D.hpp
#pragma once
#include "Microsoft/Xna/Framework/Matrix.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/MathHelper.hpp"

class Camera2D {
public:
    Camera2D(intcs viewportWidth, intcs viewportHeight)
        : viewportW_(viewportWidth), viewportH_(viewportHeight) {}

    // Getters / setters
    void     setPosition(Vector2 p)  { position_ = p; dirty_ = true; }
    Vector2  getPosition()     const { return position_; }
    void     setZoom(Single z)       { zoom_ = std::max(0.1f, z); dirty_ = true; }
    Single   getZoom()         const { return zoom_; }
    void     setRotation(Single r)   { rotation_ = r; dirty_ = true; }
    Single   getRotation()     const { return rotation_; }

    // Build transform matrix (lazy)
    Matrix getTransform() {
        if (dirty_) {
            Vector2 origin((Single)viewportW_ / 2.0f, (Single)viewportH_ / 2.0f);
            cached_ =
                Matrix::CreateTranslation(Vector3(-position_, 0.0f)) *
                Matrix::CreateRotationZ(-rotation_)                  *
                Matrix::CreateScale(zoom_, zoom_, 1.0f)              *
                Matrix::CreateTranslation(Vector3(origin, 0.0f));
            dirty_ = false;
        }
        return cached_;
    }

    // Convert screen point to world position
    Vector2 ScreenToWorld(Vector2 screen) const {
        Matrix inv = Matrix::Invert(const_cast<Camera2D*>(this)->getTransform());
        return Vector2::Transform(screen, inv);
    }

    // Clamp camera so it does not scroll outside a world rectangle
    void ClampToWorld(intcs worldWidth, intcs worldHeight) {
        Single halfW = (Single)viewportW_ / (2.0f * zoom_);
        Single halfH = (Single)viewportH_ / (2.0f * zoom_);
        position_.X = std::clamp(position_.X, halfW, (Single)worldWidth  - halfW);
        position_.Y = std::clamp(position_.Y, halfH, (Single)worldHeight - halfH);
        dirty_ = true;
    }

private:
    intcs   viewportW_, viewportH_;
    Vector2 position_  = Vector2::Zero;
    Single  zoom_      = 1.0f;
    Single  rotation_  = 0.0f;
    bool    dirty_     = true;
    Matrix  cached_;
};

Follow-player camera

#include <memory>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Camera2D.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;

static constexpr intcs WORLD_W = 3200;
static constexpr intcs WORLD_H = 2400;

class CameraDemo final : public Game {
public:
    CameraDemo() : graphics_(this) {
        graphics_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

protected:
    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
        playerTex_   = Content.Load<Texture2D>("Sprites/player");
        worldTex_    = Content.Load<Texture2D>("Sprites/world_background");

        camera_ = std::make_unique<Camera2D>(800, 600);
        playerPos_ = Vector2((Single)WORLD_W / 2.0f, (Single)WORLD_H / 2.0f);
    }

    void Update(GameTime& gt) override {
        auto kb = Keyboard::GetState();
        Single dt = (Single)gt.getElapsedGameTime().TotalSeconds();

        // Move player in world space
        const Single speed = 250.0f;
        if (kb.IsKeyDown(Keys::Left))  playerPos_.X -= speed * dt;
        if (kb.IsKeyDown(Keys::Right)) playerPos_.X += speed * dt;
        if (kb.IsKeyDown(Keys::Up))    playerPos_.Y -= speed * dt;
        if (kb.IsKeyDown(Keys::Down))  playerPos_.Y += speed * dt;

        playerPos_.X = std::clamp(playerPos_.X, 0.0f, (Single)WORLD_W);
        playerPos_.Y = std::clamp(playerPos_.Y, 0.0f, (Single)WORLD_H);

        // Smooth follow: lerp camera toward player centre
        Vector2 target = playerPos_ + Vector2(24.0f, 32.0f);
        Vector2 camPos = Vector2::Lerp(camera_->getPosition(), target, 5.0f * dt);
        camera_->setPosition(camPos);

        // Zoom with Z/X
        if (kb.IsKeyDown(Keys::Z)) camera_->setZoom(camera_->getZoom() + dt);
        if (kb.IsKeyDown(Keys::X)) camera_->setZoom(camera_->getZoom() - dt);

        // Clamp camera to world bounds
        camera_->ClampToWorld(WORLD_W, WORLD_H);
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::Black);

        // Draw world with camera transform
        spriteBatch_->Begin(SpriteSortMode::Deferred,
                             nullptr, nullptr, nullptr, nullptr, nullptr,
                             camera_->getTransform());
        spriteBatch_->Draw(*worldTex_,  Vector2::Zero,   Color::White);
        spriteBatch_->Draw(*playerTex_, playerPos_,      Color::White);
        spriteBatch_->End();

        // HUD — draw without camera transform (screen space)
        spriteBatch_->Begin();
        // Draw minimap, health bar, etc. here
        spriteBatch_->End();

        gd.Present();
    }

private:
    GraphicsDeviceManager        graphics_;
    std::unique_ptr<SpriteBatch> spriteBatch_;
    std::unique_ptr<Texture2D>   playerTex_, worldTex_;
    std::unique_ptr<Camera2D>    camera_;
    Vector2                      playerPos_;
};

int main() { CameraDemo game; game.Run(); }

Screen shake

Screen shake is a small random offset added to the camera position each frame, decaying over time:

// Add to Camera2D or Game class:
Single shakeIntensity_ = 0.0f;
Single shakeDuration_  = 0.0f;

void Shake(Single intensity, Single duration) {
    shakeIntensity_ = intensity;
    shakeDuration_  = duration;
}

// In Update():
if (shakeDuration_ > 0) {
    shakeDuration_ -= dt;
    Single s = shakeIntensity_ * (shakeDuration_ / originalDuration_);
    Vector2 offset(
        ((Single)rand() / RAND_MAX * 2.0f - 1.0f) * s,
        ((Single)rand() / RAND_MAX * 2.0f - 1.0f) * s
    );
    camera_->setPosition(basePosition_ + offset);
}