Start

Theory

Reference

Examples

← All examples
Rendering

Camera & Viewport

Panning, zooming, and a picture-in-picture minimap

Showcases IRenderer's Camera/Viewport pair directly (no ECS involved): a checkerboard world bigger than the window, panned and zoomed by a controllable Camera, plus a picture-in-picture minimap — a second Camera, zoomed to fit the whole world, rendered through a small Viewport in the corner with a live outline tracking the main camera's visible area.

Controls

  • W A S D Pan the camera
  • Scroll Zoom in/out
  • R Reset the camera
Rendering
Camera & Viewport screenshot

Run it yourself

build just this example
cmake --build --preset windows-debug --target camera_demo
# binary lands under bin/ (e.g. bin/Debug/camera_demo.exe on Windows)

See Installation for the full build setup.

Source

examples/camera_demo/Game.hpp
#pragma once

#include <ASGE/ASGE.hpp>

/**
 * @brief Showcases IRenderer's Camera/Viewport pair: a large world panned
 *        and zoomed by a controllable Camera, plus a picture-in-picture
 *        minimap rendered through a second Camera into a small Viewport.
 */
class CameraDemoState final : public asge::game::state::IGameState<int>
{
    asge::video::Camera m_Camera{}; // world-space camera, panned/zoomed by input

    void UpdateCamera(float inDeltaTime, asge::input::InputState const& inInput);
    void RenderWorld(asge::video::IRenderer& inRenderer) const;
    void RenderMinimap(asge::video::IRenderer& inRenderer) const;

public:
    [[nodiscard]] std::optional<asge::game::state::Transition<int>>
    Update(float inDeltaTime, asge::input::InputState const& inInput) override;
    void Render(asge::video::IRenderer& inRenderer) override;
    void OnSystemEvent(asge::event::SystemEvent const& inSysEvent) override;
};

class CameraDemoGame final : public asge::game::Game<int>
{
public:
    explicit CameraDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev);

protected:
    [[nodiscard]] std::unique_ptr<StateType> CreateState(int inId) override;
};
examples/camera_demo/Game.cpp
#include "Game.hpp"

#include <algorithm>
#include <array>

namespace
{

using asge::input::Keycode;
using asge::graphics::RGBA_Color;
using asge::video::Camera;
using asge::video::Viewport;

constexpr float kWindowW = 800.0f;
constexpr float kWindowH = 600.0f;

constexpr float kTileSize  = 100.0f;
constexpr int   kGridCols  = 16;
constexpr int   kGridRows  = 12;
constexpr float kWorldW    = kGridCols * kTileSize;
constexpr float kWorldH    = kGridRows * kTileSize;

constexpr float kPanSpeed = 400.0f; // world units/second
constexpr float kZoomStep = 0.1f;   // camera zoom per scroll wheel unit
constexpr float kMinZoom  = 0.3f;
constexpr float kMaxZoom  = 3.0f;

// Backdrop is drawn in full-window screen space; the viewport below is the
// smaller area inside it the minimap's own camera actually renders into.
constexpr asge::math::Rect kMinimapBackdrop{ 620.0f, 10.0f, 170.0f, 130.0f };
constexpr asge::math::Rect kMinimapViewport{ 625.0f, 15.0f, 160.0f, 120.0f };

constexpr float kLandmarkRadius = 35.0f;

struct Landmark
{
    asge::math::Float2 m_Pos;
    RGBA_Color          m_Color;
};

std::array<Landmark, 5> const kLandmarks{ {
    { { 150.0f,  150.0f }, RGBA_Color{ 220,  60,  60, 255 } },
    { { 1450.0f, 150.0f }, RGBA_Color{  60, 200,  90, 255 } },
    { { 800.0f,  600.0f }, RGBA_Color{ 240, 200,  60, 255 } },
    { { 150.0f, 1050.0f }, RGBA_Color{  90, 170, 240, 255 } },
    { { 1450.0f,1050.0f }, RGBA_Color{ 200,  90, 220, 255 } },
} };

}

void CameraDemoState::UpdateCamera(float inDeltaTime, asge::input::InputState const &inInput)
{
    float const dx = (inInput.IsKeyDown(Keycode::D) ? kPanSpeed : 0.0f)
                    - (inInput.IsKeyDown(Keycode::A) ? kPanSpeed : 0.0f);
    float const dy = (inInput.IsKeyDown(Keycode::S) ? kPanSpeed : 0.0f)
                    - (inInput.IsKeyDown(Keycode::W) ? kPanSpeed : 0.0f);

    m_Camera.m_X += dx * inDeltaTime;
    m_Camera.m_Y += dy * inDeltaTime;
    m_Camera.m_Zoom = std::clamp(
        m_Camera.m_Zoom + inInput.GetScrollDelta().y() * kZoomStep, kMinZoom, kMaxZoom
    );

    // Keep the visible area inside the world when it fits within it,
    // otherwise center the (now larger than the world) view on it.
    float const visibleW = kWindowW / m_Camera.m_Zoom;
    float const visibleH = kWindowH / m_Camera.m_Zoom;
    m_Camera.m_X = visibleW < kWorldW
        ? std::clamp(m_Camera.m_X, 0.0f, kWorldW - visibleW)
        : (kWorldW - visibleW) * 0.5f;
    m_Camera.m_Y = visibleH < kWorldH
        ? std::clamp(m_Camera.m_Y, 0.0f, kWorldH - visibleH)
        : (kWorldH - visibleH) * 0.5f;
}

