Skip to content

Game Feedback

Snowpulse's snowpulse::fx namespace contains reusable presentation effects for common success, reward, and transition moments. These helpers build on the lower-level particle, renderer, and action systems while owning their temporary scene nodes and cleanup.

The visual spawn() helpers return transient, non-owning pointers. While an effect is active, use its pointer to call isPlaying() or cancel(). Natural completion or cancellation queues the owned effect subtree for destruction, after which the pointer must not be used.

ConfettiEffect

snowpulse::fx::ConfettiEffect creates a responsive, one-shot, screen-space confetti celebration. Its default paper pieces use the renderer's textureless white quad tinted by a built-in palette; callers can supply their own SpriteRegion and tune the effect through the nested Config type.

The simplest call uses the default configuration. Choose the sort order at the call site so the celebration sits in the intended UI layer:

const int sortOrder = scene->rootUI()->baseSortOrder() - 200;
snowpulse::fx::ConfettiEffect::spawn(scene, sortOrder);

The effect uses logical screen coordinates, so its coverage follows the current viewport and is not displaced by camera movement, zoom, or shake. A spawned celebration cleans itself up after emission and all live particles finish.

Customization

Pass a Config as the third argument to replace the default confetti sprite or adjust the presentation. Load replacements through the scene's AssetManager, just like any other sprite, so they are packaged with the rest of the application assets on desktop, web, iOS, and Android.

snowpulse::fx::ConfettiEffect::Config config;
config.sprite = scene->appContext()->assetManager()->loadSprite(
    "effects/confetti.png");
config.pieceCount = 192;
config.launchSpeedMin = 680.0f;
config.launchSpeedMax = 960.0f;
config.leftCannonOffset = { -220.0f, -380.0f };
config.rightCannonOffset = { 220.0f, -380.0f };
config.palette = {
    { 0.26f, 0.82f, 1.0f, 1.0f },
    { 1.0f, 0.34f, 0.58f, 1.0f },
    { 1.0f, 0.86f, 0.18f, 1.0f },
};

snowpulse::fx::ConfettiEffect::spawn(
    scene,
    scene->rootUI()->baseSortOrder() - 200,
    config);

Config::pieceCount is the positive total shared across both cannons; spawn() returns nullptr for a non-positive count. The optional sprite region is stored by value, so callers do not have to keep a separate region object alive. leftCannonOffset and rightCannonOffset use screen-center coordinates: { 0, 0 } is the center, positive X points right, and positive Y points up. Offsets and other physical values such as piece size, speed, and gravity are logical units at Config::referenceHeight and scale uniformly with the viewport height when the effect is spawned. Active cannon positions are re-resolved if the logical viewport changes; already-emitted pieces keep their spawn-time physics.

The returned effect pointer is a transient, non-owning handle. Use it only to query isPlaying() or call cancel() while the effect is active, because the effect destroys itself after completion.

An empty sprite value, or a region without a texture, uses the renderer's white quad tinted by the configured palette.

FireworksEffect

snowpulse::fx::FireworksEffect creates a one-shot, screen-space fireworks show. Each shell rises from the lower viewport with an additive trail and then spawns a separate radial spark burst in the upper viewport. Calls are independent, and each controller removes its temporary nodes after the final trail and spark have expired.

auto* fireworks = snowpulse::fx::FireworksEffect::spawn(
    scene,
    scene->rootUI()->baseSortOrder() - 200);

The default show launches five shells with 48 sparks each. Config can change the shell and spark counts, normalized launch and burst regions, launch timing, rocket and spark physics, palette, trail dimensions, and reference-height scaling. Optional rocketSprite and sparkSprite regions replace the default white quads. Set seed when a repeatable show is required.

snowpulse::fx::FireworksEffect::Config config;
config.shellCount = 8;
config.sparksPerShell = 64;
config.seed = 2026u;
config.burstAreaMin = { 0.12f, 0.50f };
config.burstAreaMax = { 0.88f, 0.90f };

snowpulse::fx::FireworksEffect::spawn(scene, -1200, config);

Launch and burst regions use normalized logical-screen coordinates with the origin at the bottom-left. Other physical values are logical units at referenceHeight and scale with the viewport height.

Both shellCount and sparksPerShell must be positive; otherwise spawn() returns nullptr. At least one endpoint in each rocket-flight and spark-lifetime range must also be positive and finite. The returned pointer supports isPlaying() and immediate cancel(), and becomes invalid after the show naturally cleans itself up.

SunburstEffect

snowpulse::fx::SunburstEffect draws a procedural, rotating sunburst that covers the logical viewport. It is persistent by design and remains active until cancelled or until its scene is destroyed.

auto* sunburst = snowpulse::fx::SunburstEffect::spawn(scene, -1050);

// Later, when the reward presentation closes:
if (sunburst && sunburst->isPlaying()) {
    sunburst->cancel();
}

The default uses 18 alternating gold wedges centered on the screen and rotates at eight degrees per second. Config controls the normalized center, wedge count, colors, starting rotation, rotation speed, and blend mode. Odd or small wedge counts are normalized to an even value of at least four. The mesh tracks viewport changes and stays fixed while the camera moves, zooms, or shakes.

