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

+5
View File
@@ -51,6 +51,11 @@ export interface NodeContext {
dt: number;
findByName(name: string): EntityHandle | null;
isKeyDown(key: string): boolean;
/**
* Whether any collider of `a` overlaps any collider of `b` right now. A collider entity
* stands for itself; any other entity stands for its direct collider children.
*/
isTouching(a: EntityHandle, b: EntityHandle): boolean;
log(message: string): void;
destroy(target: EntityHandle): void;
stop(): void;
+20
View File
@@ -32,6 +32,12 @@ registerNode({
outputs: [exec()],
params: [{ key: 'interval', label: 'Interval (s)', kind: 'number', default: 1, min: 0.01, step: 0.1 }],
});
// Collision events reach the behavior whose owner is the collider's body (the collider's
// parent, or the collider itself when it is top-level). `other` is the other body.
const collisionOutputs = (): PortDef[] => [exec(), obj('other'), obj('otherCollider', 'other collider'), obj('collider')];
registerNode({ type: 'OnCollisionEnter', label: 'On collision enter', category: 'event', description: 'Runs once when a collider of the owner starts overlapping another collider.', inputs: [], outputs: collisionOutputs(), params: [] });
registerNode({ type: 'OnCollisionStay', label: 'On collision stay', category: 'event', description: 'Runs every frame while a collider of the owner overlaps another collider.', inputs: [], outputs: collisionOutputs(), params: [] });
registerNode({ type: 'OnCollisionExit', label: 'On collision exit', category: 'event', description: 'Runs once when a collider of the owner stops overlapping another collider.', inputs: [], outputs: collisionOutputs(), params: [] });
// --- flow ---------------------------------------------------------------------------------
@@ -266,6 +272,20 @@ registerNode({ type: 'And', label: 'And', category: 'data', description: 'Both t
registerNode({ type: 'Or', label: 'Or', category: 'data', description: 'Either true.', inputs: [bool('a'), bool('b')], outputs: [bool('result')], params: [], run: (_c, i) => ({ outputs: { result: asBoolean(i.a) || asBoolean(i.b) } }) });
registerNode({ type: 'Not', label: 'Not', category: 'data', description: 'Inverts a boolean.', inputs: [bool('value')], outputs: [bool('result')], params: [], run: (_c, i) => ({ outputs: { result: !asBoolean(i.value) } }) });
registerNode({ type: 'KeyIsDown', label: 'Key is down', category: 'data', description: 'Whether a key is currently held.', inputs: [], outputs: [bool('down')], params: [keyParam], run: (ctx, _i, p) => ({ outputs: { down: ctx.isKeyDown(String(p.key ?? '')) } }) });
registerNode({
type: 'IsTouching',
label: 'Is touching',
category: 'data',
description: 'Whether the colliders of two objects overlap right now (an object stands for its collider children; a collider for itself).',
inputs: [obj('a'), obj('b')],
outputs: [bool('result')],
params: [],
run: (ctx, i) => {
const a = asEntity(i.a);
const b = asEntity(i.b);
return { outputs: { result: Boolean(a && b && ctx.isTouching(a, b)) } };
},
});
// --- actions --------------------------------------------------------------------------------