Skip to content

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 of draw()
  • 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 in draw()
  • Check followTarget() or setPosition() is called in update()
  • 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