Skip to content

Snowgears

Snowgears is a first-party gameplay layer on top of Snowpulse. It provides reusable systems for input, camera follow, movement/navigation, FX, ads/playable flows, and UI helpers.

Public include contract:

#include <snowgears.h>

Linking example:

snowpulse_add_game(sampleapp
    SOURCES ${SAMPLEAPP_SOURCES}
    DISPLAY_NAME "Sample App"
    BUNDLE_ID "com.example.sampleapp"
    ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets"
    ASSIMP OFF
    SPINE_VERSION "4.2"
    WEB_MODE GAME
    WEB_PLATFORM SNOWBLINK_PLAIN
)
target_link_libraries(sampleapp PRIVATE snowgears)

WSADInput

Description

snowgears::input::WSADInput is a small Script that converts the held W, S, A, and D keys into a normalized two-dimensional movement direction.

Important Info To Note

  • direction() is updated every frame: W/S control Y and A/D control X.
  • Diagonal input is normalized, so it has the same maximum magnitude as input on one axis.
  • Opposing keys cancel each other and no held input produces {0, 0}.
  • refresh() samples input immediately when code needs the value before the script update.
  • This is keyboard-only convenience input. Use VirtualAnalogStick or the underlying Snowpulse input/touch APIs on touch-first devices.

Sample Usage

auto* systems = createNode("systems");
auto* keys = systems->addComponent<snowgears::input::WSADInput>();
addChild(systems);

// In update:
movable->moveByAxis(keys->direction());

VirtualAnalogStick

Description

snowgears::input::VirtualAnalogStick is a screen-space analog stick script that exposes a normalized movement axis via axis(), plus press state via pressed().

Important Info To Note

  • The component auto-creates UI elements under the scene UIRoot (UIRect base + knob).
  • Pointer input is read from MouseLeft; axis() is clamped to unit length.
  • lockCenter=true means touch/click must begin inside the configured radius.
  • lockCenter=false means the stick center snaps to the press position.
  • visibleWhenIdle=false hides the stick visuals when not pressed.

Sample Usage

auto* systems = createNode("systems");
auto* stick = systems->addComponent<snowgears::input::VirtualAnalogStick>();

snowgears::input::VirtualAnalogStick::Config cfg;
cfg.anchor = { 0.0f, 0.0f };
cfg.center = { 170.0f, 170.0f };
cfg.radius = 86.0f;
cfg.lockCenter = true;
cfg.visibleWhenIdle = true;
stick->config(cfg);

addChild(systems);

// In update:
if (stick->pressed()) {
    movable->moveByAxis(stick->axis());
}

CameraFollower

Description

snowgears::camera::CameraFollower moves a camera node toward a target with independent follow modes for position, offset, and zoom (Snap, Smooth, Spring).

Important Info To Note

  • Attach it to a node that already has a snowpulse::Camera component.
  • It runs in lateUpdate, so it follows final transforms for the frame.
  • zoom() is clamped to a positive value (minimum 0.0001f).
  • snapNow() forces a one-frame snap to target values on the next update.
  • If no target is set, it still manages zoom/offset state but does not move position to a target.
  • The target pointer is non-owning. Clear or replace it before destroying the target node.

Sample Usage

auto* sceneCamera = camera();
auto* follower = sceneCamera->owner()->addComponent<snowgears::camera::CameraFollower>();
follower->target(trackedNode);
follower->offset({ 0.0f, 0.0f });
follower->zoom(1.0f);

snowgears::camera::CameraFollower::ChannelConfig pos;
pos.mode = snowgears::camera::CameraFollower::FollowMode::Smooth;
pos.smoothResponseSeconds = 0.10f;
follower->positionConfig(pos);

follower->snapNow();

WalkableUnit

Description

snowgears::movement::WalkableUnit is a movement script that supports direct axis driving (moveByAxis) and path-driven travel (moveToWorld / setPathCells).

Important Info To Note

  • With a GridNavMap assigned, moveToWorld uses A* to generate waypoints.
  • Without a nav map, moveToWorld still works by moving directly to the world target.
  • moveByAxis with non-zero input clears any active path.
  • onDestinationReached fires when the path is fully completed.
  • Movement uses acceleration and optional rotate-to-motion behavior from Config.
  • The navigation-map pointer is non-owning and must outlive the component (or be cleared with navigationMap(nullptr)).

