Start

Theory

Reference

Examples

← All examples
Input

Input System

Polling InputState end-to-end

Step 3 of the input-system roadmap: a small InputState/InputSystem showcase.

Everything here is driven by polling InputState inside Update() — movement, an edge-triggered toggle, a mouse cursor, held vs. edge-triggered mouse buttons, and scroll — with OnSystemEvent left completely empty, proving the polling API is enough on its own for a real game loop.

Controls

  • W A S D Move the box
  • SPACE Toggle light/dark background (edge-triggered)
  • Left Click (hold) Shown as held while down
  • Right Click Drop a mark at the cursor (edge-triggered)
  • Scroll Shown live
Input
Input System screenshot

Run it yourself

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

See Installation for the full build setup.

Source

examples/input_demo/Game.hpp
#pragma once

#include <ASGE/ASGE.hpp>
#include <array>

/**
 * @brief Step 3 of the input-system roadmap: a small InputState/InputSystem showcase.
 *
 * Everything here is driven by polling InputState in Update() -- WASD movement,
 * a SPACE toggle (edge-triggered), a mouse cursor, left-click-held vs. right-click
 * (edge-triggered) and scroll -- with OnSystemEvent left empty, proving the polling
 * API is enough on its own for a real game loop.
 */
class InputDemoState final : public asge::game::state::IGameState<int>
{
    static constexpr std::size_t kMaxMarks = 24;

    asge::math::Float2 m_BoxPos{ 380.0f, 280.0f };
    float               m_BoxSize{ 48.0f };
    bool                m_LightBackground{ false };

    // Render() only gets an IRenderer, not InputState, so the cursor's
    // position/held-state are cached here in Update() and just drawn in Render().
    asge::math::Float2 m_CursorPos{};
    bool                m_CursorHeld{ false };

    std::array<asge::math::Float2, kMaxMarks> m_Marks{};
    std::size_t m_MarkCount{ 0 };
    std::size_t m_NextMarkSlot{ 0 };

    void UpdatePlayerBox(float inDeltaTime, asge::input::InputState const& inInput);
    void UpdateBackgroundToggle(asge::input::InputState const& inInput);
    void UpdateBoxSizeFromScroll(asge::input::InputState const& inInput);
    void UpdateCursor(asge::input::InputState const& inInput);
    void UpdateMarks(asge::input::InputState const& inInput);

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 InputDemoGame final : public asge::game::Game<int>
{
public:
    explicit InputDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev);

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

#include <algorithm>

using asge::input::Keycode;
using asge::input::MouseButton;

namespace
{
constexpr float kBoxSpeed      = 250.0f; // pixels/second
constexpr float kMinBoxSize    = 20.0f;
constexpr float kMaxBoxSize    = 120.0f;
constexpr float kScrollToSize  = 6.0f;   // pixels of box size per scroll unit
constexpr int   kCursorRadius  = 12;
constexpr int   kMarkRadius    = 5;
constexpr float kWindowW       = 800.0f;
constexpr float kWindowH       = 600.0f;
}

void InputDemoState::UpdatePlayerBox(float inDeltaTime, asge::input::InputState const &inInput)
{
    // Continuous, held-down movement -- exactly what IsKeyDown polling replaces
    // the hand-rolled m_Up/m_Down/m_Left/m_Right + OnSystemEvent pattern for.
    float const dx = (inInput.IsKeyDown(Keycode::D) ? kBoxSpeed : 0.0f)
                    - (inInput.IsKeyDown(Keycode::A) ? kBoxSpeed : 0.0f);
    float const dy = (inInput.IsKeyDown(Keycode::S) ? kBoxSpeed : 0.0f)
                    - (inInput.IsKeyDown(Keycode::W) ? kBoxSpeed : 0.0f);

    m_BoxPos += asge::math::Float2{ dx, dy } * inDeltaTime;
    m_BoxPos = {
        std::clamp( m_BoxPos.x(), 0.0f, kWindowW - m_BoxSize ),
        std::clamp( m_BoxPos.y(), 0.0f, kWindowH - m_BoxSize )
    };
}

void InputDemoState::UpdateBackgroundToggle(asge::input::InputState const &inInput)
{
    // Edge-triggered: only flips once per press, unlike IsKeyDown above.
    if ( inInput.IsKeyPressed(Keycode::SPACE) )
        m_LightBackground = !m_LightBackground;
}

void InputDemoState::UpdateBoxSizeFromScroll(asge::input::InputState const &inInput)
{
    m_BoxSize += inInput.GetScrollDelta().y() * kScrollToSize;
    m_BoxSize = std::clamp( m_BoxSize, kMinBoxSize, kMaxBoxSize );
}

void InputDemoState::UpdateCursor(asge::input::InputState const &inInput)
{
    m_CursorPos = inInput.GetMousePosition();
    m_CursorHeld = inInput.IsMouseButtonDown(MouseButton::LEFT);
}

void InputDemoState::UpdateMarks(asge::input::InputState const &inInput)
{
    // Right-click drops a mark at the cursor -- edge-triggered, same as the
    // SPACE toggle above but for a mouse button instead of a key.
    if ( !inInput.IsMouseButtonPressed(MouseButton::RIGHT) ) return;

    m_Marks[m_NextMarkSlot] = inInput.GetMousePosition();
    m_NextMarkSlot = (m_NextMarkSlot + 1) % kMaxMarks;
    m_MarkCount = std::min( m_MarkCount + 1, kMaxMarks );
}

std::optional<asge::game::state::Transition<int>>
InputDemoState::Update(float inDeltaTime, asge::input::InputState const &inInput)
{
    UpdatePlayerBox( inDeltaTime, inInput );
    UpdateBackgroundToggle( inInput );
    UpdateBoxSizeFromScroll( inInput );
    UpdateCursor( inInput );
    UpdateMarks( inInput );
    return std::nullopt;
}

void InputDemoState::Render(asge::video::IRenderer &inRenderer)
{
    inRenderer.Clear( m_LightBackground
        ? asge::graphics::RGBA_Color{ 225, 225, 230, 255 }
        : asge::graphics::RGBA_Color{ 20, 20, 25, 255 } );

    // Marks dropped by right-click.
    for ( std::size_t i = 0; i < m_MarkCount; ++i )
    {
        inRenderer.DrawCircle(
            asge::math::Int2{ static_cast<int>(m_Marks[i].x()), static_cast<int>(m_Marks[i].y()) },
            kMarkRadius, { 240, 200, 60, 255 }, true );
    }

    // WASD-controlled box.
    inRenderer.DrawRect(
        asge::math::Rect{ m_BoxPos.x(), m_BoxPos.y(), m_BoxSize, m_BoxSize },
        { 220, 60, 60, 255 }, true );

    // Mouse cursor: filled while left-click is held, outline otherwise. Both the
    // position and the held-state come from InputState polling, cached in Update().
    inRenderer.DrawCircle(
        asge::math::Int2{ static_cast<int>(m_CursorPos.x()), static_cast<int>(m_CursorPos.y()) },
        kCursorRadius, { 90, 170, 240, 255 }, m_CursorHeld );
}

void InputDemoState::OnSystemEvent([[maybe_unused]] asge::event::SystemEvent const &inSysEvent)
{
    // Deliberately empty -- every reaction to input in this example comes from
    // polling InputState in Update() instead.
}

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

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

Browse the full example on GitHub →