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

+6
View File
@@ -39,6 +39,12 @@ export const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
],
// Behaviors declare their own properties (variables) per asset – see `schemaForAsset`.
behavior: [],
collider: [
{ key: 'shape', label: 'Shape', kind: 'enum', default: 'box', options: [{ value: 'box', label: 'Box' }, { value: 'circle', label: 'Circle' }] },
{ key: 'width', label: 'Width', kind: 'number', default: 100, min: 1, step: 1 },
{ key: 'height', label: 'Height', kind: 'number', default: 100, min: 1, step: 1 },
{ key: 'radius', label: 'Radius', kind: 'number', default: 50, min: 1, step: 1 },
],
};
/** Properties every scene object has regardless of its asset type. */
+40
View File
@@ -0,0 +1,40 @@
import type { PropertyBag, Vec2 } from '../model/Properties';
import { defaultsFor } from './PropertySchema';
/**
* Collider geometry in world units. Pure functions shared by the editor (drawing, gizmos)
* and the runtime (CollisionSystem), so both agree on what "overlaps" means.
*/
export type ColliderShape =
| { kind: 'box'; x: number; y: number; w: number; h: number }
| { kind: 'circle'; x: number; y: number; r: number };
/** The shape described by collider properties, centred on `centre` (a world position). */
export function colliderShape(props: PropertyBag, centre: Vec2): ColliderShape {
const d = defaultsFor('collider');
if (props.shape === 'circle') return { kind: 'circle', x: centre.x, y: centre.y, r: Math.max(0, Number(props.radius ?? d.radius)) };
return { kind: 'box', x: centre.x, y: centre.y, w: Math.max(0, Number(props.width ?? d.width)), h: Math.max(0, Number(props.height ?? d.height)) };
}
/** Footprint of a collider shape (used for culling and the selection gizmo). */
export function colliderSize(props: PropertyBag): { w: number; h: number } {
const shape = colliderShape(props, { x: 0, y: 0 });
return shape.kind === 'circle' ? { w: shape.r * 2, h: shape.r * 2 } : { w: shape.w, h: shape.h };
}
/** Whether two shapes overlap. Shapes that merely touch along an edge do not overlap. */
export function shapesOverlap(a: ColliderShape, b: ColliderShape): boolean {
if (a.kind === 'box' && b.kind === 'box') {
return Math.abs(a.x - b.x) * 2 < a.w + b.w && Math.abs(a.y - b.y) * 2 < a.h + b.h;
}
if (a.kind === 'circle' && b.kind === 'circle') {
const r = a.r + b.r;
return (a.x - b.x) ** 2 + (a.y - b.y) ** 2 < r * r;
}
const box = a.kind === 'box' ? a : (b as Extract<ColliderShape, { kind: 'box' }>);
const circle = a.kind === 'circle' ? a : (b as Extract<ColliderShape, { kind: 'circle' }>);
// Closest point of the box to the circle centre, then a plain distance check.
const cx = Math.max(box.x - box.w / 2, Math.min(circle.x, box.x + box.w / 2));
const cy = Math.max(box.y - box.h / 2, Math.min(circle.y, box.y + box.h / 2));
return (circle.x - cx) ** 2 + (circle.y - cy) ** 2 < circle.r * circle.r;
}