Skip to content

Components

Components are attached to Node objects. A component may also implement:

  • Drawable to render
  • Updatable to receive update(dt)
  • LateUpdatable to receive lateUpdate(dt) after all regular component updates

All components inherit from Component, which gives access to AppContext, Scene, AssetManager, Input, EventBus, and SaveSystem via helper functions.

Built-in Components

  • Transform (always present on every node)
  • Camera (2D view + shake)
  • SpriteRenderer (draws a textured quad)
  • QuadRenderer (draws a solid-color quad)
  • TriangleRenderer (draws a solid-color triangle)
  • CircleRenderer (draws a solid-color circle)
  • LineRenderer (draws a thick polyline with optional smoothing and rounded caps)
  • FontRenderer (MSDF text rendering)
  • SpineRenderer (Spine skeletal animation)
  • EffekseerRenderer (Effekseer runtime effects)
  • TiledRenderer (Tiled JSON map rendering + query/mutation API)
  • FbxRenderer (optional static/skinned FBX model rendering and animation)
  • ParticleEffects (2D particle emitter)
  • Trail (vertex trail that follows node movement)
  • RigidBody (2D physics body)
  • PhysicsCollider (2D collision shape/material/filter)
  • Script (base class with onStart/onUpdate)
  • ActionRunner (runs actions/tweens on a node)

FontRenderer supports MSDF text effects through effects.outline and effects.shadow. Effect widths, spreads, softness, and offsets use the same local units as fontSize and reuse the loaded font atlas. Very thick effects require a font atlas generated with enough MSDF distance range and padding.

Text uses the solid color by default. Enable gradient for a two-color vertical fill spanning the complete laid-out text block; outlines and shadows remain solid-colored:

auto* labelNode = scene()->createNode("label");
auto* label = labelNode->addComponent<snowpulse::FontRenderer>();
label->font(assetManager()->loadFont("fonts/interface.json"));
label->text = "Ready";
label->fontSize = 36.0f;
label->effects.outline.width = 2.0f;
label->effects.shadow.enabled = true;
label->effects.shadow.setDirection({ 1.0f, -1.0f }, 3.0f);
label->gradient.enabled = true;
label->gradient.topColor = { 1.0f, 0.96f, 0.82f, 1.0f };
label->gradient.bottomColor = { 0.96f, 0.68f, 0.22f, 1.0f };
scene()->rootNode()->addChild(labelNode);

FontRenderer uses RenderSpace::World by default. Set screen render space when its owning node's transform should use logical screen coordinates and remain unaffected by camera position, rotation, zoom, or shake:

label->renderSpace = snowpulse::FontRenderer::RenderSpace::Screen;
labelNode->transform().position = { 320.0f, 180.0f, 0.0f };

For a complete self-cleaning floating-text presentation with screen anchors, movement, fading, and either FontRenderer or UIText, use snowpulse::fx::ToastEffect from Game Feedback.

Primitive Renderers

QuadRenderer, TriangleRenderer, and CircleRenderer draw simple solid-color shapes without requiring textures. They are useful for prototypes, debug overlays, hit-area visualization, and simple VFX.

The examples in this section assume node is already attached to the scene tree. A quad needs only a size and color:

auto* panel = node->addComponent<snowpulse::QuadRenderer>();
panel->size = { 240.0f, 72.0f };
panel->color = { 0.08f, 0.12f, 0.18f, 0.9f };

TriangleRenderer renders exactly three local-space points transformed by the owner node. Degenerate triangles are skipped. Set points, color, sortOrder, and blendMode directly:

auto* triangle = node->addComponent<snowpulse::TriangleRenderer>();
triangle->points = {
    glm::vec2 { 0.0f, 0.0f },
    glm::vec2 { 120.0f, 0.0f },
    glm::vec2 { 60.0f, 90.0f }
};
triangle->color = { 1.0f, 0.6f, 0.1f, 1.0f };
triangle->sortOrder = 5;
triangle->blendMode = snowpulse::BlendMode::Normal;

CircleRenderer draws a full circle by default. Set fillAmount between 0.0f and 1.0f to draw a radial sector, with angles measured in degrees from the positive x-axis:

auto* timer = node->addComponent<snowpulse::CircleRenderer>();
timer->radius = 16.0f;
timer->color = { 1.0f, 0.0f, 0.0f, 1.0f };
timer->fillAmount = 0.5f;
timer->radialStartAngle = 90.0f;
timer->radialDirection = snowpulse::CircleRadialDirection::Clockwise;

Fill amounts are clamped to the valid range. A value of 0.0f draws nothing, while 1.0f uses the normal full-circle rendering path.

Line Renderer

