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:
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
VirtualAnalogStickor 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(UIRectbase + knob). - Pointer input is read from
MouseLeft;axis()is clamped to unit length. lockCenter=truemeans touch/click must begin inside the configured radius.lockCenter=falsemeans the stick center snaps to the press position.visibleWhenIdle=falsehides 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::Cameracomponent. - It runs in
lateUpdate, so it follows final transforms for the frame. zoom()is clamped to a positive value (minimum0.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
GridNavMapassigned,moveToWorlduses A* to generate waypoints. - Without a nav map,
moveToWorldstill works by moving directly to the world target. moveByAxiswith non-zero input clears any active path.onDestinationReachedfires 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 optionalorigin. cellSizeis clamped internally to at least0.01f.resize()recreates the grid and resets all cells to walkable.worldToCell()usesfloor, so negative positions map predictably by cell.- Use
fillWalkable,setWalkable, andsetBlockedto 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+gridSizein grid cells (inclusive min, exclusive max). gridSizeis clamped to at least{1, 1}.type=Unknownkeeps the legacy dwell/enter/leave behavior and does not accept deposits, withdrawals, or work progress.stayDurationSeconds <= 0triggersonTriggerStartimmediately on entry.onTriggerEndfires only for units that already firedonTriggerStart.- Only units explicitly registered with
trackUnit(...)are evaluated. - Navigation maps and tracked units are non-owning pointers. Call
removeUnit(...)orclearUnits()before a tracked component is destroyed. Sinkaccepts deposits withdeposit(amount, unit). Finite sinks complete whenamount()reachesmaxAmount;maxAmount <= 0means unlimited and never completes.Containeracceptsdeposit(...)andwithdraw(...). Finite containers fireonCompletedeach time they cross from not-full to full;maxAmount <= 0means unlimited and never becomes full.Workadvances per activated unit after the dwell gate.preserveIncompleteWork=falseresets unfinished work when a unit exits;truepreserves 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. depositUnitWhitelistgates sink/container deposits,withdrawUnitWhitelistgates container withdrawals, andworkUnitWhitelistgates work dwell/progress.- Use
depositUnitWhitelist(...),withdrawUnitWhitelist(...), andworkUnitWhitelist(...)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(...)andgridAreaTriggerTypeFromString(...)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=trueenables 8-direction traversal (with diagonal cost).start == goalreturnsfound=truewith a single-cell path.cellsare 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 callsstop(), so retriggering clears any active burst.- Source/target positions passed to
play()are in the owner node's local space. - If
coinSpritePathdoes not resolve, it falls back toQuadRenderervisuals. onCoinArrived(index)fires per coin;onCompleted()fires after all arrivals.coinCountis clamped to at least1.
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, andsun-rays. apply()resets modules to defaults before applying preset values.applyByName()is case-insensitive and acceptssun-rays,sun_rays, orsunrays.ParticlePresetOptionscan apply a sprite and control autoplay/restart.apply()andapplyByName()returnfalseon 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
UIRooton start. show()/hide()only toggle visibility; UI elements are reused.- Overlay blocks pointer input while visible (
blocksInput=true). - If
logoSpritePathis 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:
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
Snapmode and response-smoothed mode viasmoothResponseSeconds. - 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);