Cameras and Scrolling¶
Camera2D allows you to create worlds larger than the screen by scrolling the view. This guide covers camera setup, following targets, boundaries, and parallax effects.
Camera2D Basics¶
A Camera2D defines what portion of your game world is visible on screen.
Creating a Camera¶
#include <graphics/Camera2D.h>
// Create camera with viewport size
pixelroot32::graphics::Camera2D camera(240, 240); // Screen width, height
// Set camera position
camera.setPosition(0, 0);
// Apply camera to renderer (in draw method)
camera.apply(renderer);
How It Works¶
The camera translates world coordinates to screen coordinates: - Objects at world position (100, 50) with camera at (0, 0) appear at screen (100, 50) - Objects at world position (100, 50) with camera at (50, 0) appear at screen (50, 50) - The camera effectively "moves" the world relative to the screen
Following a Target¶
The most common use is following a player or other target.
Basic Follow¶
class GameScene : public pixelroot32::core::Scene {
private:
pixelroot32::graphics::Camera2D camera;
PlayerActor* player;
public:
void init() override {
int screenWidth = engine.getRenderer().getWidth();
int screenHeight = engine.getRenderer().getHeight();
// Create camera
camera = pixelroot32::graphics::Camera2D(screenWidth, screenHeight);
// Create player
player = new PlayerActor(500, 300); // World position
addEntity(player);
}
void update(unsigned long deltaTime) override {
Scene::update(deltaTime);
// Make camera follow player
camera.followTarget(player->x, player->y);
}
void draw(pixelroot32::graphics::Renderer& renderer) override {
// Apply camera before drawing
camera.apply(renderer);
// Now all drawing uses camera coordinates
Scene::draw(renderer);
}
};
Dead Zone (Smooth Following)¶
For smoother following, you can implement a dead zone where the camera doesn't move until the target leaves the zone:
void update(unsigned long deltaTime) override {
Scene::update(deltaTime);
// Get screen center
int screenCenterX = engine.getRenderer().getWidth() / 2;
int screenCenterY = engine.getRenderer().getHeight() / 2;
// Calculate player position relative to screen center
float playerScreenX = player->x - camera.getX();
float playerScreenY = player->y - camera.getY();
// Dead zone size
const int DEAD_ZONE_X = 40;
const int DEAD_ZONE_Y = 40;
// Move camera if player leaves dead zone
if (playerScreenX < screenCenterX - DEAD_ZONE_X) {
camera.setPosition(player->x - (screenCenterX - DEAD_ZONE_X), camera.getY());
} else if (playerScreenX > screenCenterX + DEAD_ZONE_X) {
camera.setPosition(player->x - (screenCenterX + DEAD_ZONE_X), camera.getY());
}
if (playerScreenY < screenCenterY - DEAD_ZONE_Y) {
camera.setPosition(camera.getX(), player->y - (screenCenterY - DEAD_ZONE_Y));
} else if (playerScreenY > screenCenterY + DEAD_ZONE_Y) {
camera.setPosition(camera.getX(), player->y - (screenCenterY + DEAD_ZONE_Y));
}
}
Camera Boundaries¶
Limit camera movement to keep it within your level bounds.
Setting Boundaries¶
void init() override {
// Create camera
camera = pixelroot32::graphics::Camera2D(240, 240);
// Set horizontal boundaries (level is 2000 pixels wide)
camera.setBounds(0, 2000 - 240); // minX, maxX
// Set vertical boundaries (level is 1000 pixels tall)
camera.setVerticalBounds(0, 1000 - 240); // minY, maxY
}
Example: Side-Scroller with Boundaries¶
class SideScrollerScene : public pixelroot32::core::Scene {
private:
pixelroot32::graphics::Camera2D camera;
PlayerActor* player;
static const int LEVEL_WIDTH = 2000;
static const int LEVEL_HEIGHT = 240;
public:
void init() override {
int screenWidth = engine.getRenderer().getWidth();
int screenHeight = engine.getRenderer().getHeight();
camera = pixelroot32::graphics::Camera2D(screenWidth, screenHeight);
// Set boundaries (camera can't go outside level)
camera.setBounds(0, LEVEL_WIDTH - screenWidth);
camera.setVerticalBounds(0, LEVEL_HEIGHT - screenHeight);
// Create player at start
player = new PlayerActor(100, 100);
addEntity(player);
}
void update(unsigned long deltaTime) override {
Scene::update(deltaTime);
// Follow player horizontally
camera.followTarget(player->x, camera.getY());
}
void draw(pixelroot32::graphics::Renderer& renderer) override {
camera.apply(renderer);
Scene::draw(renderer);
}
};
Parallax Scrolling¶
Parallax creates depth by moving background layers at different speeds.
Basic Parallax¶
class ParallaxBackground : public pixelroot32::core::Entity {
private:
float parallaxSpeed; // 0.0 to 1.0 (1.0 = normal, 0.5 = half speed)
float baseX;
public:
ParallaxBackground(float speed)
: Entity(0, 0, 240, 240, pixelroot32::core::EntityType::GENERIC),
parallaxSpeed(speed), baseX(0) {
setRenderLayer(0);
}
void update(unsigned long deltaTime) override {
// Get camera position
auto& camera = getCamera(); // You'll need to pass camera reference
// Calculate parallax offset
baseX = camera.getX() * parallaxSpeed;
}
void draw(pixelroot32::graphics::Renderer& renderer) override {
// Draw background with parallax offset
renderer.drawTileMap(backgroundTileMap,
static_cast<int>(baseX), 0,
pixelroot32::graphics::Color::White);
}
};
Multiple Parallax Layers¶
class ParallaxScene : public pixelroot32::core::Scene {
private:
pixelroot32::graphics::Camera2D camera;
// Parallax layers (farther = slower)
ParallaxLayer* farBackground; // Speed: 0.2
ParallaxLayer* midBackground; // Speed: 0.5
ParallaxLayer* nearBackground; // Speed: 0.8
PlayerActor* player; // Speed: 1.0 (normal)
public:
void init() override {
camera = pixelroot32::graphics::Camera2D(240, 240);
// Create parallax layers
farBackground = new ParallaxLayer(0.2f); // Moves slowest
midBackground = new ParallaxLayer(0.5f);
nearBackground = new ParallaxLayer(0.8f);
addEntity(farBackground);
addEntity(midBackground);
addEntity(nearBackground);
player = new PlayerActor(100, 100);
addEntity(player);
}
void update(unsigned long deltaTime) override {
Scene::update(deltaTime);
// Update parallax layers with camera position
farBackground->updateParallax(camera.getX());
midBackground->updateParallax(camera.getX());
nearBackground->updateParallax(camera.getX());
// Follow player
camera.followTarget(player->x, player->y);
}
void draw(pixelroot32::graphics::Renderer& renderer) override {
camera.apply(renderer);
Scene::draw(renderer);
}
};
Using setDisplayOffset for Parallax¶
For simpler parallax, you can use setDisplayOffset():
void draw(pixelroot32::graphics::Renderer& renderer) override {
// Apply camera
camera.apply(renderer);
// Draw far background with offset (moves slower)
renderer.setDisplayOffset(
static_cast<int>(camera.getX() * 0.3f),
0
);
renderer.drawTileMap(farBackground, 0, 0, Color::White);
// Draw mid background
renderer.setDisplayOffset(
static_cast<int>(camera.getX() * 0.6f),
0
);
renderer.drawTileMap(midBackground, 0, 0, Color::White);
// Reset offset for normal drawing
renderer.setDisplayOffset(0, 0);
// Draw game objects (normal speed)
Scene::draw(renderer);
}
Complete Example: Platformer with Camera¶
class PlatformerScene : public pixelroot32::core::Scene {
private:
pixelroot32::graphics::Camera2D camera;
PlayerActor* player;
static const int LEVEL_WIDTH = 3000;
static const int LEVEL_HEIGHT = 800;
public:
void init() override {
int screenWidth = engine.getRenderer().getWidth();
int screenHeight = engine.getRenderer().getHeight();
// Create camera
camera = pixelroot32::graphics::Camera2D(screenWidth, screenHeight);
// Set boundaries
camera.setBounds(0, LEVEL_WIDTH - screenWidth);
camera.setVerticalBounds(0, LEVEL_HEIGHT - screenHeight);
// Create player
player = new PlayerActor(100, 400);
addEntity(player);
// Create platforms, enemies, etc.
// ...
}
void update(unsigned long deltaTime) override {
Scene::update(deltaTime);
// Follow player with dead zone
int screenCenterX = engine.getRenderer().getWidth() / 2;
int screenCenterY = engine.getRenderer().getHeight() / 2;
float playerScreenX = player->x - camera.getX();
float playerScreenY = player->y - camera.getY();
const int DEAD_ZONE = 60;
// Horizontal follow
if (playerScreenX < screenCenterX - DEAD_ZONE) {
camera.setPosition(player->x - (screenCenterX - DEAD_ZONE), camera.getY());
} else if (playerScreenX > screenCenterX + DEAD_ZONE) {
camera.setPosition(player->x - (screenCenterX + DEAD_ZONE), camera.getY());
}
// Vertical follow (only when falling or jumping high)
if (playerScreenY < screenCenterY - DEAD_ZONE ||
playerScreenY > screenCenterY + DEAD_ZONE) {
camera.setPosition(camera.getX(), player->y - screenCenterY);
}
}
void draw(pixelroot32::graphics::Renderer& renderer) override {
// Apply camera
camera.apply(renderer);
// Draw background (parallax)
renderer.setDisplayOffset(
static_cast<int>(camera.getX() * 0.3f),
0
);
renderer.drawTileMap(backgroundTileMap, 0, 0, Color::DarkGray);
renderer.setDisplayOffset(0, 0);
// Draw game objects
Scene::draw(renderer);
}
};
Best Practices¶
Camera Movement¶
- Use dead zones: Prevents jittery camera movement
- Smooth transitions: Consider lerping camera position for smoother movement
- Set boundaries: Always limit camera to level bounds
- Test on hardware: Camera performance may differ on ESP32
Parallax¶
- Layer speeds: Farther layers move slower (0.2-0.5), closer move faster (0.7-0.9)
- Limit layers: Too many parallax layers can impact performance
- Use tilemaps: Parallax works best with tilemaps
- Test visually: Ensure parallax effect is noticeable but not distracting
Performance¶
- Apply once: Call
camera.apply()once per frame, at start ofdraw() - Cull off-screen: Don't draw entities outside camera view
- Limit parallax layers: 2-3 layers is usually enough
- Optimize tilemaps: Use efficient tilemap rendering
Common Patterns¶
Camera Helper Class¶
class CameraController {
private:
pixelroot32::graphics::Camera2D camera;
float targetX, targetY;
float smoothSpeed = 0.1f;
public:
void followTarget(float x, float y) {
targetX = x;
targetY = y;
}
void update(unsigned long deltaTime) {
// Smooth camera movement
float currentX = camera.getX();
float currentY = camera.getY();
float newX = currentX + (targetX - currentX) * smoothSpeed;
float newY = currentY + (targetY - currentY) * smoothSpeed;
camera.setPosition(newX, newY);
}
void apply(pixelroot32::graphics::Renderer& renderer) {
camera.apply(renderer);
}
};
Viewport Culling¶
Only draw entities within camera view:
bool isVisible(float x, float y, int width, int height) {
float cameraX = camera.getX();
float cameraY = camera.getY();
int screenWidth = engine.getRenderer().getWidth();
int screenHeight = engine.getRenderer().getHeight();
return !(x + width < cameraX ||
x > cameraX + screenWidth ||
y + height < cameraY ||
y > cameraY + screenHeight);
}
Troubleshooting¶
Camera Not Moving¶
- Verify
camera.apply()is called indraw() - Check
followTarget()orsetPosition()is called inupdate() - Ensure camera is created with correct viewport size
- Check boundaries aren't preventing movement
Objects Not Visible¶
- Verify objects are within camera view
- Check world coordinates vs screen coordinates
- Ensure camera is applied before drawing
- Verify render layers are correct
Parallax Not Working¶
- Check
setDisplayOffset()is used correctly - Verify parallax speed values (0.0 to 1.0)
- Ensure offset is reset after parallax layers
- Test with different speed values
Next Steps¶
Now that you understand cameras and scrolling, learn about: - Tilemaps - Build levels with tiles - Particles and Effects - Add visual effects - Performance Optimization - Optimize your game
See also: - API Reference - Camera2D - Manual - Basic Rendering - Manual - Tilemaps