LineRenderer turns local-space anchor points into a thick triangle strip. It can smooth corners with cubic Bézier sampling and optionally add rounded end caps.

auto* route = node->addComponent<snowpulse::LineRenderer>();
route->thickness = 10.0f;
route->color = { 0.2f, 0.8f, 1.0f, 0.9f };
route->useBezierCorners = true;
route->useRoundedEndCaps = true;
route->smoothing = 0.75f;
route->sampleStep = 12.0f;
route->setAnchorPoints({
    { 0.0f, 0.0f, 0.0f },
    { 80.0f, 40.0f, 0.0f },
    { 160.0f, 0.0f, 0.0f },
});

Use clear() to remove all points. Fewer than two points, or non-positive thickness, produces no geometry.

FBX Models

FbxRenderer is available when the target selects ASSIMP ON through snowpulse_add_game() or snowpulse_target_link_engine(). It renders static and skinned FBX submeshes through the model render queue and uses the active scene's ambient and directional lighting.

auto* model = node->addComponent<snowpulse::FbxRenderer>();
model->sortOrder = 20;
model->color = { 1.0f, 1.0f, 1.0f, 1.0f };
model->animationSpeed = 1.0f;

if (model->load("models/character.fbx")) {
    model->playAnimation("Idle", true);
}

animationNames() lists clips in the base model. addAnimationClips() can load additional clips from another FBX file when its skeleton bone layout matches the base model; an optional prefix prevents name collisions.

Camera

The scene's active Camera exposes zoom and transient shake directly:

auto* activeCamera = scene()->camera();
activeCamera->zoom = 1.25f;
activeCamera->shakeFrequency = 30.0f;
activeCamera->shake(8.0f, 0.25f);

if (activeCamera->isShaking()) {
    // Avoid starting another shake if the current one should finish first.
}

Particles and Trails

ParticleEffects can load an effect document, be controlled at runtime, and report its active state. Trail records movement of its owner node.

auto* emitterNode = scene()->createNode("emitter");
auto* emitter = emitterNode->addComponent<snowpulse::ParticleEffects>();
scene()->rootNode()->addChild(emitterNode);

if (emitter->effects("effects/burst.json")) {
    emitter->play(true);
}
emitter->emit(4);  // Emit an additional burst without rebuilding the component.

auto* trailNode = scene()->createNode("trail");
auto* trail = trailNode->addComponent<snowpulse::Trail>();
trail->width = 10.0f;
trail->lifetime = 0.45f;
trail->color = { 0.3f, 0.8f, 1.0f, 0.8f };
scene()->rootNode()->addChild(trailNode);

Script Helpers

Script includes helpers for quick node creation:

  • createNodeAndComponent<T>(name, ...) creates a detached scene-owned node and adds a component; parent the returned component's owner explicitly.
  • addChildNodeAndComponent<T>(name, ...) does the same, then parents the node under the script owner.
class Spawner final : public snowpulse::Script {
public:
    void onStart() override {
        auto* fx = createNodeAndComponent<snowpulse::ParticleEffects>("spawn_fx");
        fx->effects("effects/spawn.json");
        addChild(fx->owner());

        auto* badge = addChildNodeAndComponent<snowpulse::SpriteRenderer>("badge");
        badge->sprite(assetManager()->loadSprite("ui_badge.png"));
    }
};

Late Update Components

Use LateUpdatable for behavior that should run after movement/physics and other normal Updatable logic in the same frame (for example, camera follow that must read final target positions).

class CameraFollow final : public snowpulse::Script, public snowpulse::LateUpdatable {
public:
    void onUpdate(const float dt) override {
        (void)dt;
        // Normal application update.
    }

    void lateUpdate(const float dt) override {
        (void)dt;
        // runs after all Updatable components
    }
};

Sample Usage

auto* node = scene()->createNode("content");
node->transform().position = { 0.0f, 0.0f, 0.0f };

auto* sprite = node->addComponent<snowpulse::SpriteRenderer>();
sprite->sprite(assetManager()->loadSprite("images/icon.png"));

auto* spine = node->addComponent<snowpulse::SpineRenderer>();
spine->load("spine/character/character.atlas",
            "spine/character/character.json", 1.0f);
spine->setAnimation(0, "loop", true);

auto* tiled = node->addComponent<snowpulse::TiledRenderer>();
tiled->load("maps/layout.tmj");

#if SNOWPULSE_HAS_EFFEKSEER
// Also include <runtime/components/effekseer_renderer.h> when this feature is enabled.
auto* efk = node->addComponent<snowpulse::EffekseerRenderer>();
efk->sortOrder = 8;
efk->load("effects/sample.efkefc", 1.0f);
efk->playPrimary();
#endif

scene()->rootNode()->addChild(node);