Skip to content

Physics

Snowpulse physics uses Box2D when available and exposes two gameplay components:

  • RigidBody
  • PhysicsCollider

If Box2D is unavailable or disabled, physics calls are safe no-ops.

To put a simulated object on screen first, start with Quick Start. World scaling and build configuration are covered after the basic component model.

World Scale Factor

Use AppContext::physicsScaleFactor(float) to set global world scaling for physics.

  • Default: 100.0f
  • Minimum: clamped to a small positive value (0.0001f)
  • Mapping:
  • game -> physics (spatial only): physics = game / scale
  • physics -> game (spatial only): game = physics * scale
  • Runtime behavior: changing scale while a scene is active immediately rebuilds the Box2D world from current scene objects/transforms.

physicsScaleFactor only affects spatial values:

  • Node/body positions synced into Box2D
  • Collider geometry (size, radius, offset) synced into Box2D
  • Simulated positions written back to node transforms

Dynamics values stay raw Box2D units (not auto-rescaled):

  • World gravity
  • setLinearVelocity / getLinearVelocity
  • applyForce / applyImpulse
  • Collision approachSpeed

Build Toggle

Physics integration is controlled at configure time:

  • CMake option: SNOWPULSE_ENABLE_BOX2D (ON by default)
  • Compile definition: SNOWPULSE_HAS_BOX2D (1 when Box2D is linked, otherwise 0)

See Dependencies for build commands.

Quick Start

  1. Create a node and add it under the scene root (for Scene subclasses this is addChild(node)).
  2. Add any visual component (SpriteRenderer, QuadRenderer, etc.).
  3. Add RigidBody if the node should have dynamic/kinematic/static body behavior.
  4. Add PhysicsCollider to define collision shape/material/filtering.
auto* boxNode = createNode("box");
boxNode->transform().position = { 70.0f, 160.0f, 0.0f };

auto* quad = boxNode->addComponent<snowpulse::QuadRenderer>();
quad->size = { 52.0f, 52.0f };
quad->color = { 0.96f, 0.62f, 0.16f, 1.0f };

auto* body = boxNode->addComponent<snowpulse::RigidBody>();
body->bodyType = snowpulse::RigidBody::BodyType::Dynamic;
body->gravityScale = 120.0f;
body->linearDamping = 0.03f;
body->angularDamping = 0.05f;
body->isBullet = true;

auto* collider = boxNode->addComponent<snowpulse::PhysicsCollider>();
collider->shape = snowpulse::PhysicsCollider::Shape::Box;
collider->size = { 52.0f, 52.0f };
collider->density = 1.0f;
collider->friction = 0.35f;
collider->restitution = 0.82f;

addChild(boxNode);

RigidBody

Key fields:

  • bodyType: Static, Kinematic, Dynamic
  • gravityScale, linearDamping, angularDamping
  • fixedRotation, isBullet
  • linearVelocity, angularVelocity

Key methods:

  • setLinearVelocity, getLinearVelocity
  • setAngularVelocity, getAngularVelocity
  • applyForce
  • applyImpulse

Collision callbacks:

  • onCollisionEnter(...)
  • onCollisionExit(...)
  • onCollisionHit(...)

Callback payload (RigidBody::CollisionInfo) includes:

  • otherNode
  • otherRigidBody
  • selfCollider, otherCollider
  • normal
  • approachSpeed (for hit events)

PhysicsCollider

Key fields:

  • shape: Box or Circle
  • size (box), radius (circle)
  • offset
  • density, friction, restitution
  • isSensor (overlap-only trigger; no physical collision response)
  • categoryBits, maskBits, groupIndex (collision filtering)

Density notes:

  • Engine accepts density >= 0 (negative values are clamped to 0).
  • There is no hard upper clamp in Snowpulse; practical values depend on your chosen physicsScaleFactor and collider sizes.

Notes:

  • Colliders can exist without RigidBody.
  • isSensor = true turns a collider into a trigger volume. Use onCollisionEnter / onCollisionExit to track overlap timing.
  • For collider-only nodes, runtime creates an internal proxy body so transforms still participate in collisions.
  • Physics derives position and rotation from the node's world pose, so colliders and rigid bodies can live on child nodes under transformed parents.
  • When Box2D updates a child body, Snowpulse writes the simulated world pose back into the node's local transform so rendering stays aligned with hierarchy-local authoring.
  • Physics ignores node and parent scale. Collider size, radius, and offset stay in authored physics units.
  • Collision callbacks are dispatched on RigidBody, not on PhysicsCollider.
  • onCollisionHit is only for non-sensor contact impacts.

Collision Filtering

Use filtering fields to control who collides with whom:

  • categoryBits: what this collider is
  • maskBits: what categories it collides with
  • groupIndex: force always-collide or never-collide behavior for same-group bodies

Example: disable all collisions for one collider:

collider->maskBits = 0;

Runtime Behavior

  • Physics steps at fixed 1/60 with substeps for stability.
  • Render interpolation is enabled by default through AppContext::physicsInterpolation(true).
  • Scene transform and physics body are synchronized by the runtime.
  • Dynamic bodies update node transforms from simulation.
  • Static/kinematic (and manually moved bodies) push transform changes back to Box2D.
  • Parent translation and Z rotation are included when syncing child physics nodes to Box2D.
  • Disabling a RigidBody or its owner node pauses simulation; re-enabling it restores the latest cached linear and angular velocity, including commands issued while inactive.
  • Physics remains 2D only: only position.x/y and rotation.z participate.

Render Interpolation

After fixed stepping, Snowpulse interpolates active dynamic bodies between their previous and current simulated world poses. The interpolated position and shortest-arc Z rotation are stored as render-only transform overrides; physics queries and gameplay transforms still use the current simulation state.

Disable interpolation when exact fixed-step presentation is preferable:

context()->physicsInterpolation(false);

Disabling it clears render overrides before drawing, so dynamic bodies render at their latest simulated pose. Static and kinematic bodies are never interpolated.

Practical Tips

  • In pixel-scale games, tune gravityScale, size, and impulse together.
  • Use setLinearVelocity/setAngularVelocity (or impulses/forces) during gameplay rather than mutating only local cached fields.
  • Ensure both component isActive and owner node isActive are true for simulation/collision participation.
  • Parent and child scale do not resize or reposition physics shapes; if you need a larger collider, change the collider data directly.