Files
feuerwerk/src/runtime/systems/CollisionSystem.ts
T
rootandClaude Fable 5.1 0a59513b74 Collider asset type, CollisionSystem and collision nodes
- Asset type 'collider' (box/circle) placed as child of a body object
- 'Add collider' shortcut (hierarchy context menu + inspector)
- CollisionSystem with enter/stay/exit pairs, sibling colliders excluded
- Nodes: On collision enter/stay/exit, Is touching
- Green outline in edit mode, hidden in play

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:25:04 +02:00

76 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { colliderShape, shapesOverlap } from '../../core/properties/collider';
import type { Entity } from '../Entity';
import type { World } from '../World';
import type { System } from './System';
/** Two collider entities that overlap (or overlapped) – always the same pair order per key. */
export interface CollisionPair {
a: Entity;
b: Entity;
}
/**
* Detects overlaps between collider entities once per tick and sorts them into enter /
* stay / exit lists for the frame. Detection only – nothing is pushed apart. Colliders
* of the same body (same parent) never collide with each other. Runs before the
* BehaviorSystem so graphs see this frame's events.
*/
export class CollisionSystem implements System {
enters: CollisionPair[] = [];
stays: CollisionPair[] = [];
exits: CollisionPair[] = [];
private previous = new Map<string, CollisionPair>();
update(world: World): void {
const colliders = world.entities.filter((e) => e.type === 'collider');
const current = new Map<string, CollisionPair>();
for (let i = 0; i < colliders.length; i++) {
for (let j = i + 1; j < colliders.length; j++) {
const a = colliders[i]!;
const b = colliders[j]!;
if (bodyOf(a) === bodyOf(b)) continue;
if (this.overlapping(world, a, b)) current.set(`${a.id}|${b.id}`, { a, b });
}
}
this.enters = [];
this.stays = [];
this.exits = [];
for (const [key, pair] of current) (this.previous.has(key) ? this.stays : this.enters).push(pair);
for (const [key, pair] of this.previous) if (!current.has(key)) this.exits.push(pair);
this.previous = current;
}
stop(): void {
this.previous = new Map();
this.enters = [];
this.stays = [];
this.exits = [];
}
/** Live test of two collider entities (world positions applied). */
overlapping(world: World, a: Entity, b: Entity): boolean {
return shapesOverlap(colliderShape(a.props, world.worldPosition(a)), colliderShape(b.props, world.worldPosition(b)));
}
/** Whether any collider of `a` overlaps any collider of `b` (see `collidersOf`). */
touching(world: World, a: Entity, b: Entity): boolean {
for (const ca of collidersOf(world, a)) {
for (const cb of collidersOf(world, b)) {
if (ca !== cb && this.overlapping(world, ca, cb)) return true;
}
}
return false;
}
}
/** The object a collider belongs to: its parent, or the collider itself when top-level. */
export function bodyOf(collider: Entity): Entity {
return collider.parent ?? collider;
}
/** A collider stands for itself; any other entity stands for its direct collider children. */
export function collidersOf(world: World, entity: Entity): Entity[] {
if (entity.type === 'collider') return [entity];
return world.childrenOf(entity).filter((e) => e.type === 'collider');
}