FlashEffect

snowpulse::fx::FlashEffect snaps a full-screen color curtain to its configured opacity, optionally holds it, then fades it to transparent and cleans itself up. It is presentation-only and does not intercept input.

snowpulse::fx::FlashEffect::Config config;
config.color = { 1.0f, 1.0f, 1.0f, 1.0f };
config.holdDurationSeconds = 0.0f;
config.fadeDurationSeconds = 0.20f;
config.fadeEase = snowpulse::EaseType::OutQuad;

snowpulse::fx::FlashEffect::spawn(scene, -1400, config);

The curtain follows logical-resolution changes and remains invariant under all camera transforms. It participates in the normal render queue, so it can cover scene and UI content according to its sort order, but it does not cover ImGui or native platform overlays rendered after the scene. Keep the returned pointer only while isPlaying() is true; cancel() removes the curtain immediately.

ToastEffect

snowpulse::fx::ToastEffect displays transient MSDF text at a logical-screen location, optionally holds it, then moves and fades it before removing all of its temporary objects. Its position and font size are unaffected by camera position, rotation, zoom, shake, or pump effects.

screenAnchor is normalized from the bottom-left of the logical viewport and defaults to { 0.5, 0.5 }. position is a logical-pixel offset from that anchor, with positive X pointing right and positive Y pointing up. Therefore, position = { 10, 10 } places the text ten pixels right and ten pixels above the center, even when the camera is at { 100, 100 } with zoom = 5.

snowpulse::fx::ToastEffect::Config config;
config.font = scene->appContext()->assetManager()->loadFont(
    "fonts/interface.json");
config.text = "+250";
config.fontSize = 42.0f;
config.color = { 1.0f, 0.84f, 0.22f, 1.0f };
config.effects.outline.width = 3.0f;
config.effects.outline.color = { 0.08f, 0.06f, 0.02f, 1.0f };
config.sortOrder = scene->rootUI()->baseSortOrder() - 100;
config.screenAnchor = { 0.5f, 0.5f };
config.position = { 10.0f, 10.0f };
config.movement = { 0.0f, 56.0f };
config.holdDurationSeconds = 0.15f;
config.fadeDurationSeconds = 0.65f;

snowpulse::fx::ToastEffect::spawn(scene, config);

TextRendererType names the implementation choice more precisely than a space type, because both choices are screen-positioned:

  • TextRendererType::UIText is the default. It creates a temporary UIText under the UI root and supports textBoxSize, autoSize, wordWrap, and overflowMode. The toast remains screen-space even if the application's UI root uses its view transform.
  • TextRendererType::FontRenderer creates a FontRenderer on the toast's scene node and sets its render space to FontRenderer::RenderSpace::Screen.

Both choices support font, fontSize, lineSpacing, letterSpacing, solid or gradient colors, outline and shadow effects, texture filtering, horizontal and vertical alignment, and an absolute render-queue sortOrder. Use uiText() or fontRenderer() to inspect the selected transient text object while isPlaying() is true. Natural completion and cancel() remove both the text and controller; the returned pointer is invalid afterward.

PumpEffect

snowpulse::fx::PumpEffect is a camera-owned punch zoom. It immediately multiplies the active view zoom, then eases the multiplier back to neutral. The camera's public base zoom is never changed, so camera controllers can continue updating it while the pump is active.

The default snaps to 1.08x and returns over 0.18 seconds with OutCubic.

snowpulse::fx::PumpEffect::play(scene);

snowpulse::fx::PumpEffect::Config config;
config.zoomMultiplier = 1.12f;
config.returnDurationSeconds = 0.24f;
config.returnEase = snowpulse::EaseType::OutCubic;
snowpulse::fx::PumpEffect::play(scene, config);

Retriggering replaces and restarts the active pump. cancel(scene) immediately returns the multiplier to neutral, and isPlaying(scene) reports whether the active camera is still returning.

ShakeEffect

snowpulse::fx::ShakeEffect exposes the existing camera shake through the FX catalog without removing or deprecating Camera::shake().

The default uses intensity 8 for 0.30 seconds.

snowpulse::fx::ShakeEffect::Config config;
config.intensity = 12.0f;
config.durationSeconds = 0.35f;
snowpulse::fx::ShakeEffect::play(scene, config);

if (snowpulse::fx::ShakeEffect::isPlaying(scene)) {
    snowpulse::fx::ShakeEffect::cancel(scene);
}

The newest shake replaces the previous one. Frequency and noise continue to come from the camera's public shakeFrequency and noiseScale settings. Pump and Shake may run at the same time; default UI and all screen-space FX remain stationary while world content is transformed.

Choosing the right layer

Snowpulse uses lower sort-order values for later, foreground rendering. Derive screen-effect ordering from UIRoot::baseSortOrder() rather than hard-coding a global value. This keeps the effect aligned if an application changes its UI base layer.

For custom emitters or persistent environmental effects, configure ParticleEffects directly. See Particles and Trails.