Physics
Most games should use physics through Scene and GameObject.
In practice, the workflow is:
- Set gravity on the scene.
- Call
setPhisics(...)on the objects that should participate. - Add one or more hitboxes.
- Optionally author
speed/angularVelocity. - React with
beforeColision(...)andonColision(...).
Typical setup:
import { GameObject, Scene, SquareHitbox, Vector } from "sliver-engine";
const scene = new Scene("level");
scene.setGravity(new Vector(0, 1200));
const crate = new GameObject("crate", new Vector(100, 100));
crate.setPhisics({
immovable: false,
affectedByGravity: true,
mass: 2,
staticFriction: 0.5,
dynamicFriction: 0.3,
});
crate.addHitbox(
new SquareHitbox(Vector.zero(), new Vector(32, 32), crate, {
solid: true,
}),
);
scene.addGameObject(crate);
Configuring a body
Use setPhisics(...) to configure how a GameObject behaves in the physics world:
gameObject.setPhisics({
immovable: false,
affectedByGravity: true,
restitution: 0.2,
friction: 0.4,
staticFriction: 0.5,
dynamicFriction: 0.3,
mass: 2,
inertiaScale: 0.85,
});
Physics settings:
immovable: trueis what you usually want for floors, walls, ramps, and fixed props.immovable: falsemakes the object dynamic so it can be pushed, fall, and rotate.affectedByGravityis the main switch for platformer-style falling vs top-down or scripted motion.restitutioncontrols bounce.frictionis the simple one-value option and acts as a fallback for both friction coefficients.staticFrictionis useful when you want objects to hold more firmly before they start slipping.dynamicFrictionis useful when you want to control how slippery a contact feels once it is already sliding.masschanges how strongly dynamic bodies push each other around.inertiaScalechanges how easily the object spins without changing its mass. Lower values tumble more easily; higher values resist rotation more.
Units and timing
speed and angularVelocity are integrated with dt.
speedis a linear velocity in world units (pixels) per second.angularVelocityis a rotational velocity per second in the same angle unit asrotation.dtis a fixed1 / tickRatein seconds. The physics step does not increasedtto compensate for lag; if the game falls behind, ticks are skipped instead. That means heavy lag can still lead to issues like thin-floor tunneling or noticeable clipping.
Example:
import { Vector } from "sliver-engine";
gameObject.speed = new Vector(120, 0); // 120 px/s
At 60 ticks per second, that moves about 2 pixels each tick because the physics world integrates speed * dt.
Scene gravity is also time-based acceleration:
scene.setGravity(new Vector(0, 1200));
That means a dynamic body with affectedByGravity: true gains roughly 20 units/s of vertical velocity each step at 60 ticks per second.
Hitboxes, compound bodies, and rotation
Physics only exists where hitboxes exist, so a physical object should usually have at least one SquareHitbox or CircleHitbox:
import { GameObject, SquareHitbox, Vector } from "sliver-engine";
const crate = new GameObject("crate", new Vector(100, 100));
crate.setPhisics({ immovable: false, mass: 2 });
crate.addHitbox(
new SquareHitbox(Vector.zero(), new Vector(32, 32), crate, {
solid: true,
debug: false,
}),
);
How hitboxes are used:
- One
GameObjectbecomes one physics body. - Every hitbox on that object becomes a shape on that same body.
- Multiple hitboxes act like one compound body.
- Physics rotation uses the same pivot as rendering, so offset hitboxes still rotate with the visible object.
Hitbox options:
solid: truemakes the hitbox participate in physical collision response.solid: falseturns it into a trigger/sensor.debug: truedraws the hitbox overlay.
How scenes apply physics
You should not step physics manually. Scene.tick() handles it for you:
- your objects run
tick()first - the scene applies gravity and resolves overlaps/collisions
- the solved
position,rotation,speed, andangularVelocityare written back to each object
That means you can author intent in tick() and then let the scene physics step produce the resulting motion.
Sensors, Callbacks, and Suppression
The main gameplay hooks are:
beforeColision(other)is the filter hook.onColision(other, penetration)is the overlap notification hook.
Use them like this:
- return
falsefrombeforeColisionto ignore a pair for that tick - use
onColisionto react to overlaps, pickups, triggers, damage zones, and simple collision responses - mark a hitbox
solid: falsewhen you want notifications without physical pushing
Example trigger:
trigger.addHitbox(
new SquareHitbox(Vector.zero(), new Vector(64, 64), trigger, {
solid: false,
}),
);
Example filter:
override beforeColision(other): boolean {
return other.name !== "player-bullet";
}
The penetration passed to onColision comes from the contact manifold normal and penetration depth for that overlap.
Inspecting Physics Results
Scenes expose the last physics step result:
const result = scene.getLastPhysicsStepResult();
console.log(result.candidatePairs);
console.log(result.sensorManifolds);
console.log(result.solidManifolds);
console.log(result.notifiedPairIds);
console.log(result.suppressedPairIds);
This is useful for:
- debugging sensors vs solid contacts
- checking whether a pair was suppressed by
beforeColision - inspecting which pairs were actually solved
- writing deterministic physics tests
Lower-level physics APIs
Most games should stay with Scene + GameObject + hitboxes. Reach for the lower-level exports only when the high-level API is no longer enough. Common reasons are:
- explicit
PhysicsBodycreation kinematicbody types- collision category/mask filtering
- direct access to
ScenePhysicsWorld - custom gameplay collision stepping or solver configuration
Those APIs are exported directly from "sliver-engine", for example:
import {
PhysicsBody,
ScenePhysicsWorld,
stepGameplayCollisions,
} from "sliver-engine";
Live Examples
Physics Material Tuning
This sandbox compares two groups of draggable boxes with different mass, static friction, dynamic friction, restitution, and inertia scaling settings.
export type PhysicsOptions = { immovable?: boolean; affectedByGravity?: boolean; restitution?: number; friction?: number; staticFriction?: number; dynamicFriction?: number; mass?: number; inertiaScale?: number; }; type ColorPhysicsOptions = { cyan: PhysicsOptions; rose: PhysicsOptions; }; export const createColorPhysicsOptions = (): ColorPhysicsOptions => { // Edit this section only. const cyanPhysicsOptions = { immovable: false, affectedByGravity: true, restitution: 0.35, staticFriction: 0.82, dynamicFriction: 0.46, mass: 1.2, inertiaScale: 0.82, }; const rosePhysicsOptions = { immovable: false, affectedByGravity: true, restitution: 0.8, staticFriction: 0.18, dynamicFriction: 0.08, mass: 0.9, inertiaScale: 1.05, }; return { cyan: cyanPhysicsOptions, rose: rosePhysicsOptions, }; };
Scene Gravity
This sandbox shows a single draggable dynamic box under scene gravity. Edit the gravity vector to change the feel of the simulation.
import { Vector } from "sliver-engine"; import { scaleEnclosureScalar } from "./enclosureDimensions"; export const createGravity = () => { // Edit this section only. const gravityX = 0; const gravityY = 1200; return new Vector( scaleEnclosureScalar(gravityX), scaleEnclosureScalar(gravityY), ); };
Compound Hitboxes
This sandbox shows objects whose visible shape and collision shape do not have to match one simple rectangle. Edit the hitbox configs to see how one object can behave as a compound body.
import type { Vector } from "sliver-engine"; import { mapEnclosurePoint, scaleEnclosureOffset, scaleEnclosureScalar, scaleEnclosureSize, } from "./enclosureDimensions"; export type HitboxType = "square" | "circle"; export type HitboxConfig = { type: HitboxType; offset: Vector; squareSize: Vector; circleRadius: number; solid: boolean; debug: boolean; }; export type HitboxDemoObjectConfig = { position: Vector; shapeSize: Vector; shapeColor: string; hitboxes: HitboxConfig[]; }; type HitboxDemoConfigs = { objectA: HitboxDemoObjectConfig; objectB: HitboxDemoObjectConfig; }; export const createHitboxConfigs = (): HitboxDemoConfigs => { // Edit this section only. return { objectA: { position: mapEnclosurePoint(136, 138), shapeSize: scaleEnclosureSize(54, 30), shapeColor: "#22d3ee", hitboxes: [ { type: "square", offset: scaleEnclosureOffset(18, -10), squareSize: scaleEnclosureSize(22, 22), circleRadius: scaleEnclosureScalar(11), solid: true, debug: true, }, { type: "circle", offset: scaleEnclosureOffset(-8, 8), squareSize: scaleEnclosureSize(18, 18), circleRadius: scaleEnclosureScalar(10), solid: true, debug: true, }, ], }, objectB: { position: mapEnclosurePoint(308, 144), shapeSize: scaleEnclosureSize(48, 44), shapeColor: "#fb7185", hitboxes: [ { type: "circle", offset: scaleEnclosureOffset(-12, 12), squareSize: scaleEnclosureSize(26, 18), circleRadius: scaleEnclosureScalar(14), solid: true, debug: true, }, { type: "square", offset: scaleEnclosureOffset(14, -6), squareSize: scaleEnclosureSize(16, 24), circleRadius: scaleEnclosureScalar(8), solid: true, debug: true, }, ], }, }; };
Non-solid Trigger Hitboxes
This sandbox shows sensor behavior: the green zone overlaps the moving box and triggers gameplay logic without physically blocking it.
import { GameObject, SquareHitbox, Vector } from "sliver-engine"; import { mapEnclosurePoint, scaleEnclosureSize, } from "./enclosureDimensions"; export const TELEPORT_TRIGGER_NAME = "teleport-trigger"; const TRIGGER_SIZE = scaleEnclosureSize(64, 128); const TRIGGER_POSITION = mapEnclosurePoint(368, 68); class TriggerZone extends GameObject { constructor() { super(TELEPORT_TRIGGER_NAME, TRIGGER_POSITION.clone()); this.addHitbox( new SquareHitbox(Vector.zero(), TRIGGER_SIZE.clone(), this, { solid: false, debug: true, }), ); this.setPhisics({ immovable: true, }); this.setRenderFunction((obj, canvas) => { const pos = obj.getPosition(); canvas .getShapeDrawer() .drawRectangle( pos.x, pos.y, TRIGGER_SIZE.x, TRIGGER_SIZE.y, "rgba(16, 185, 129, 0.28)", true, ); }); } } export const createTriggerZone = (): TriggerZone => { return new TriggerZone(); };