Files
feuerwerk/src/runtime/systems/CollisionSystem.ts
T

95 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isHandleEnabled } from '../../core/graph';
import { shapesOverlap } from '../../core/properties/collider';
import type { Entity } from '../Entity';
import type { World } from '../World';
import type { Vec2 } from '../../core/model/Properties';
import {
bodyOf,
type Contact,
type CollisionPair,
type CollisionSource,
isCollisionBody,
type MoveResult,
partsOf,
type RayFilter,
type RayResult,
Solids,
} from './solids';
import type { System } from './System';
/**
* Sorts collider overlaps into enter / stay / exit per tick, detection only; colliders of one body never
* collide. Runs before the BehaviorSystem so graphs see this frame's events. This is the editor
* run's collision; a played game has the physics engine instead (`PhysicsSystem`, #146).
*
* A placed tilemap is a solid body of its own: its blocking cells, merged into rectangles, stop a
* move and report a collision with the tilemap as the other side. Two tilemaps never meet – they
* are ground, not things that walk into each other.
*/
export class CollisionSystem implements System, CollisionSource {
enters: CollisionPair[] = [];
stays: CollisionPair[] = [];
exits: CollisionPair[] = [];
private previous = new Map<string, CollisionPair>();
private readonly solids = new Solids();
update(world: World): void {
this.solids.reset();
// Switched-off bodies (or ones below something switched off) take no part at all.
const bodies = world.entities.filter((e) => isCollisionBody(e) && isHandleEnabled(e));
const current = new Map<string, CollisionPair>();
for (let i = 0; i < bodies.length; i++) {
for (let j = i + 1; j < bodies.length; j++) {
const a = bodies[i]!;
const b = bodies[j]!;
if (bodyOf(a) === bodyOf(b)) continue;
if (a.type === 'tilemap' && b.type === 'tilemap') continue;
if (this.overlaps(world, a, b)) current.set(`${a.id}|${b.id}`, { a, b });
}
}
this.enters = [];
this.stays = [];
this.exits = [];
for (const [key, pair] of current) (this.previous.has(key) ? this.stays : this.enters).push(pair);
for (const [key, pair] of this.previous) if (!current.has(key)) this.exits.push(pair);
this.previous = current;
}
stop(): void {
this.previous = new Map();
this.solids.reset();
this.enters = [];
this.stays = [];
this.exits = [];
}
/** Live test of two bodies (world positions applied): any shape of one over any of the other. */
private overlaps(world: World, a: Entity, b: Entity): boolean {
const others = this.solids.shapesOf(world, b);
return this.solids.shapesOf(world, a).some((sa) => others.some((sb) => shapesOverlap(sa, sb)));
}
/** Whether any collider of `a` overlaps any collider of `b` (see `partsOf`). */
touching(world: World, a: Entity, b: Entity): boolean {
for (const ca of partsOf(world, a)) {
for (const cb of partsOf(world, b)) {
if (ca !== cb && this.overlaps(world, ca, cb)) return true;
}
}
return false;
}
moveAndSlide(world: World, entity: Entity, dx: number, dy: number): MoveResult {
return this.solids.moveAndSlide(world, entity, dx, dy);
}
raycast(world: World, from: Vec2, direction: Vec2, length: number, filter: RayFilter): RayResult | null {
return this.solids.raycast(world, from, direction, length, filter);
}
/** Nothing is simulated here, so a contact has no normal or impulse to tell. */
contactOf(): Contact | null {
return null;
}
}