Sample Usage

auto* unit = createNode("unit");
auto* walkable = unit->addComponent<snowgears::movement::WalkableUnit>();
addChild(unit);
walkable->navigationMap(&_navMap);

snowgears::movement::WalkableUnit::Config cfg;
cfg.maxSpeed = 250.0f;
cfg.acceleration = 1800.0f;
cfg.stopDistance = 8.0f;
walkable->config(cfg);

walkable->onDestinationReached = []() {
    // Handle destination arrival.
};

const bool routeStarted = walkable->moveToWorld({ 640.0f, 320.0f }, true);

GridNavMap

Description

snowgears::navigation::GridNavMap stores a walkable/blocked grid and converts between world space and cell coordinates.

Important Info To Note

  • Constructor takes width, height, cellSize, and optional origin.
  • cellSize is clamped internally to at least 0.01f.
  • resize() recreates the grid and resets all cells to walkable.
  • worldToCell() uses floor, so negative positions map predictably by cell.
  • Use fillWalkable, setWalkable, and setBlocked to define traversable areas.

Sample Usage

snowgears::navigation::GridNavMap navMap(20, 12, 64.0f, { -640.0f, -384.0f });
navMap.fillWalkable(true);
navMap.setBlocked({ 9, 5 }, true);
navMap.setBlocked({ 9, 6 }, true);

glm::ivec2 cell = navMap.worldToCell({ 32.0f, 64.0f });
glm::vec2 center = navMap.cellToWorldCenter(cell);
const bool canUseCell = navMap.inBounds(cell) && navMap.isWalkable(cell);

GridAreaTrigger

Description

snowgears::navigation::GridAreaTrigger watches tracked WalkableUnit pointers against a configured grid rectangle and fires callbacks when units stay inside for long enough, then later leave. It can also model reusable area interactions with GridAreaTriggerType::Sink, Container, or Work.

Important Info To Note

  • Trigger area uses gridCoordinate + gridSize in grid cells (inclusive min, exclusive max).
  • gridSize is clamped to at least {1, 1}.
  • type=Unknown keeps the legacy dwell/enter/leave behavior and does not accept deposits, withdrawals, or work progress.
  • stayDurationSeconds <= 0 triggers onTriggerStart immediately on entry.
  • onTriggerEnd fires only for units that already fired onTriggerStart.
  • Only units explicitly registered with trackUnit(...) are evaluated.
  • Navigation maps and tracked units are non-owning pointers. Call removeUnit(...) or clearUnits() before a tracked component is destroyed.
  • Sink accepts deposits with deposit(amount, unit). Finite sinks complete when amount() reaches maxAmount; maxAmount <= 0 means unlimited and never completes.
  • Container accepts deposit(...) and withdraw(...). Finite containers fire onCompleted each time they cross from not-full to full; maxAmount <= 0 means unlimited and never becomes full.
  • Work advances per activated unit after the dwell gate. preserveIncompleteWork=false resets unfinished work when a unit exits; true preserves it.
  • Unit whitelists compare against WalkableUnit::owner()->name(). Empty whitelists allow all units; non-empty whitelists require an exact, case-sensitive node-name match and deny null units.
  • depositUnitWhitelist gates sink/container deposits, withdrawUnitWhitelist gates container withdrawals, and workUnitWhitelist gates work dwell/progress.
  • Use depositUnitWhitelist(...), withdrawUnitWhitelist(...), and workUnitWhitelist(...) to change allow lists at runtime without resetting amount state. Updating the work whitelist ends active work triggers for units that are no longer allowed.
  • onAmountChanged(previous, current, unit) fires when sink/container runtime amount changes.
  • onCompleted(unit) fires for finite sink target completion, finite container full crossings, and per-unit work completion.
  • Optional debug visual draws a world-space tint quad over the area.
  • Use gridAreaTriggerTypeName(...) and gridAreaTriggerTypeFromString(...) when binding external data such as Tiled object properties.

Sample Usage

auto* systems = createNode("systems");
auto* trigger = systems->addComponent<snowgears::navigation::GridAreaTrigger>();
addChild(systems);
trigger->navigationMap(&_navMap);

