Physics stage 1a runs on Rapier: a new Rigidbody child object (body type, mass, gravity scale, damping, fixed rotation, CCD) makes its parent fall, bounce and come to rest in play, colliders gain friction, bounciness and density, the scene gains gravity (0, 980), the new PhysicsSystem steps at a fixed 60 Hz with interpolation in world coordinates and treats a tilemap as static ground, On collision enter gains normal and impulse, play waits for Rapier and does not start without it, and the editor run keeps the old CollisionSystem; the existing collision tests run against both systems unchanged (#146)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d02bcca266
commit
78899b1ace
34 files changed
+2291
-676
No files matched your search
@@ -26,7 +26,10 @@ import { loopsOf, volumeOf } from '../../core/properties/sounds';
|
||||
import { type AudioSink, SILENT_AUDIO } from './Audio';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { World } from '../World';
|
||||
import { bodyOf, type CollisionPair, CollisionSystem } from './CollisionSystem';
|
||||
import type { Rapier } from '../physics/engine';
|
||||
import { PhysicsSystem } from '../physics/PhysicsSystem';
|
||||
import { CollisionSystem } from './CollisionSystem';
|
||||
import { bodyOf, type CollisionPair, type CollisionSource } from './solids';
|
||||
import { AnimationSystem } from './AnimationSystem';
|
||||
import type { Input } from './Input';
|
||||
import type { System } from './System';
|
||||
@@ -78,17 +81,26 @@ interface Instance {
|
||||
*/
|
||||
const ERROR_PREFIX = 'Error in behavior';
|
||||
|
||||
export interface GameSystemOptions {
|
||||
shouldRun?: (entity: Entity) => boolean;
|
||||
audio?: AudioSink;
|
||||
/** What a scene swap hands over from the world before it (see `BehaviorSystem.carryOver`). */
|
||||
carried?: Readonly<SharedState> | null;
|
||||
/**
|
||||
* The loaded engine: a played game simulates its rigidbodies on it (#146). Without one the
|
||||
* collisions are detected only, which is what the editor run still does.
|
||||
*/
|
||||
physics?: Rapier | null;
|
||||
}
|
||||
|
||||
/** Collisions and animations are ticked first, so behaviors see this frame's events. */
|
||||
export function addGameSystems(
|
||||
world: World,
|
||||
project: Project,
|
||||
input: Input,
|
||||
shouldRun?: (entity: Entity) => boolean,
|
||||
audio: AudioSink = SILENT_AUDIO,
|
||||
/** What a scene swap hands over from the world before it (see `BehaviorSystem.carryOver`). */
|
||||
carried: Readonly<SharedState> | null = null,
|
||||
{ shouldRun, audio = SILENT_AUDIO, carried = null, physics = null }: GameSystemOptions = {},
|
||||
): BehaviorSystem {
|
||||
const collisions = new CollisionSystem();
|
||||
const collisions = physics ? new PhysicsSystem(physics) : new CollisionSystem();
|
||||
const animations = new AnimationSystem();
|
||||
const behaviors = new BehaviorSystem(project, input, { collisions, animations, audio }, shouldRun);
|
||||
if (carried) behaviors.carryOver(carried);
|
||||
@@ -100,7 +112,7 @@ export function addGameSystems(
|
||||
|
||||
/** The systems whose per-frame results become events; a missing one means no such events. */
|
||||
export interface EventSources {
|
||||
collisions?: CollisionSystem | null;
|
||||
collisions?: (CollisionSource & System) | null;
|
||||
animations?: AnimationSystem | null;
|
||||
/** Where `Play sound` ends up. Absent is a run that plays silently, which is what a test wants. */
|
||||
audio?: AudioSink | null;
|
||||
@@ -121,7 +133,7 @@ export class BehaviorSystem implements System {
|
||||
private pending: StateChange[] = [];
|
||||
/** The declarations as last read; an edit is what differs from these (see `syncDeclarations`). */
|
||||
private declared: Record<string, PropertyBag> = {};
|
||||
private readonly collisions: CollisionSystem | null;
|
||||
private readonly collisions: CollisionSource | null;
|
||||
private readonly animations: AnimationSystem | null;
|
||||
private readonly audio: AudioSink;
|
||||
|
||||
@@ -423,15 +435,23 @@ export class BehaviorSystem implements System {
|
||||
private fireCollisions(instance: Instance, interpreter: GraphInterpreter, ctx: NodeContext): void {
|
||||
if (!this.collisions) return;
|
||||
const body = instance.entity.parent ?? instance.entity;
|
||||
const fire = (type: string, pairs: CollisionPair[]) => {
|
||||
for (const { a, b } of pairs) {
|
||||
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;
|
||||
interpreter.fire(type, ctx, { other: bodyOf(other), otherCollider: other, collider: own });
|
||||
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);
|
||||
}
|
||||
};
|
||||
fire('OnCollisionEnter', this.collisions.enters);
|
||||
fire('OnCollisionEnter', this.collisions.enters, true);
|
||||
fire('OnCollisionStay', this.collisions.stays);
|
||||
fire('OnCollisionExit', this.collisions.exits);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user