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 -1
View File
@@ -6,6 +6,7 @@ import { listScenes } from '../core/model/Project';
import { HttpProjectStore } from '../core/store/HttpProjectStore';
import type { ProjectStore } from '../core/store/ProjectStore';
import { BehaviorSystem } from '../runtime/systems/BehaviorSystem';
import { CollisionSystem } from '../runtime/systems/CollisionSystem';
import { Input } from '../runtime/systems/Input';
import { World } from '../runtime/World';
import { Button, Dialog, HStack, Menu, type MenuItem, Splitter, Theme, ui, VStack, type Widget } from '../ui';
@@ -262,7 +263,10 @@ class Editor {
ui().focus.blur();
this.world = World.instantiate(this.state.project, scene.id);
this.input.attach();
this.behaviors = new BehaviorSystem(this.state.project, this.input);
// Collisions are detected first so behaviors see this frame's enter/stay/exit events.
const collisions = new CollisionSystem();
this.behaviors = new BehaviorSystem(this.state.project, this.input, collisions);
this.world.addSystem(collisions);
this.world.addSystem(this.behaviors);
this.viewport.setWorld(this.world);
this.state.setMode('play');
+1 -1
View File
@@ -7,7 +7,7 @@ import { findAsset, findUsages } from '../../core/model/Project';
import { Button, Dialog, ListView, Menu, type MenuItem, Panel, ScrollContainer } from '../../ui';
import type { EditorState } from '../state';
const TYPE_LABELS: Record<AssetType, string> = { scene: 'Scene', camera: 'Camera', sprite: 'Sprite', behavior: 'Behavior' };
const TYPE_LABELS: Record<AssetType, string> = { scene: 'Scene', camera: 'Camera', sprite: 'Sprite', behavior: 'Behavior', collider: 'Collider' };
/** Lists all assets of the project; creates, places and deletes them. */
export class AssetPanel extends Panel {
+10
View File
@@ -1,3 +1,4 @@
import { AddColliderCommand } from '../../core/commands/colliderCommands';
import { RemoveSceneObjectCommand, SetParentCommand } from '../../core/commands/sceneObjectCommands';
import { treeOrder } from '../../core/model/hierarchy';
import { findAsset } from '../../core/model/Project';
@@ -33,8 +34,17 @@ export class HierarchyPanel extends Panel {
const scene = state.activeScene;
if (!scene) return;
const object = scene.objects.find((o) => o.id === id);
const type = object && findAsset(state.project, object.assetId)?.type;
new Menu([
{ label: 'Select asset', onSelect: () => object && state.select({ kind: 'asset', assetId: object.assetId }) },
{
label: 'Add collider',
disabled: !type || type === 'collider',
onSelect: () => {
const command = new AddColliderCommand(scene.id, id);
if (state.execute(command)) state.select({ kind: 'object', sceneId: scene.id, objectId: command.objectId });
},
},
{ label: 'Unparent (move to top level)', disabled: !object?.parentId, onSelect: () => state.execute(new SetParentCommand(scene.id, id, '')) },
{ label: 'Remove from scene', danger: true, onSelect: () => state.execute(new RemoveSceneObjectCommand(scene.id, id)) },
]).open(x, y);
+15
View File
@@ -1,4 +1,5 @@
import { RenameAssetCommand, ResetAssetPropertyCommand, SetAssetPropertyCommand } from '../../core/commands/assetCommands';
import { AddColliderCommand } from '../../core/commands/colliderCommands';
import { RemoveNodeCommand, SetNodeParamCommand } from '../../core/commands/graphCommands';
import {
ClearOverrideCommand,
@@ -275,6 +276,20 @@ export class Inspector extends Panel {
this.fields.set(def.key, row);
this.body.add(row.row);
}
// Sprites (and other bodies) get their collider with one click: a child sized like the object.
if (asset.type !== 'collider') {
this.body.add(new Label(''));
this.body.add(
new Button('+ Add collider', {
variant: 'ghost',
onClick: () => {
const command = new AddColliderCommand(sceneId, objectId);
if (this.state.execute(command)) this.state.select({ kind: 'object', sceneId, objectId: command.objectId });
},
}),
);
}
}
/**