snowgears::navigation::GridAreaTrigger::Config cfg;
cfg.gridCoordinate = { 4, 3 };
cfg.gridSize = { 3, 2 };
cfg.stayDurationSeconds = 0.8f;
cfg.debugVisualEnabled = true;
cfg.debugColor = { 0.25f, 0.07f, 0.10f, 0.35f };
trigger->config(cfg);

trigger->trackUnit(trackedUnit);

trigger->onTriggerStart = [](snowgears::movement::WalkableUnit* unit) {
    (void)unit;
    // Unit stayed in area long enough.
};
trigger->onTriggerEnd = [](snowgears::movement::WalkableUnit* unit) {
    (void)unit;
    // Unit left the area after activation.
};

Finite sink payment:

snowgears::navigation::GridAreaTrigger::Config cfg;
cfg.type = snowgears::navigation::GridAreaTriggerType::Sink;
cfg.gridCoordinate = { 8, 4 };
cfg.gridSize = { 2, 1 };
cfg.maxAmount = 25;
trigger->config(cfg);

trigger->onCompleted = [](snowgears::movement::WalkableUnit* unit) {
    (void)unit;
    // Payment target reached.
};

const int paid = trigger->deposit(10, trackedUnit);
const int stillNeeded = trigger->remainingAmount();

Unlimited trash sink:

cfg.type = snowgears::navigation::GridAreaTriggerType::Sink;
cfg.maxAmount = 0;
trigger->config(cfg);

const int discarded = trigger->deposit(itemCount, trackedUnit);

Container deposit and withdraw:

cfg.type = snowgears::navigation::GridAreaTriggerType::Container;
cfg.maxAmount = 12;
cfg.initialAmount = 3;
cfg.depositUnitWhitelist = { "depositor" };
cfg.withdrawUnitWhitelist = { "recipient" };
trigger->config(cfg);

if (trigger->canDeposit(depositUnit)) {
    const int stored = trigger->deposit(6, depositUnit);
    (void)stored;
}
const int taken = trigger->withdraw(4, withdrawUnit);
const bool full = trigger->isFull();

Work station with per-unit progress:

cfg.type = snowgears::navigation::GridAreaTriggerType::Work;
cfg.workDuration = 2.5f;
cfg.preserveIncompleteWork = true;
cfg.workUnitWhitelist = { "operator", "assistant" };
trigger->config(cfg);

trigger->onCompleted = [](snowgears::movement::WalkableUnit* unit) {
    (void)unit;
    // This unit finished the job.
};

float progress = trigger->workProgress01(trackedUnit);
if (trigger->workCompleted(trackedUnit)) {
    trigger->resetWork(trackedUnit);
}

AStarPathfinder

Description

snowgears::navigation::AStarPathfinder computes a path on GridNavMap and returns a list of cell coordinates from start to goal.

Important Info To Note

  • Pathfinding fails (found=false) if start/goal is out-of-bounds or blocked.
  • allowDiagonal=true enables 8-direction traversal (with diagonal cost).
  • start == goal returns found=true with a single-cell path.
  • cells are ordered from start to goal when a path is found.
  • The optional passability predicate can reject otherwise walkable cells for a single query.

Sample Usage

const glm::ivec2 start = navMap.worldToCell(currentWorldPosition);
const glm::ivec2 goal = navMap.worldToCell(targetWorldPosition);
const glm::ivec2 temporarilyBlockedCell { 6, 4 };

const auto path = snowgears::navigation::AStarPathfinder::findPath(
    navMap, start, goal, true,
    [temporarilyBlockedCell](const glm::ivec2& cell) {
        return cell != temporarilyBlockedCell;
    });
if (path.found) {
    walkable->setPathCells(path.cells);
}

CoinBurstToTargetEffect

Description

snowgears::fx::CoinBurstToTargetEffect spawns coin visuals near a source, spreads them outward, then flies them to a target with configurable timing and callbacks.

Important Info To Note

  • play() first calls stop(), so retriggering clears any active burst.
  • Source/target positions passed to play() are in the owner node's local space.
  • If coinSpritePath does not resolve, it falls back to QuadRenderer visuals.
  • onCoinArrived(index) fires per coin; onCompleted() fires after all arrivals.
  • coinCount is clamped to at least 1.

Sample Usage

auto* fxNode = createNode("token_burst");
auto* burst = fxNode->addComponent<snowgears::fx::CoinBurstToTargetEffect>();
addChild(fxNode);

