Skip to content

Snap Physics Plugin

he Snap Physics Plugin provides a lightweight, deterministic physics system inspired by Maddy Thorson’s TowerFall physics engine. It excels at platformer-style games requiring precise collision detection and resolution.

Core Concepts

The system is built around three main entity types:

Actors (Actor)

Dynamic entities that can move and collide with other objects. Actors:

  • Handle their own movement and collision detection
  • Can ride on moving platforms
  • Support pass-through collision filtering
  • Maintain sub-pixel movement precision
class Player extends Actor {
update(deltaTime: number) {
// Move with precise collision detection
this.moveX(velocity.x * deltaTime);
this.moveY(velocity.y * deltaTime);
}
}

Solids (Solid)

Static or kinematic objects that block movement. Solids:

  • Can move and push actors
  • Support one-way platforms
  • Handle riding actors automatically
  • Can be static or kinematic
class Platform extends Solid {
move(x: number, y: number) {
// Automatically pushes riding actors
this.move(x, y);
}
}

Sensors (Sensor)

Trigger areas that detect but don’t block movement. Sensors:

  • Detect overlapping entities
  • Don’t affect movement
  • Support filtering by entity type
  • Useful for triggers and detection zones
class TriggerZone extends Sensor {
passThroughTypes = ['Actor', 'Player']; // Entities that won't trigger
update() {
// Check all overlapping entities
this.activeCollisions = this.resolveAllCollisions();
}
}

Collision Resolution

Basic Resolution

By default, collisions stop movement and prevent overlap. However, you can customize this behavior by implementing a collision resolver:

src/scenes/MyPhysicsScene.ts
import { Scene } from 'dill-pixel';
import SnapPhysicsPlugin from '@dill-pixel/plugin-snap-physics';
export class MyPhysicsScene extends Scene {
get physics() {
return this.app.getPlugin('snap-physics') as SnapPhysicsPlugin;
}
async initialize() {
// ... other initialization code
this.physics.system.initialize({
// ... other options
collisionResolver: this._resolveCollision,
});
}
private _resolveCollision(collision: Collision) {
// Implement your custom collision resolution logic here
switch (collision.type) {
case 'Player|Enemy':
handlePlayerEnemyCollision(collision);
return false; // Don't block movement
case 'Player|Coin':
collectCoin(collision);
return false; // Pass through
default:
return true; // Block movement (default behavior)
}
}
}

Advanced Resolution

The collision resolver receives detailed information about each collision:

type Collision = {
entity1: Entity; // First colliding entity
entity2: Entity; // Second colliding entity
type: string; // Combined type (e.g. "Player|Platform")
direction: string; // Collision direction
overlap: {
// Overlap amounts
x: number;
y: number;
};
// Additional collision data
};

Riding & Pass-Through

The system supports advanced features like:

  • Riding - Actors can ride moving platforms
  • Pass-Through - One-way platforms and selective collision filtering
  • Pushing - Solids can push actors out of the way when moving
// Example of configuring pass-through types
class Player extends Actor {
passThroughTypes = ['OneWayPlatform'];
}

Common Resolution Patterns

  1. One-Way Platforms:
// Example of a one-way platform`
private _resolveCollision(collision: Collision): boolean {
if (collision.type === 'Player|Platform') {
const platform = collision.entity2 as Platform;
if (platform.oneWay) {
// Only collide when coming from above
return collision.direction === 'bottom';
}
}
return true;
}
  1. Trigger Areas:
class TriggerZone extends Sensor {
private _onPlayerEnter() {
// Handle player entering trigger zone
}
update() {
// Check for collisions without blocking
const collisions = this.resolveAllCollisions();
if (collisions) {
collisions.forEach((collision) => {
if (collision.entity2.type === 'Player') {
this._onPlayerEnter();
}
});
}
}
}
  1. Moving Platforms:
class MovingPlatform extends Solid {
move(dx: number, dy: number) {
// First move the platform
this.position.set(this.x + dx, this.y + dy);
// Then push any riding actors
this.getAllRiding().forEach((actor) => {
actor.move(dx, dy);
});
}
}

Spatial Partitioning

For better performance with many objects, the system uses a spatial hash grid:

// Configure in your scene
this.physics.system.initialize({
useSpatialHashGrid: true,
gridCellSize: 200, // Adjust based on average object size
});

Fixed Update System

Unlike other plugins that use Pixi’s ticker, the Snap Physics Plugin implements a fixed timestep update loop for deterministic physics simulation. This approach ensures:

  • Consistent physics behavior regardless of frame rate
  • Predictable collision detection
  • Frame-rate independent gameplay logic

How it Works

The system uses setInterval to run physics updates at a fixed frequency (default 60 FPS):

static set enabled(value: boolean) {
if (value === System._enabled) return;
System._enabled = value;
if (System._enabled) {
// Start fixed update loop
System._fixedUpdateInterval = setInterval(() => {
System.fixedUpdate(System._fixedTimeStep / 1000);
}, System._fixedTimeStep);
} else {
// Stop fixed update loop
if (System._fixedUpdateInterval) {
clearInterval(System._fixedUpdateInterval);
System._fixedUpdateInterval = null;
}
}
}

The fixed update cycle processes entities in a specific order:

  1. Pre-update phase for all entities
  2. Custom update hooks
  3. Solids update
  4. Sensors update
  5. Actors update
  6. Post-update phase for all entities
  7. Camera update (if enabled)
  8. Debug drawing (if enabled)

Configuration

You can configure the update frequency when initializing the plugin:

this.physics.initialize({
fps: 60, // Sets the fixed update rate
// other options...
});

Entity Updates

Each entity type (Actor, Solid, Sensor) can implement the following update methods:

class MyEntity extends Actor {
// Called before physics update
preFixedUpdate() {}
// Main physics update
fixedUpdate(deltaTime: number) {
// Implement physics logic
}
// Called after physics update
postFixedUpdate() {}
}

For implementation examples, see:

startLine: 67
endLine: 115

Cleanup

When destroying the plugin, the fixed update loop is automatically cleaned up.

Examples

For implementation examples, see the following: