Move with collision reacts along the contact normal: it travels to the surface, stays flush and slides the rest of the move along it with Coulomb friction (the mean of both colliders, a tile counts as 0.5), a circle now rolls over an edge instead of catching on it, pushed dynamic bodies move off along the normal, Move gains the outputs hit, normal x and normal y, an object without its own collider moves freely, and editor run and play share the same path through Solids; contact point and normal come from our own f64 geometry (contactBetween), because Rapier's castShape lands 3e-6 px inside the wall in f32 (#150)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d931e66f92
commit
1b5e386b60
15 files changed
+680
-161
No files matched your search
@@ -97,6 +97,17 @@ export interface PhysicsControl {
|
||||
setGravity(x: number, y: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a move with collision met: which of the object's own axes something stood against, whether
|
||||
* it hit anything, and the world normal of the first contact (pointing away from what was hit).
|
||||
*/
|
||||
export interface MoveOutcome {
|
||||
blockedX: boolean;
|
||||
blockedY: boolean;
|
||||
hit: boolean;
|
||||
normal: { x: number; y: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything the graph does to the world goes through here, so the core stays free of runtime
|
||||
* imports. Implemented by the runtime (BehaviorSystem) and by test doubles.
|
||||
@@ -132,10 +143,10 @@ export interface NodeContext {
|
||||
/** A collider entity stands for itself; any other entity for its direct collider children. */
|
||||
isTouching(a: EntityHandle, b: EntityHandle): boolean;
|
||||
/**
|
||||
* Moves `target` by (dx, dy) in local units, stopping solid colliders at other solid ones axis by
|
||||
* axis (x first, then y) so it slides along walls.
|
||||
* Moves `target` by (dx, dy) in local units; its solid colliders stop flush at other solid ones
|
||||
* and the rest of the move goes on along the surface, less what friction takes.
|
||||
*/
|
||||
moveAndSlide(target: EntityHandle, dx: number, dy: number): { blockedX: boolean; blockedY: boolean };
|
||||
moveAndSlide(target: EntityHandle, dx: number, dy: number): MoveOutcome;
|
||||
/** Absent where nothing is simulated: the editor run, and a test double that does not ask for it. */
|
||||
physics?: PhysicsControl;
|
||||
log(message: string): void;
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import type { MoveOutcome } from '../../NodeContext';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asBoolean, asEntity, asNumber } from '../../values';
|
||||
import { bool, exec, num, obj, withDefault } from '../shared';
|
||||
|
||||
const FREE = { blockedX: false, blockedY: false, hit: false, normalX: 0, normalY: 0 };
|
||||
|
||||
function outputsOf(outcome: MoveOutcome) {
|
||||
return {
|
||||
blockedX: outcome.blockedX,
|
||||
blockedY: outcome.blockedY,
|
||||
hit: outcome.hit,
|
||||
normalX: outcome.normal.x,
|
||||
normalY: outcome.normal.y,
|
||||
};
|
||||
}
|
||||
|
||||
export const Move: NodeDef = {
|
||||
type: 'Move',
|
||||
label: 'Move',
|
||||
category: 'action',
|
||||
description:
|
||||
"Moves an object by (dx, dy) × scale × dt. Connect dt to an event's dt for per-second movement; " +
|
||||
'leave it at 1 for per-frame. With "Collide with solids" on, the object\'s solid colliders stop at ' +
|
||||
'other solid colliders and slide along them; blocked x/y tell which axes hit.',
|
||||
'leave it at 1 for per-frame. With "Collide with solids" on, the object\'s solid colliders stop ' +
|
||||
'flush at other solid colliders and the rest of the move goes on along the surface, less what the ' +
|
||||
'friction of both colliders takes; blocked x/y tell which axes something stood against, hit whether ' +
|
||||
'anything was met, and normal x/y which way the first surface faces.',
|
||||
inputs: [
|
||||
exec(),
|
||||
obj('object'),
|
||||
@@ -18,17 +33,25 @@ export const Move: NodeDef = {
|
||||
withDefault(num('scale'), 1),
|
||||
withDefault(num('dt'), 1),
|
||||
],
|
||||
outputs: [exec(), bool('blockedX', 'blocked x'), bool('blockedY', 'blocked y')],
|
||||
outputs: [
|
||||
exec(),
|
||||
bool('blockedX', 'blocked x'),
|
||||
bool('blockedY', 'blocked y'),
|
||||
bool('hit'),
|
||||
num('normalX', 'normal x'),
|
||||
num('normalY', 'normal y'),
|
||||
],
|
||||
params: [{ key: 'collide', label: 'Collide with solids', kind: 'boolean', default: false }],
|
||||
run: (ctx, inputs, params) => {
|
||||
const target = asEntity(inputs.object);
|
||||
if (!target) return { next: 'exec', outputs: { blockedX: false, blockedY: false } };
|
||||
if (!target) return { next: 'exec', outputs: FREE };
|
||||
const factor = asNumber(inputs.scale) * asNumber(inputs.dt);
|
||||
const dx = asNumber(inputs.dx) * factor;
|
||||
const dy = asNumber(inputs.dy) * factor;
|
||||
if (asBoolean(params.collide)) return { next: 'exec', outputs: ctx.moveAndSlide(target, dx, dy) };
|
||||
if (asBoolean(params.collide))
|
||||
return { next: 'exec', outputs: outputsOf(ctx.moveAndSlide(target, dx, dy)) };
|
||||
target.x += dx;
|
||||
target.y += dy;
|
||||
return { next: 'exec', outputs: { blockedX: false, blockedY: false } };
|
||||
return { next: 'exec', outputs: FREE };
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user