- 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>
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
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');
|
||
}
|