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:
rootandClaude Fable 5.1 committed 2026-09-07 15:25:04 +02:00
1 parent 26d5edc496
commit 0a59513b74
21 files changed
+525 -16

No files matched your search

+34 -1
View File
@@ -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);