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

+40
View File
@@ -541,6 +541,46 @@ try {
o2 = JSON.parse(await objectJson('o2'));
check('Unparent moves the object to the top level, keeping its world position', o2.parentId === '' && o2.position.x === 120 && o2.position.y === -80, JSON.stringify(o2));
// 15. Colliders. Right-click the red crate → "Add collider": a collider asset sized like the
// crate (100×60) is placed as its child at the centre, selected and drawn in the overlay.
const redRow3 = await locate('Red crate');
await click(redRow3.x, redRow3.y, 'right');
await sleep(150);
await clickText('Add collider');
await sleep(200);
s = JSON.parse(await stateJson());
const colObj = s.selection?.kind === 'object' ? JSON.parse(await objectJson(s.selection.objectId)) : null;
const colAsset = colObj ? JSON.parse(await evaluate(`JSON.stringify(__gm.state.project.assets.find(a => a.id === '${colObj.assetId}'))`)) : null;
check('Add collider creates a collider child sized like the sprite', colObj?.parentId === 'o1' && colAsset?.type === 'collider' && colAsset.props.width === 100 && colAsset.props.height === 60, JSON.stringify({ colObj, colAsset }));
check('collider is drawn in the overlay layer', colObj && (await evaluate(`__gm.layerOf('${colObj.id}')`)) === 'overlay');
check('collider inspector shows the shape menu', Boolean(await evaluate(`__gm.locate('Box ▾')`)));
await shot('21-collider');
// 15a. Give the blue crate the same collider, wire "On collision enter → Log(other)" into the
// Mover (owner = red crate) and play with the crates overlapping: the log names the blue
// crate exactly once (enter, not stay), and colliders are not rendered in play mode.
await evaluate(`(() => {
const scene = __gm.state.activeScene;
scene.objects.push({ id: 'oc2', assetId: '${colAsset.id}', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: 'o2' });
const mover = __gm.state.project.assets.find(a => a.id === 'mover');
mover.graph.nodes.push({ id: 'n-hit', type: 'OnCollisionEnter', x: 0, y: 600, params: {} }, { id: 'n-hitlog', type: 'Log', x: 340, y: 600, params: {} });
mover.graph.edges.push(
{ id: 'e-hit1', from: { node: 'n-hit', port: 'exec' }, to: { node: 'n-hitlog', port: 'exec' } },
{ id: 'e-hit2', from: { node: 'n-hit', port: 'other' }, to: { node: 'n-hitlog', port: 'message' } });
scene.objects.find(o => o.id === 'o2').position = { x: -100, y: 0, z: 0 }; // 50 from the red crate → 100-wide boxes overlap
__gm.state.select(null);
__gm.state.preview();
})()`);
await keyPress('Enter', 'Enter', { modifiers: 2, windowsVirtualKeyCode: 13 });
await sleep(500);
const collisionWorld = JSON.parse(await evaluate('JSON.stringify(__gm.world())'));
check('On collision enter fired once with the other body', collisionWorld && collisionWorld.log.filter((l) => l === 'Blue crate').length === 1, JSON.stringify(collisionWorld?.log));
const spriteCount = await evaluate(`__gm.state.activeScene.objects.filter(o => __gm.state.project.assets.find(a => a.id === o.assetId)?.type === 'sprite').length`);
info = await evaluate('JSON.stringify(__gm.viewportInfo())');
check('colliders are not rendered in play mode', JSON.parse(info).visibleItems === spriteCount, `${info} sprites=${spriteCount}`);
await keyPress('Escape', 'Escape', { windowsVirtualKeyCode: 27 });
await sleep(200);
check('no exceptions during run', !consoleLog.some((l) => l.startsWith('EXCEPTION')), consoleLog.filter((l) => l.startsWith('EXCEPTION')).slice(0, 3).join(' | '));
ws.close();
} catch (error) {