snowgears::fx::CoinBurstToTargetEffect::Config cfg;
cfg.coinCount = 12;
cfg.coinSpritePath = "sprites/token.png";
cfg.spreadRadius = 102.0f;
cfg.flyDuration = 0.28f;
burst->config(cfg);

burst->onCompleted = []() {
    // Update presentation state after all visuals arrive.
};

burst->play({ 0.0f, 0.0f, 0.0f }, { 320.0f, 180.0f, 0.0f });

ParticlePresetCatalog

Description

snowgears::fx::ParticlePresetCatalog applies built-in particle preset tuning to a snowpulse::ParticleEffects component.

For new celebratory confetti, prefer the engine-level snowpulse::fx::ConfettiEffect described in Game Feedback. It provides a screen-space, one-shot lifecycle and custom asset/settings overrides. The Snowgears confetti preset remains available for existing code that needs direct, low-level control of a ParticleEffects component.

Important Info To Note

  • Built-in presets are fire, confetti, explosion, and sun-rays.
  • apply() resets modules to defaults before applying preset values.
  • applyByName() is case-insensitive and accepts sun-rays, sun_rays, or sunrays.
  • ParticlePresetOptions can apply a sprite and control autoplay/restart.
  • apply() and applyByName() return false on invalid input.

Sample Usage

auto* fxNode = createNode("presetFx");
auto* effect = fxNode->addComponent<snowpulse::ParticleEffects>();
addChild(fxNode);

snowgears::fx::ParticlePresetOptions options;
options.spritePath = "sprites/particle.png";
options.autoPlay = true;
options.restart = true;

snowgears::fx::ParticlePresetCatalog::applyByName(effect, "explosion", options);

PlayableCtaPage

Description

snowgears::ads::PlayableCtaPage is a full-screen CTA overlay with a logo area and continue button, designed for playable-ad style flows.

Important Info To Note

  • It auto-creates its UI under UIRoot on start.
  • show() / hide() only toggle visibility; UI elements are reused.
  • Overlay blocks pointer input while visible (blocksInput=true).
  • If logoSpritePath is invalid or missing, it shows a fallback colored rect.
  • Handle button taps via onContinue.

Sample Usage

auto* ctaNode = createNode("cta");
auto* cta = ctaNode->addComponent<snowgears::ads::PlayableCtaPage>();
addChild(ctaNode);

snowgears::ads::PlayableCtaPage::Config cfg;
cfg.showOnStart = false;
cfg.logoSpritePath = "branding/logo.png";
cta->config(cfg);

cta->onContinue = [cta]() {
    cta->hide();
};

After the component has received its first update and created its UI, show the existing overlay when the surrounding flow is ready:

cta->show();

ProgressFill

Description

snowgears::ui::ProgressFill is a self-managed 3-layer progress widget (background, fill, foreground) with rectangular or radial fill behavior.

Important Info To Note

  • Progress is clamped to [0, 1] for both target and rendered values.
  • Supports Snap mode and response-smoothed mode via smoothResponseSeconds.
  • Supports rectangular directions and radial direction/start-angle configuration.
  • Sprite paths can be set by config or overridden at runtime via SpriteRegion.
  • It auto-creates and manages UI elements under UIRoot.

Sample Usage

auto* progressNode = createNode("progress_ui");
auto* progressFill = progressNode->addComponent<snowgears::ui::ProgressFill>();
addChild(progressNode);

snowgears::ui::ProgressFill::Config cfg;
cfg.anchorMin = { 0.5f, 1.0f };
cfg.anchorMax = { 0.5f, 1.0f };
cfg.position = { 0.0f, -48.0f };
cfg.size = { 320.0f, 36.0f };
cfg.backgroundSpritePath = "ui/progress_background.png";
cfg.fillSpritePath = "ui/progress_fill.png";
cfg.progressType = snowgears::ui::ProgressFill::ProgressType::Rect;
cfg.progressMode = snowgears::ui::ProgressFill::ProgressMode::Smooth;
cfg.smoothResponseSeconds = 0.12f;
cfg.initialProgress = 1.0f;
progressFill->config(cfg);

progressFill->setProgress(0.65f);
const float target = progressFill->targetProgress();
progressFill->setProgressImmediate(1.0f);