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>
This commit is contained in:
1 parent
26d5edc496
commit
0a59513b74
21 files changed
+525
-16
No files matched your search
@@ -2,6 +2,7 @@ import { type EntityHandle, GraphInterpreter, type NodeContext, type TraceListen
|
||||
import { findAsset, type Project } from '../../core/model/Project';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { World } from '../World';
|
||||
import { bodyOf, type CollisionPair, type CollisionSystem } from './CollisionSystem';
|
||||
import type { Input } from './Input';
|
||||
import type { System } from './System';
|
||||
|
||||
@@ -28,7 +29,12 @@ export class BehaviorSystem implements System {
|
||||
private instances: Instance[] = [];
|
||||
private world: World | null = null;
|
||||
|
||||
constructor(private readonly project: Project, private readonly input: Input) {}
|
||||
constructor(
|
||||
private readonly project: Project,
|
||||
private readonly input: Input,
|
||||
/** Source of this frame's collision events; null = no collision detection. */
|
||||
private readonly collisions: CollisionSystem | null = null,
|
||||
) {}
|
||||
|
||||
start(world: World): void {
|
||||
this.world = world;
|
||||
@@ -63,11 +69,33 @@ export class BehaviorSystem implements System {
|
||||
i.fire('OnTimer', ctx, {}, (n) => n.id === node.id);
|
||||
}
|
||||
}
|
||||
this.fireCollisions(instance, i, ctx);
|
||||
i.fire('OnUpdate', ctx, { dt });
|
||||
}, dt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collision events for this behavior: every pair in which one collider belongs to the
|
||||
* behavior's body (its owner, or itself when it has none) fires once, with `other` being
|
||||
* the body on the far side.
|
||||
*/
|
||||
private fireCollisions(instance: Instance, interpreter: GraphInterpreter, ctx: NodeContext): void {
|
||||
if (!this.collisions) return;
|
||||
const body = instance.entity.parent ?? instance.entity;
|
||||
const fire = (type: string, pairs: CollisionPair[]) => {
|
||||
for (const { a, b } of pairs) {
|
||||
const own = bodyOf(a) === body ? a : bodyOf(b) === body ? b : null;
|
||||
if (!own) continue;
|
||||
const other = own === a ? b : a;
|
||||
interpreter.fire(type, ctx, { other: bodyOf(other), otherCollider: other, collider: own });
|
||||
}
|
||||
};
|
||||
fire('OnCollisionEnter', this.collisions.enters);
|
||||
fire('OnCollisionStay', this.collisions.stays);
|
||||
fire('OnCollisionExit', this.collisions.exits);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.instances = [];
|
||||
this.world = null;
|
||||
@@ -105,6 +133,11 @@ export class BehaviorSystem implements System {
|
||||
dt,
|
||||
findByName: (name) => world.findByName(name) ?? null,
|
||||
isKeyDown: (key) => this.input.isDown(key),
|
||||
isTouching: (a, b) => {
|
||||
const ea = world.entities.find((e) => e === a || e.id === a.id);
|
||||
const eb = world.entities.find((e) => e === b || e.id === b.id);
|
||||
return Boolean(this.collisions && ea && eb && this.collisions.touching(world, ea, eb));
|
||||
},
|
||||
log: (message) => {
|
||||
world.log.push(message);
|
||||
console.log(`[behavior ${entity.name}]`, message);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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');
|
||||
}
|
||||
Reference in new issue
Block a user