Trigger colliders and Move and slide against solid colliders
- Collider property isTrigger (no blocking, still reports enter/stay/exit) - Node 'Move and slide' with blocked x/y outputs, axis-wise resolution - axisSeparation/axisCorrection geometry for box/box, circle/circle, box/circle - Amber outline for trigger colliders in the viewport - Demo mover uses Move and slide; E2E step 15b, 7 new unit tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a59513b74
commit
6ecb20cf02
14 files changed
+283
-12
No files matched your search
@@ -56,6 +56,11 @@ export interface NodeContext {
|
||||
* stands for itself; any other entity stands for its direct collider children.
|
||||
*/
|
||||
isTouching(a: EntityHandle, b: EntityHandle): boolean;
|
||||
/**
|
||||
* Moves `target` by (dx, dy) in local units, stopping its solid colliders at other solid
|
||||
* colliders axis by axis (x first, then y) so it slides along walls. Reports which axes hit.
|
||||
*/
|
||||
moveAndSlide(target: EntityHandle, dx: number, dy: number): { blockedX: boolean; blockedY: boolean };
|
||||
log(message: string): void;
|
||||
destroy(target: EntityHandle): void;
|
||||
stop(): void;
|
||||
|
||||
@@ -307,6 +307,25 @@ registerNode({
|
||||
return { next: 'exec' };
|
||||
},
|
||||
});
|
||||
registerNode({
|
||||
type: 'MoveAndSlide',
|
||||
label: 'Move and slide',
|
||||
category: 'action',
|
||||
description: 'Like Move, but solid colliders of the object stop at other solid colliders and slide along them. Outputs which axes were blocked.',
|
||||
inputs: [exec(), obj('object'), num('dx'), num('dy'), { ...num('scale'), default: 1 }, { ...num('dt'), default: 1 }],
|
||||
outputs: [exec(), bool('blockedX', 'blocked x'), bool('blockedY', 'blocked y')],
|
||||
params: [],
|
||||
run: (ctx, i) => {
|
||||
const target = asEntity(i.object);
|
||||
let blockedX = false;
|
||||
let blockedY = false;
|
||||
if (target) {
|
||||
const factor = asNumber(i.scale) * asNumber(i.dt);
|
||||
({ blockedX, blockedY } = ctx.moveAndSlide(target, asNumber(i.dx) * factor, asNumber(i.dy) * factor));
|
||||
}
|
||||
return { next: 'exec', outputs: { blockedX, blockedY } };
|
||||
},
|
||||
});
|
||||
registerNode({
|
||||
type: 'SetPosition',
|
||||
label: 'Set position',
|
||||
|
||||
@@ -44,6 +44,8 @@ export const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
{ 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 },
|
||||
// Triggers report collisions but never block "Move and slide"; solid colliders do both.
|
||||
{ key: 'isTrigger', label: 'Trigger (no blocking)', kind: 'boolean', default: false },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ export function colliderShape(props: PropertyBag, centre: Vec2): ColliderShape {
|
||||
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)) };
|
||||
}
|
||||
|
||||
/** Whether the collider only reports overlaps (true) or also blocks movement (false). */
|
||||
export function isTrigger(props: PropertyBag): boolean {
|
||||
return Boolean(props.isTrigger ?? defaultsFor('collider').isTrigger);
|
||||
}
|
||||
|
||||
/** 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 });
|
||||
@@ -38,3 +43,45 @@ export function shapesOverlap(a: ColliderShape, b: ColliderShape): boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
export type Axis = 'x' | 'y';
|
||||
|
||||
/**
|
||||
* The centre distance along `axis` at which the two shapes exactly touch, given their
|
||||
* current offset on the other axis. Only meaningful when the shapes overlap on that other
|
||||
* axis (otherwise they cannot collide by moving along `axis`); returns 0 when they do not.
|
||||
*/
|
||||
export function axisSeparation(a: ColliderShape, b: ColliderShape, axis: Axis): number {
|
||||
const other: Axis = axis === 'x' ? 'y' : 'x';
|
||||
const offset = Math.abs(a[other] - b[other]);
|
||||
if (a.kind === 'box' && b.kind === 'box') {
|
||||
return offset * 2 < extent(a, other) + extent(b, other) ? (extent(a, axis) + extent(b, axis)) / 2 : 0;
|
||||
}
|
||||
if (a.kind === 'circle' && b.kind === 'circle') {
|
||||
const r = a.r + b.r;
|
||||
return offset < r ? Math.sqrt(r * r - offset * offset) : 0;
|
||||
}
|
||||
const box = a.kind === 'box' ? a : (b as Extract<ColliderShape, { kind: 'box' }>);
|
||||
const circle = a.kind === 'circle' ? a : (b as Extract<ColliderShape, { kind: 'circle' }>);
|
||||
const halfOther = extent(box, other) / 2;
|
||||
const halfAxis = extent(box, axis) / 2;
|
||||
// Circle centre within the box's span on the other axis: the flat side is hit.
|
||||
if (offset <= halfOther) return halfAxis + circle.r;
|
||||
// Otherwise the corner is the closest point: a circle-vs-point solution.
|
||||
const corner = offset - halfOther;
|
||||
return corner < circle.r ? halfAxis + Math.sqrt(circle.r * circle.r - corner * corner) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* How far `mover` has to be pushed back against its travel direction `dir` (±1) along
|
||||
* `axis` so it just touches `obstacle` (0 when they do not overlap). Only valid when
|
||||
* `mover` approached from the far side, i.e. it did not overlap before its move.
|
||||
*/
|
||||
export function axisCorrection(mover: ColliderShape, obstacle: ColliderShape, axis: Axis, dir: 1 | -1): number {
|
||||
if (!shapesOverlap(mover, obstacle)) return 0;
|
||||
return Math.max(0, dir * (mover[axis] - obstacle[axis]) + axisSeparation(mover, obstacle, axis));
|
||||
}
|
||||
|
||||
function extent(box: Extract<ColliderShape, { kind: 'box' }>, axis: Axis): number {
|
||||
return axis === 'x' ? box.w : box.h;
|
||||
}
|
||||
Reference in new issue
Block a user