void CameraDemoState::RenderWorld(asge::video::IRenderer &inRenderer) const
{
    for ( int row = 0; row < kGridRows; ++row )
    {
        for ( int col = 0; col < kGridCols; ++col )
        {
            bool const light = (row + col) % 2 == 0;
            inRenderer.DrawRect(
                asge::math::Rect{
                    static_cast<float>(col) * kTileSize, static_cast<float>(row) * kTileSize,
                    kTileSize, kTileSize
                },
                light ? RGBA_Color{ 60, 65, 80, 255 } : RGBA_Color{ 40, 44, 56, 255 },
                true
            );
        }
    }

    // World border, so panning past the edge of the map is obvious.
    inRenderer.DrawRect(asge::math::Rect{ 0.0f, 0.0f, kWorldW, kWorldH }, RGBA_Color{ 240, 240, 245, 255 }, false);

    for ( auto const& landmark : kLandmarks )
    {
        inRenderer.DrawCircle(
            asge::math::Int2{ static_cast<int>(landmark.m_Pos.x()), static_cast<int>(landmark.m_Pos.y()) },
            static_cast<int>(kLandmarkRadius), landmark.m_Color, true
        );
    }
}

void CameraDemoState::RenderMinimap(asge::video::IRenderer &inRenderer) const
{
    inRenderer.SetViewport(kMinimapViewport);

    // Zoom chosen so the whole world fits inside the minimap viewport.
    float const zoom = std::min(kMinimapViewport.m_Width / kWorldW, kMinimapViewport.m_Height / kWorldH);
    inRenderer.SetCamera(Camera{ .m_X = 0.0f, .m_Y = 0.0f, .m_Zoom = zoom });

    RenderWorld(inRenderer);

    // The main camera's visible area, in world space -- SetCamera above
    // means this lands in minimap space exactly like everything RenderWorld
    // just drew, with no extra math needed here.
    asge::math::Rect const visible{
        m_Camera.m_X, m_Camera.m_Y, kWindowW / m_Camera.m_Zoom, kWindowH / m_Camera.m_Zoom
    };
    inRenderer.DrawRect(visible, RGBA_Color{ 255, 255, 255, 255 }, false);
}

std::optional<asge::game::state::Transition<int>>
CameraDemoState::Update(float inDeltaTime, asge::input::InputState const &inInput)
{
    UpdateCamera(inDeltaTime, inInput);
    return std::nullopt;
}

void CameraDemoState::Render(asge::video::IRenderer &inRenderer)
{
    inRenderer.Clear(RGBA_Color{ 15, 15, 20, 255 });

    inRenderer.SetViewport(Viewport{ 0.0f, 0.0f, kWindowW, kWindowH });
    inRenderer.SetCamera(m_Camera);
    RenderWorld(inRenderer);

    // Minimap backdrop, drawn in full-window screen space (identity camera)
    // before RenderMinimap below switches to its own viewport/camera.
    inRenderer.SetCamera(Camera{});
    inRenderer.DrawRect(kMinimapBackdrop, RGBA_Color{ 10, 10, 14, 230 }, true);
    inRenderer.DrawRect(kMinimapBackdrop, RGBA_Color{ 200, 200, 210, 255 }, false);

    RenderMinimap(inRenderer);

    // Restore full-window/main-camera state -- RenderMinimap leaves both
    // pointed at its own viewport/camera, and Render() should hand the
    // renderer back the way it found it.
    inRenderer.SetViewport(Viewport{ 0.0f, 0.0f, kWindowW, kWindowH });
    inRenderer.SetCamera(m_Camera);
}

void CameraDemoState::OnSystemEvent(asge::event::SystemEvent const &inSysEvent)
{
    auto const* keyEvent = inSysEvent.TryGet<asge::event::KeyboardEvent>();
    if ( !keyEvent ) return;
    if ( keyEvent->s_Type != asge::event::EventType::KEYBOARD_KEY_PRESSED ) return;
    if ( keyEvent->s_Keycode == Keycode::R ) m_Camera = Camera{};
}

CameraDemoGame::CameraDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev)
: Game(inRenderer, inAudioDev)
{
    SetInitialState(0);
}

std::unique_ptr<CameraDemoGame::StateType> CameraDemoGame::CreateState([[maybe_unused]] int inId)
{
    return std::make_unique<CameraDemoState>();
}

Browse the full example on GitHub →