Tutorial 17: 2D Camera and 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);
}