One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
947a6b1977
commit
6a31f5e0ad
33 files changed
+2064
-125
No files matched your search
@@ -18,7 +18,7 @@ import {
|
||||
} from '../../core/graph';
|
||||
import { stepKeyRepeat } from '../../core/graph/keyRepeat';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import { type PropertyBag, textOf } from '../../core/model/Properties';
|
||||
import { type PropertyBag, textOf, type Vec2 } from '../../core/model/Properties';
|
||||
import { findAssetOf, findScene, type Project } from '../../core/model/Project';
|
||||
import { soundFileOf } from '../../core/model/sound';
|
||||
import { parentChain } from '../../core/model/walk';
|
||||
@@ -30,7 +30,7 @@ import type { World } from '../World';
|
||||
import type { Rapier } from '../physics/engine';
|
||||
import { PhysicsSystem } from '../physics/PhysicsSystem';
|
||||
import { CollisionSystem } from './CollisionSystem';
|
||||
import { bodyOf, type CollisionPair, type CollisionSource, NOT_BLOCKED } from './solids';
|
||||
import { bodyOf, type CollisionPair, type CollisionSource, type Contact, NOT_BLOCKED } from './solids';
|
||||
import { AnimationSystem } from './AnimationSystem';
|
||||
import type { Input } from './Input';
|
||||
import type { System } from './System';
|
||||
@@ -68,6 +68,13 @@ const NO_DYNAMIC_BODY =
|
||||
/** How often state events may cause the next move before the run is called endless. */
|
||||
const MAX_STATE_ROUNDS = 100;
|
||||
|
||||
/** A collision pair seen from one behavior's body: its own collider and the one across. */
|
||||
interface Side {
|
||||
pair: CollisionPair;
|
||||
own: Entity;
|
||||
other: Entity;
|
||||
}
|
||||
|
||||
interface Instance {
|
||||
entity: Entity;
|
||||
interpreter: GraphInterpreter;
|
||||
@@ -387,7 +394,7 @@ export class BehaviorSystem implements System {
|
||||
i.fire('OnTimer', ctx, {}, (n) => n.id === node.id);
|
||||
}
|
||||
}
|
||||
this.fireCollisions(instance, i, ctx);
|
||||
this.fireCollisions(instance, i, ctx, dt);
|
||||
this.fireAnimationsFinished(instance, i, ctx);
|
||||
// With an owner the click has to land on it; a behavior without one has no shape to
|
||||
// be hit, so it listens to the whole game view (anywhere inside the camera frame).
|
||||
@@ -433,31 +440,65 @@ export class BehaviorSystem implements System {
|
||||
|
||||
/**
|
||||
* Collision events for this behavior: every pair in which one collider belongs to the
|
||||
* behavior's body (its owner, or itself when it has none) fires once, with `other` being
|
||||
* the body on the far side.
|
||||
* behavior's body (its owner, or itself when it has none) fires, with `other` being the body on
|
||||
* the far side – enter and exit once, stay every frame or, with an interval, once each time that
|
||||
* long has passed since the pair met (#149). Each stay node keeps a clock per pair in its node
|
||||
* state, and a pair that is gone takes its clock with it.
|
||||
*/
|
||||
private fireCollisions(instance: Instance, interpreter: GraphInterpreter, ctx: NodeContext): void {
|
||||
if (!this.collisions) return;
|
||||
private fireCollisions(
|
||||
instance: Instance,
|
||||
interpreter: GraphInterpreter,
|
||||
ctx: NodeContext,
|
||||
dt: number,
|
||||
): void {
|
||||
const collisions = this.collisions;
|
||||
if (!collisions) return;
|
||||
const body = instance.entity.parent ?? instance.entity;
|
||||
const fire = (type: string, pairs: CollisionPair[], contact = false) => {
|
||||
for (const { a, b, normal, impulse } of pairs) {
|
||||
const own = bodyOf(a) === body ? a : bodyOf(b) === body ? b : null;
|
||||
if (!own) continue;
|
||||
const other = own === a ? b : a;
|
||||
const outputs: Record<string, Value> = { other: bodyOf(other), otherCollider: other, collider: own };
|
||||
if (contact) {
|
||||
// The pair's normal points towards `a`; seen from `b` it points the other way.
|
||||
const sign = own === a ? 1 : -1;
|
||||
outputs.normalX = sign * (normal?.x ?? 0) || 0;
|
||||
outputs.normalY = sign * (normal?.y ?? 0) || 0;
|
||||
outputs.impulse = impulse ?? 0;
|
||||
}
|
||||
interpreter.fire(type, ctx, outputs);
|
||||
const mine = (pairs: CollisionPair[]) =>
|
||||
pairs.flatMap((pair) => {
|
||||
const own = bodyOf(pair.a) === body ? pair.a : bodyOf(pair.b) === body ? pair.b : null;
|
||||
return own ? [{ pair, own, other: own === pair.a ? pair.b : pair.a }] : [];
|
||||
});
|
||||
const outputsOf = ({ own, other, pair }: Side, contact?: Partial<Contact> | null) => {
|
||||
const outputs: Record<string, Value> = { other: bodyOf(other), otherCollider: other, collider: own };
|
||||
if (contact !== undefined) {
|
||||
// The pair's normal points towards `a`; seen from `b` it points the other way.
|
||||
const sign = own === pair.a ? 1 : -1;
|
||||
outputs.normalX = sign * (contact?.normal?.x ?? 0) || 0;
|
||||
outputs.normalY = sign * (contact?.normal?.y ?? 0) || 0;
|
||||
outputs.impulse = contact?.impulse ?? 0;
|
||||
outputs.pointX = contact?.point?.x ?? 0;
|
||||
outputs.pointY = contact?.point?.y ?? 0;
|
||||
}
|
||||
return outputs;
|
||||
};
|
||||
fire('OnCollisionEnter', this.collisions.enters, true);
|
||||
fire('OnCollisionStay', this.collisions.stays);
|
||||
fire('OnCollisionExit', this.collisions.exits);
|
||||
for (const side of mine(collisions.enters))
|
||||
interpreter.fire('OnCollisionEnter', ctx, outputsOf(side, side.pair));
|
||||
const stays = mine(collisions.stays);
|
||||
const contacts = new Map<CollisionPair, Contact | null>();
|
||||
const contactOf = (pair: CollisionPair) => {
|
||||
if (!contacts.has(pair)) contacts.set(pair, collisions.contactOf(pair));
|
||||
return contacts.get(pair)!;
|
||||
};
|
||||
for (const node of interpreter.eventNodes('OnCollisionStay')) {
|
||||
const interval = Math.max(0, Number(node.params.interval ?? 0)) || 0;
|
||||
const clocks = interpreter.nodeState.get(node.id) ?? {};
|
||||
const running: Record<string, number> = {};
|
||||
for (const side of stays) {
|
||||
let fires = 1;
|
||||
if (interval > 0) {
|
||||
const key = `${side.pair.a.id}|${side.pair.b.id}`;
|
||||
let elapsed = (clocks[key] ?? 0) + dt;
|
||||
for (fires = 0; elapsed >= interval - 1e-9; fires++) elapsed -= interval;
|
||||
running[key] = elapsed;
|
||||
}
|
||||
for (let i = 0; i < fires; i++)
|
||||
interpreter.fire('OnCollisionStay', ctx, outputsOf(side, contactOf(side.pair)), (n) => n === node);
|
||||
}
|
||||
if (interval > 0) interpreter.nodeState.set(node.id, running);
|
||||
else interpreter.nodeState.delete(node.id);
|
||||
}
|
||||
for (const side of mine(collisions.exits)) interpreter.fire('OnCollisionExit', ctx, outputsOf(side));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,6 +610,18 @@ export class BehaviorSystem implements System {
|
||||
found.y += dy;
|
||||
return NOT_BLOCKED;
|
||||
},
|
||||
raycast: (from, direction, length, { ignore, triggers }) => {
|
||||
const hit = this.collisions?.raycast(world, from, direction, length, {
|
||||
ignore: resolve(ignore) ?? null,
|
||||
triggers,
|
||||
});
|
||||
world.noteRay(from, hit?.point ?? pointAlong(from, direction, length), Boolean(hit));
|
||||
return hit ? { ...hit, object: bodyOf(hit.collider) } : null;
|
||||
},
|
||||
ignoreOneWay: (target, seconds) => {
|
||||
const found = resolve(target);
|
||||
if (found) world.ignoreOneWay(found, seconds);
|
||||
},
|
||||
physics:
|
||||
this.collisions instanceof PhysicsSystem
|
||||
? physicsControl(world, this.collisions, resolve)
|
||||
@@ -608,6 +661,13 @@ export class BehaviorSystem implements System {
|
||||
}
|
||||
}
|
||||
|
||||
/** Where a ray of that length ends; where it has no direction, where it starts. */
|
||||
function pointAlong(from: Vec2, direction: Vec2, length: number): Vec2 {
|
||||
const d = Math.hypot(direction.x, direction.y);
|
||||
if (!(d > 0) || !(length > 0)) return from;
|
||||
return { x: from.x + (direction.x / d) * length, y: from.y + (direction.y / d) * length };
|
||||
}
|
||||
|
||||
/** The physics nodes' way into the played game's engine, with the handles of the graph resolved. */
|
||||
function physicsControl(
|
||||
world: World,
|
||||
|
||||
Reference in new issue
Block a user