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

+12 -3
View File
@@ -5,6 +5,7 @@ import { type Position, type PropertyBag, sortByZ, type Vec2 } from '../core/mod
import { treeOrder, worldPosition } from '../core/model/hierarchy';
import { findAsset } from '../core/model/Project';
import { cameraFrame, rectAround, rectsIntersect, type WorldRect } from '../core/properties/camera';
import { colliderShape, colliderSize } from '../core/properties/collider';
import { resolveSceneObjectProps } from '../core/properties/resolve';
import type { World } from '../runtime/World';
import { Theme, textStyle, Widget, type Size } from '../ui';
@@ -238,6 +239,7 @@ export class Viewport extends Widget {
private itemSize(item: DrawItem): { w: number; h: number } {
if (item.type === 'sprite') return { w: Number(item.props.width ?? 1), h: Number(item.props.height ?? 1) };
if (item.type === 'behavior') return { w: BEHAVIOR_SYMBOL, h: BEHAVIOR_SYMBOL };
if (item.type === 'collider') return colliderSize(item.props);
return cameraFrame(item.props);
}
@@ -298,6 +300,12 @@ export class Viewport extends Widget {
node.moveTo(-6, -3).lineTo(-1, -3).lineTo(-1, 3).lineTo(6, 3).stroke({ width: 2, color: Theme.colors.accentText });
node.circle(-6, -3, 2).fill(Theme.colors.accentText);
node.circle(6, 3, 2).fill(Theme.colors.accentText);
} else if (item.type === 'collider') {
// Outline plus a faint fill: the fill keeps the shape hit-testable and reads as "area".
const shape = colliderShape(item.props, { x: 0, y: 0 });
if (shape.kind === 'circle') node.circle(0, 0, shape.r);
else node.rect(-shape.w / 2, -shape.h / 2, shape.w, shape.h);
node.fill({ color: Theme.colors.collider, alpha: 0.12 }).stroke({ width: 2 / view.scale, color: Theme.colors.collider });
}
}
this.drawParentLinks(items, editing, view.scale);
@@ -335,14 +343,15 @@ export class Viewport extends Widget {
/**
* Dashed line from every parented symbol (behavior, camera) to its parent (edit mode only).
* Sprites show their nesting in the hierarchy panel instead, to keep the scene uncluttered.
* Sprites show their nesting in the hierarchy panel instead, to keep the scene uncluttered;
* colliders sit on their body anyway, so they get no line either.
*/
private drawParentLinks(items: DrawItem[], editing: boolean, scale: number): void {
this.links.clear();
if (!editing) return;
const byId = new Map(items.map((i) => [i.id, i]));
for (const item of items) {
if (!this.isSymbol(item) || !item.parentId) continue;
if (!this.isSymbol(item) || !item.parentId || item.type === 'collider') continue;
const parent = byId.get(item.parentId);
if (!parent) continue;
const color = item.type === 'behavior' ? Theme.colors.behavior : Theme.colors.textMuted;
@@ -368,7 +377,7 @@ export class Viewport extends Widget {
label.visible = true;
label.text = item.name;
label.scale.set(1 / scale);
if (item.type === 'camera') label.position.set(item.x - size.w / 2, item.y - size.h / 2 - 16 / scale);
if (item.type === 'camera' || item.type === 'collider') label.position.set(item.x - size.w / 2, item.y - size.h / 2 - 16 / scale);
else label.position.set(item.x + size.w / 2 + 4 / scale, item.y - 8 / scale);
}