Skip to content

Snap Physics Plugin

The Snap Physics Plugin provides a lightweight 2D physics system for basic collision detection and resolution. It’s ideal for platformers, endless runners, and other games that need simple physics without the overhead of a full physics engine.

Key Features

  • Spatial hash grid for efficient collision detection
  • Configurable FPS for physics updates
  • Support for circular and rectangular colliders
  • Basic gravity and velocity systems
  • Projectile physics support

Getting Started

  1. Add the plugin

    // in package.json
    {
    "dependencies": {
    // ... other dependencies
    "@dill-pixel/plugin-snap-physics": "latest"
    }
    }
  2. Configure the Plugin

    Add the snap-physics plugin to your dill-pixel.config.ts:

    export const config = defineConfig({
    // ... rest of your config
    plugins: [
    [
    'snap-physics',
    {
    autoLoad: false,
    options: {
    useSpatialHashGrid: true,
    gridCellSize: 200,
    },
    },
    ],
    ],
    });
  3. Initialize Physics in Your Scene

    export default class SnapPhysicsScene extends Scene {
    protected get physics(): SnapPhysics {
    return this.app.getPlugin('snap-physics') as unknown as SnapPhysics;
    }
    async initialize() {
    // Initialize physics system
    this.physics.system.initialize({
    gravity: 10,
    container: this.level,
    debug: true,
    useSpatialHashGrid: true,
    cellSize: 300,
    fps: 60,
    boundary: {
    padding: 10,
    thickness: 10,
    height: this.app.size.height,
    width: this.app.size.width,
    },
    });
    }
    }

Collision Detection

Spatial Hash Grid

The spatial hash grid system improves collision detection performance by dividing the game world into a grid and only checking for collisions between objects in the same or adjacent cells.

// Configure spatial hash in your scene (can also sbe set in the config)
this.physics.useSpatialHashGrid = true;
this.physics.gridCellSize = 400; // Adjust based on your game's needs

Collision Resolution

Handle collisions by implementing a collision resolution method:

// in your scene
private resolveCollision(collision: Collision): boolean {
switch (collision.type) {
case 'Player|Platform':
const platform = collision.entity2 as Platform;
const player = collision.entity1 as Player;
if (platform.oneWay) {
if (collision.top) {
player.setPassingThrough(platform);
}
return !player.isPassingThrough(platform);
}
return true;
default:
return true;
}
}

Physics Objects

The plugin supports different types of physics objects:

Basic Actors

Here’s a complete example of some different simple actors and solids:

import { Actor, Solid, WithVelocity } from '@dill-pixel/plugin-snap-physics';
import { Signal, Size } from 'dill-pixel';
import { Texture } from 'pixi.js';
type BasicSolidConfig = {
color: number;
};
type BasicActorConfig = {
color: number;
activeColor: number;
};
export class BasicActor<T extends BasicActorConfig = BasicActorConfig> extends Actor<T, V8Application> {
static speed: number = 5;
static onActivated = new Signal<(actor: BasicActor) => void>();
constructor(config?: Partial<T>) {
super(config);
this.initialize();
}
private _active: boolean = false;
get active(): boolean {
return this._active;
}
set active(value: boolean) {
if (this._active === value) {
return;
}
this._active = value;
if (this._active) {
BasicActor.onActivated.emit(this);
}
this.setColor();
}
initialize() {
this.eventMode = 'static';
this.on('pointerup', this._setActive);
}
destroy() {
this.off('pointerup', this._setActive);
super.destroy();
}
added() {
super.added();
this.setColor();
}
update(deltaTime?: number) {
void deltaTime;
if (!this._active) {
return;
}
if (this.app.isActionActive('move_left')) {
this.moveX(-BasicActor.speed);
}
if (this.app.isActionActive('move_right')) {
this.moveX(BasicActor.speed);
}
if (this.app.isActionActive('jump')) {
this.moveY(-BasicActor.speed);
}
if (this.app.isActionActive('move_down')) {
this.moveY(BasicActor.speed);
}
}
setColor() {
this.view.tint = this._active ? (this.config.activeColor ?? this.config.color) : this.config.color;
}
protected _setActive() {
this.active = true;
}
}
export class BasicSolid<T extends BasicSolidConfig = BasicSolidConfig> extends Solid<T> {
static onActivated = new Signal<(actor: BasicActor) => void>();
constructor(config?: Partial<T>) {
super(config);
this.initialize();
this.eventMode = 'none';
this.interactiveChildren = false;
}
added() {
super.added();
this.setColor();
}
setColor() {
this.view.tint = this.config.color;
}
}
type CircActorConfig = BasicActorConfig & {
radius: number;
};
type RectActorConfig = BasicActorConfig & {
size: Size;
};
type RectSolidConfig = BasicSolidConfig & {
size: Size;
};
type CircSolidConfig = BasicSolidConfig & {
radius: number;
};
export class CircActor extends BasicActor<CircActorConfig> {
static texture: Texture;
static defaults: CircActorConfig = {
radius: 50,
color: 0x00fff0,
activeColor: 0x0000ff,
};
type = 'Circ';
isCircle = true;
constructor(config?: Partial<CircActorConfig>) {
super({ ...CircActor.defaults, ...(config ?? {}) });
}
initialize() {
super.initialize();
if (!CircActor.texture) {
const gfx = this.make.graphics().circle(0, 0, 250).fill({ color: 0xffffff });
CircActor.texture = this.app.renderer.generateTexture(gfx);
}
this.view = this.add.sprite({
asset: CircActor.texture,
anchor: 0.5,
width: this.config.radius * 2,
height: this.config.radius * 2,
tint: this.config.color,
});
}
init(config?: Partial<CircActorConfig>) {
this.config = { ...CircActor.defaults, ...(config ?? {}) };
if (this.view) {
this.removeChild(this.view);
}
this.initialize();
}
reset() {
this.active = false;
this.x = 0;
this.y = 0;
}
}
export class RectActor extends BasicActor<RectActorConfig> {
static defaults: RectActorConfig = {
size: { width: 200, height: 100 },
color: 0x00fff0,
activeColor: 0x0000ff,
};
type = 'Rect';
constructor(config?: Partial<RectActorConfig>) {
super({ ...RectActor.defaults, ...(config ?? {}) });
}
initialize() {
super.initialize();
const gfx = this.make
.graphics()
.rect(0, 0, this.config.size.width, this.config.size.height)
.fill({ color: 0xffffff });
this.view = this.add.sprite({ asset: this.app.renderer.generateTexture(gfx), anchor: 0.5 });
}
init(config?: Partial<RectActorConfig>) {
this.config = { ...RectActor.defaults, ...(config ?? {}) };
if (this.view) {
this.removeChild(this.view);
}
this.initialize();
}
reset() {
this.active = false;
this.x = 0;
this.y = 0;
}
}
export class RectSolid extends BasicSolid<RectSolidConfig> {
static defaults: RectSolidConfig = {
size: { width: 150, height: 100 },
color: 0x00ff00,
};
type = 'RectSolid';
constructor(config?: Partial<RectSolidConfig>) {
super({ ...RectSolid.defaults, ...(config ?? {}) });
}
initialize() {
const gfx = this.make
.graphics()
.rect(0, 0, this.config.size.width, this.config.size.height)
.fill({ color: 0xffffff });
this.view = this.add.sprite({ asset: this.app.renderer.generateTexture(gfx), anchor: 0.5 });
}
}
export class CircSolid extends BasicSolid<CircSolidConfig> {
static defaults: CircSolidConfig = {
radius: 100,
color: 0x00ff00,
};
type = 'CircSolid';
isCircle = true;
constructor(config?: Partial<CircSolidConfig>) {
super({ ...CircSolid.defaults, ...(config ?? {}) });
}
initialize() {
const gfx = this.make.graphics().circle(0, 0, this.config.radius).fill({ color: 0xffffff });
this.view = this.add.sprite({ asset: this.app.renderer.generateTexture(gfx), anchor: 0.5 });
}
}
export class Projectile extends WithVelocity(CircActor) {
type = 'Projectile';
initialize() {
super.initialize();
this.eventMode = 'none';
this.interactiveChildren = false;
}
update(deltaTime: number) {
super.update(deltaTime);
this.moveByVelocity(deltaTime);
}
}

Adding Physics Objects to Your Scene

Now, you can create your physics objects in your scene:

// import your physics objects, e.g:
import { CircActor, RectActor } from './entities';
// Create a circular actor
const circleActor = this.level.add.existing(
new CircActor({
radius: 50,
}),
);
// Create a rectangular actor
const rectActor = this.level.add.existing(
new RectActor({
width: 100,
height: 100,
}),
);

Projectiles

// import projectile
import { Projectile } from './entities';
class Ball extends Projectile {
type = 'Ball';
// during update, apply gravity to the ball
update(deltaTime: number) {
super.update(deltaTime);
// bounce the ball
if (this.velocity.y < this.system.gravity) {
this.velocity.y += this.velocity.y < 0 ? 1 : 0.5;
this.velocity.y = Math.min(this.system.gravity, this.velocity.y);
}
}
}

Debug Mode

Enable debug visualization to help with physics development:

// in your scene
this.physics.system.debug = true;

Cleanup

Don’t forget to clean up the physics system when destroying your scene:

// in your scene
destroy() {
this.physics.destroy();
super.destroy();
}

Examples

For implementation examples, see the following: