From 62e6f4975d19fdaab14bf7dc420f90f7cb03ff60 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 16 Sep 2024 17:38:12 +0100 Subject: [PATCH] Big refactor to speed up body -> entity detection. --- src/entities/bitmapTerrain.ts | 20 +++--- src/entities/entity.ts | 5 +- src/entities/phys/bazookaShell.ts | 13 ++-- src/entities/phys/grenade.ts | 15 ++--- src/entities/phys/physicsEntity.ts | 13 ++-- src/entities/phys/timedExplosive.ts | 24 +++----- src/entities/phys/worm.ts | 14 ++--- src/entities/water.ts | 6 +- src/game.ts | 54 ++-------------- src/scenarios/borealisTribute.ts | 22 +++---- src/scenarios/grenadeIsland.ts | 22 +++---- src/weapons/weapon.ts | 3 +- src/world.ts | 96 +++++++++++++++++++++++++++++ 13 files changed, 180 insertions(+), 127 deletions(-) create mode 100644 src/world.ts diff --git a/src/entities/bitmapTerrain.ts b/src/entities/bitmapTerrain.ts index fde33ca..6d2158e 100644 --- a/src/entities/bitmapTerrain.ts +++ b/src/entities/bitmapTerrain.ts @@ -1,8 +1,9 @@ -import { Composite, Body, Vector, Bodies, Query, Collision } from "matter-js"; +import { Body, Vector, Bodies, Query, Collision } from "matter-js"; import { UPDATE_PRIORITY, Container, Graphics, Rectangle, Texture, Sprite } from "pixi.js"; import { IMatterEntity } from "./entity"; import { generateQuadTreeFromTerrain, imageDataToTerrainBoundaries } from "../terrain"; import Flags from "../flags"; +import { GameWorld } from "../world"; export type OnDamage = () => void; export class BitmapTerrain implements IMatterEntity { @@ -27,11 +28,11 @@ export class BitmapTerrain implements IMatterEntity { private readonly spriteBackdrop: Sprite; private registeredDamageFunctions = new Map(); - static create(viewWidth: number, viewHeight: number, composite: Composite, texture: Texture) { - return new BitmapTerrain(viewWidth, viewHeight, composite, texture); + static create(viewWidth: number, viewHeight: number, gameWorld: GameWorld, texture: Texture) { + return new BitmapTerrain(viewWidth, viewHeight, gameWorld, texture); } - private constructor(viewWidth: number, viewHeight: number, private readonly composite: Composite, texture: Texture) { + private constructor(viewWidth: number, viewHeight: number, private readonly gameWorld: GameWorld, texture: Texture) { this.canvas = document.createElement('canvas'); this.canvas.width = viewWidth; this.canvas.height = viewHeight; @@ -87,7 +88,7 @@ export class BitmapTerrain implements IMatterEntity { console.log("Removing", removableBodies.length, "bodies"); for (const body of removableBodies) { - Composite.remove(this.composite, body); + this.gameWorld.removeBody(body); const key = body.position.x + "," + body.position.y; const damageFn = this.registeredDamageFunctions.get(key); if (damageFn) { @@ -109,12 +110,12 @@ export class BitmapTerrain implements IMatterEntity { // Now create the pieces const newParts: Body[] = []; for (const quad of quadtreeRects) { - const body = Bodies.rectangle(quad.x + this.sprite.x, quad.y + this.sprite.y, quad.width, quad.height, { isStatic: true }); + const body = Bodies.rectangle(quad.x + this.sprite.x, quad.y + this.sprite.y, quad.width, quad.height, { label: 'terrain', isStatic: true }); newParts.push(body); } this.parts.push(...newParts); - Composite.add(this.composite, newParts); + this.gameWorld.addBody(this, ...newParts); console.timeEnd("Generating terrain"); } @@ -193,11 +194,6 @@ export class BitmapTerrain implements IMatterEntity { this.gfx.rect(this.nearestTerrainPositionPoint.x, this.nearestTerrainPositionPoint.y, 1, 1).stroke({width: 5, color: 0xFF0000}); } - public entityOwnsBody(bodyId: number) { - // TODO: Use a set - return this.parts.some(b => b.id === bodyId); - } - public getNearestTerrainPosition(point: Vector, width: number, maxHeightDiff: number, xDirection = 0): {point?: Vector, fell: boolean} { // This needs a rethink, we really want to have it so that the character's "platform" is visualised // by this algorithm. We want to figure out if we can move left or right, and if not if we're going to fall. diff --git a/src/entities/entity.ts b/src/entities/entity.ts index af220ef..8f0d561 100644 --- a/src/entities/entity.ts +++ b/src/entities/entity.ts @@ -20,7 +20,6 @@ export interface IGameEntity { export interface IMatterEntity extends IGameEntity { // TODO: Wrong shape? explodeHandler?: (point: Vector, radius: number) => void; - entityOwnsBody(bodyId: number): boolean; /** * * @param other @@ -28,4 +27,8 @@ export interface IMatterEntity extends IGameEntity { * @returns True if the collision should stop being processed */ onCollision?(other: IMatterEntity, contactPoint: Vector): boolean; +} + +export interface IMatterPluginInfo { + wormgineEntity: IMatterEntity, } \ No newline at end of file diff --git a/src/entities/phys/bazookaShell.ts b/src/entities/phys/bazookaShell.ts index 1dcd91d..ebcd4d6 100644 --- a/src/entities/phys/bazookaShell.ts +++ b/src/entities/phys/bazookaShell.ts @@ -1,9 +1,10 @@ import { Container, Graphics, Sprite, Texture } from 'pixi.js'; import grenadePaths from "../../assets/bazooka.svg"; -import { Body, Bodies, Composite, Vector } from "matter-js"; +import { Body, Bodies, Vector } from "matter-js"; import { TimedExplosive } from "./timedExplosive"; import { loadSvg } from '../../loadSvg'; import { Game } from '../../game'; +import { GameWorld } from '../../world'; // TODO: This is buggy as all hell. @@ -14,9 +15,9 @@ export class BazookaShell extends TimedExplosive { private readonly force: Vector = Vector.create(0); private readonly gfx = new Graphics(); - static async create(game: Game, parent: Container, composite: Composite, position: {x: number, y: number}, initialAngle: number, initialForce: number, wind: number) { - const ent = new BazookaShell(game, position, await BazookaShell.bodyVertices, initialAngle, composite, initialForce, wind); - Composite.add(composite, ent.body); + static async create(parent: Container, gameWorld: GameWorld, position: {x: number, y: number}, initialAngle: number, initialForce: number, wind: number) { + const ent = new BazookaShell(position, await BazookaShell.bodyVertices, initialAngle, gameWorld, initialForce, wind); + gameWorld.addBody(ent, ent.body); parent.addChild(ent.sprite); parent.addChild(ent.wireframe.renderable); console.log("New zooka", ent.body); @@ -24,12 +25,12 @@ export class BazookaShell extends TimedExplosive { return ent; } - private constructor(game: Game, position: { x: number, y: number }, bodyVerticies: Vector[][], initialAngle: number, composite: Composite, initialForce: number, private readonly wind: number) { + private constructor(position: { x: number, y: number }, bodyVerticies: Vector[][], initialAngle: number, gameWorld: GameWorld, initialForce: number, private readonly wind: number) { const body = Bodies.fromVertices(position.x, position.y, bodyVerticies, { position, }); const sprite = new Sprite(BazookaShell.texture); - super(game, sprite, body, composite, { + super(sprite, body, gameWorld, { explosionRadius: 100, explodeOnContact: true, timerSecs: 30, diff --git a/src/entities/phys/grenade.ts b/src/entities/phys/grenade.ts index 53e9336..1d994f5 100644 --- a/src/entities/phys/grenade.ts +++ b/src/entities/phys/grenade.ts @@ -1,12 +1,12 @@ import { Container, Sprite, Text, Texture, Ticker } from 'pixi.js'; import grenadePaths from "../../assets/grenade.svg"; -import { Bodies, Composite, Vector } from "matter-js"; +import { Bodies, Vector } from "matter-js"; import { TimedExplosive } from "./timedExplosive"; import { IMatterEntity } from '../entity'; import { BitmapTerrain } from '../bitmapTerrain'; import { IMediaInstance, Sound } from '@pixi/sound'; import { loadSvg } from '../../loadSvg'; -import { Game } from '../../game'; +import { GameWorld } from '../../world'; /** * Standard grenade projectile. @@ -20,9 +20,8 @@ export class Grenade extends TimedExplosive { public static bounceSoundsLight: Sound; public static boundSoundHeavy: Sound; - static async create(game: Game, parent: Container, composite: Composite, position: {x: number, y: number}, initialForce: { x: number, y: number}) { - const ent = new Grenade(game, position, await Grenade.bodyVertices, initialForce, composite); - Composite.add(composite, ent.body); + static async create(parent: Container, world: GameWorld, position: {x: number, y: number}, initialForce: { x: number, y: number}) { + const ent = new Grenade(position, await Grenade.bodyVertices, initialForce, world); parent.addChild(ent.sprite, ent.wireframe.renderable); return ent; } @@ -34,7 +33,7 @@ export class Grenade extends TimedExplosive { } public bounceSoundPlayback?: IMediaInstance; - private constructor(game: Game, position: { x: number, y: number }, bodyVerticies: Vector[][], initialForce: { x: number, y: number}, parent: Composite) { + private constructor(position: { x: number, y: number }, bodyVerticies: Vector[][], initialForce: { x: number, y: number}, world: GameWorld) { const body = Bodies.fromVertices(position.x, position.y, bodyVerticies, { sleepThreshold: 60*(5+2), friction: Grenade.FRICTION, @@ -42,11 +41,13 @@ export class Grenade extends TimedExplosive { density: Grenade.DENSITY, isSleeping: false, isStatic: false, + label: "Grenade", }); + console.log("Created grenade body", body.id); const sprite = new Sprite(Grenade.texture); sprite.scale.set(0.5, 0.5); sprite.anchor.set(0.5, 0.5); - super(game, sprite, body, parent, { + super(sprite, body, world, { explosionRadius: 60, explodeOnContact: false, timerSecs: 3, diff --git a/src/entities/phys/physicsEntity.ts b/src/entities/phys/physicsEntity.ts index bc6d073..9bf96ae 100644 --- a/src/entities/phys/physicsEntity.ts +++ b/src/entities/phys/physicsEntity.ts @@ -1,10 +1,11 @@ import { Composite, Body, Vector } from "matter-js"; import { UPDATE_PRIORITY, Sprite } from "pixi.js"; -import { IMatterEntity } from "../entity"; +import { IMatterEntity, IMatterPluginInfo } from "../entity"; import { Water } from "../water"; import { BodyWireframe } from "../../mixins/bodyWireframe."; import globalFlags from "../../flags"; import { IMediaInstance, Sound } from "@pixi/sound"; +import { GameWorld } from "../../world"; /** * Any object that is physically present in the world i.e. a worm. @@ -27,20 +28,20 @@ export abstract class PhysicsEntity implements IMatterEntity { return this.body.id === bodyId || this.body.parent.id === bodyId; } - constructor(public readonly sprite: Sprite, protected body: Body, protected parent: Composite) { + constructor(public readonly sprite: Sprite, protected body: Body, protected gameWorld: GameWorld) { this.wireframe = new BodyWireframe(this.body, globalFlags.DebugView); globalFlags.on('toggleDebugView', (on) => { this.wireframe.enabled = on; - }) + }); + (body.plugin as IMatterPluginInfo).wormgineEntity = this; } destroy(): void { console.log('destroyed'); - if (this.parent) { - Composite.remove(this.parent, this.body); - } this.sprite.destroy(); this.wireframe.renderable.destroy(); + this.gameWorld.removeBody(this.body); + this.gameWorld.removeEntity(this); } update(dt: number): void { diff --git a/src/entities/phys/timedExplosive.ts b/src/entities/phys/timedExplosive.ts index d1f2549..0cddf70 100644 --- a/src/entities/phys/timedExplosive.ts +++ b/src/entities/phys/timedExplosive.ts @@ -1,10 +1,11 @@ import { Composite, Body, Vector, Bodies, Query } from "matter-js"; import { UPDATE_PRIORITY, Ticker, Sprite } from "pixi.js"; import { BitmapTerrain } from "../bitmapTerrain"; -import { IMatterEntity } from "../entity"; +import { IMatterEntity, IMatterPluginInfo } from "../entity"; import { PhysicsEntity } from "./physicsEntity"; import { Game } from "../../game"; import { Explosion } from "../explosion"; +import { GameWorld } from "../../world"; interface Opts { explosionRadius: number, @@ -22,13 +23,14 @@ export abstract class TimedExplosive extends PhysicsEntity implements IMatterEnt priority: UPDATE_PRIORITY = UPDATE_PRIORITY.NORMAL; - constructor(private readonly game: Game, sprite: Sprite, body: Body, parent: Composite, public readonly opts: Opts) { - super(sprite, body, parent) + constructor(sprite: Sprite, body: Body, gameWorld: GameWorld, public readonly opts: Opts) { + super(sprite, body, gameWorld); + this.gameWorld.addBody(this, body); this.timer = Ticker.targetFPMS * opts.timerSecs * 1000; } onTimerFinished() { - if (!this.body || !this.parent) { + if (!this.body || !this.gameWorld) { throw Error('Timer expired without a body'); } this.onExplode(); @@ -38,17 +40,11 @@ export abstract class TimedExplosive extends PhysicsEntity implements IMatterEnt const point = this.body.position; const radius = this.opts.explosionRadius; // Detect if anything is around us. - const circ = Bodies.circle(point.x, point.y, radius); - const hit = Query.collides(circ, this.game.matterEngine.world.bodies).sort((a,b) => b.depth - a.depth); - this.game.addEntity(Explosion.create(this.game.viewport, point, radius, 15, 35)); - console.log("Timed explosive hit", hit); + const hitOtherEntity = this.gameWorld.checkCollision(Bodies.circle(point.x, point.y, radius), this); + this.gameWorld.addEntity(Explosion.create(this.gameWorld.viewport, point, radius, 15, 35)); // Find contact point with any terrain - for (const hitBody of hit) { - const ents = (this.game.findEntityByBodies(hitBody.bodyA, hitBody.bodyB)).filter(e => e !== this); - if (ents[0]) { - // TODO: Cheating massively - this.onCollision(ents[0], hitBody.bodyA.position); - } + if (hitOtherEntity) { + this.onCollision(hitOtherEntity, point); } } diff --git a/src/entities/phys/worm.ts b/src/entities/phys/worm.ts index f565ac7..c0c2cbe 100644 --- a/src/entities/phys/worm.ts +++ b/src/entities/phys/worm.ts @@ -1,11 +1,12 @@ import { Container, Sprite, Texture } from 'pixi.js'; import { Body, Bodies, Composite, Vector, Sleeping } from "matter-js"; -import { IMatterEntity } from '../entity'; +import { IMatterEntity, IMatterPluginInfo } from '../entity'; import { BitmapTerrain } from '../bitmapTerrain'; import { PhysicsEntity } from './physicsEntity'; import { IWeaponDefiniton } from '../../weapons/weapon'; import { WeaponGrenade } from '../../weapons/grenade'; import Controller, { InputKind } from '../../input'; +import { GameWorld } from '../../world'; enum WormState { Idle = 0, @@ -29,9 +30,9 @@ export class Worm extends PhysicsEntity { private terrainPosition: Vector = Vector.create(0,0); private facingAngle = 0; - static async create(parent: Container, composite: Composite, position: { x: number, y: number }, terrain: BitmapTerrain, onFireWeapon: FireWeaponFn) { - const ent = new Worm(position, composite, terrain, onFireWeapon); - Composite.add(composite, ent.body); + static async create(parent: Container, world: GameWorld, position: { x: number, y: number }, terrain: BitmapTerrain, onFireWeapon: FireWeaponFn) { + const ent = new Worm(position, world, terrain, onFireWeapon); + world.addBody(ent, ent.body); parent.addChild(ent.sprite); parent.addChild(ent.wireframe.renderable); return ent; @@ -41,7 +42,7 @@ export class Worm extends PhysicsEntity { return this.body.position; } - private constructor(position: { x: number, y: number }, composite: Composite, + private constructor(position: { x: number, y: number }, world: GameWorld, private readonly terrain: BitmapTerrain, private readonly onFireWeapon: FireWeaponFn, ) { @@ -62,9 +63,8 @@ export class Worm extends PhysicsEntity { // timeScale: 1, // density: 50, }); - super(sprite, body, composite); + super(sprite, body, world); this.state = WormState.InMotion; - this.parent = composite; // TODO: Unbind. Controller.on('inputBegin', (inputKind: InputKind) => { diff --git a/src/entities/water.ts b/src/entities/water.ts index 1923ee5..c3287ca 100644 --- a/src/entities/water.ts +++ b/src/entities/water.ts @@ -3,6 +3,7 @@ import { Container, Filter, Geometry, Mesh, Shader, UPDATE_PRIORITY } from "pixi import { IGameEntity } from "./entity"; import vertex from '../shaders/water.vert?raw'; import fragment from '../shaders/water.frag?raw'; +import { GameWorld } from "../world"; export class Water implements IGameEntity { public readonly priority: UPDATE_PRIORITY = UPDATE_PRIORITY.LOW; @@ -34,7 +35,6 @@ export class Water implements IGameEntity { ] } }); - console.log(indexBuffer); this.geometry = new Geometry({ attributes: { aPosition: [ @@ -67,9 +67,9 @@ export class Water implements IGameEntity { this.waterMesh.scale.set(14, 2); } - async create(parent: Container, engine: Composite) { + async create(parent: Container, world: GameWorld) { parent.addChild(this.waterMesh); - Composite.add(engine, this.body); + world.addBody(this, this.body); } update(): void { diff --git a/src/game.ts b/src/game.ts index 4778020..d8221ca 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,9 +1,9 @@ import "pathseg"; -import { Application, UPDATE_PRIORITY, Text, Ticker } from 'pixi.js'; +import { Application, UPDATE_PRIORITY, Ticker } from 'pixi.js'; import { BazookaShell } from './entities/phys/bazookaShell'; import { Explosion } from './entities/explosion'; import { Grenade } from './entities/phys/grenade'; -import { IGameEntity, IMatterEntity } from './entities/entity'; +import { IGameEntity, IMatterEntity, IMatterPluginInfo } from './entities/entity'; import { QuadtreeDetector } from './quadtreeDetector'; import { Worm } from './entities/phys/worm'; import * as polyDecomp from 'poly-decomp-es'; @@ -11,10 +11,10 @@ import Matter, { Common, Engine, Events, Body } from "matter-js"; import grenadeIsland from './scenarios/grenadeIsland'; import borealisTribute from './scenarios/borealisTribute'; import { Viewport } from 'pixi-viewport'; -import globalFlags from "./flags"; import { PhysicsEntity } from "./entities/phys/physicsEntity"; import { getAssets } from "./assets"; import { GameDebugOverlay } from "./overlay"; +import { GameWorld } from "./world"; Common.setDecomp(polyDecomp); @@ -23,8 +23,8 @@ const worldHeight = 1080; export class Game { public readonly viewport: Viewport; - public readonly matterEngine: Engine; - private readonly entities: IGameEntity[] = []; + private readonly matterEngine: Engine; + public readonly world: GameWorld; private readonly quadtreeDetector: QuadtreeDetector; public get pixiRoot() { @@ -65,6 +65,7 @@ export class Game { // the interaction module is important for wheel to work properly when renderer.view is placed or scaled events: this.pixiApp.renderer.events }); + this.world = new GameWorld(this.matterEngine, this.pixiApp.ticker, this.viewport); this.pixiApp.ticker.maxFPS = 90; this.pixiApp.stage.addChild(this.viewport); this.viewport @@ -105,50 +106,7 @@ export class Game { PhysicsEntity.splashSound = sounds.splash; } - public addEntity(entity: T): T { - this.entities.push(entity); - const tickerFn = (dt: Ticker) => { - entity.update?.(dt.deltaTime); - if (entity.destroyed) { - this.pixiApp.ticker.remove(tickerFn); - this.entities.splice(this.entities.indexOf(entity), 1); - } - }; - this.pixiApp.ticker.add(tickerFn, undefined, entity.priority ? entity.priority : UPDATE_PRIORITY.LOW); - return entity; - } - - public findEntityByBodies(...bodies: Body[]) { - const result: IMatterEntity[] = new Array(bodies.length); - // TODO: o(n^2) function - // TODO: Loose typing - for (const entity of this.entities.filter(e => 'entityOwnsBody' in e) as IMatterEntity[]) { - for (let bIdx = 0; bIdx < bodies.length; bIdx++) { - const body = bodies[bIdx]; - if (entity?.entityOwnsBody(body.id)) { - result[bIdx] = entity as IMatterEntity; - } - } - } - return result; - } - public async run() { - Events.on(this.matterEngine, 'collisionStart', (event) => { - const [pairA] = event.pairs; - const [entA, entB] = this.findEntityByBodies(pairA.bodyA, pairA.bodyB.parent); - - if (!entA || !entB) { - console.warn(`Untracked collision between ${pairA.bodyA.id} (${entA}) and ${pairA.bodyB.id} (${entB})`); - return; - } - - const contact = pairA.contacts[0]; - - entA.onCollision?.(entB, contact.vertex); - entB.onCollision?.(entA, contact.vertex); - }); - // Load this scenario if (this.level === "grenadeIsland") { grenadeIsland(this); diff --git a/src/scenarios/borealisTribute.ts b/src/scenarios/borealisTribute.ts index 6ab67e0..db5dda2 100644 --- a/src/scenarios/borealisTribute.ts +++ b/src/scenarios/borealisTribute.ts @@ -8,35 +8,35 @@ import { Grenade } from "../entities/phys/grenade"; export default async function runScenario(game: Game) { const parent = game.viewport; - const composite = game.matterEngine.world; + const world = game.world; const { worldWidth, worldHeight } = game.viewport; const terrain = BitmapTerrain.create( worldWidth, worldHeight, - game.matterEngine.world, + world, Assets.get('island1') ); - const bg = await game.addEntity(Background.create(game.viewport.screenWidth, game.viewport.screenHeight, game.viewport, [20, 21, 50, 35], terrain)); - await game.addEntity(terrain); + const bg = await world.addEntity(Background.create(game.viewport.screenWidth, game.viewport.screenHeight, game.viewport, [20, 21, 50, 35], terrain)); + await world.addEntity(terrain); bg.addToWorld(game.pixiApp.stage, parent); terrain.addToWorld(parent); - const water = await game.addEntity(new Water(worldWidth,worldHeight)); - water.create(parent, composite); - const worm = game.addEntity(await Worm.create(parent, composite, {x: 900, y: 400} , terrain, async (worm, definition, duration) => { - const newProjectile = await definition.fireFn(game, parent, composite, worm, duration); - game.addEntity(newProjectile); + const water = await world.addEntity(new Water(worldWidth,worldHeight)); + world.addEntity(water); + const worm = world.addEntity(await Worm.create(parent, world, {x: 900, y: 400} , terrain, async (worm, definition, duration) => { + const newProjectile = await definition.fireFn(game, parent, world, worm, duration); + world.addEntity(newProjectile); })); game.viewport.follow(worm.sprite); game.viewport.on('clicked', async (evt) => { const position = { x: evt.world.x, y: evt.world.y }; - const entity = await Grenade.create(game, parent, composite, position, {x: 0.01, y: 0}); + const entity = await Grenade.create(parent, world, position, {x: 0.01, y: 0}); //const entity = await BazookaShell.create(parent, composite, position, 0, 0, 0 /*{x: 0.005, y: -0.01}*/); - game.addEntity(entity); + world.addEntity(entity); }); } \ No newline at end of file diff --git a/src/scenarios/grenadeIsland.ts b/src/scenarios/grenadeIsland.ts index cb3b059..221155e 100644 --- a/src/scenarios/grenadeIsland.ts +++ b/src/scenarios/grenadeIsland.ts @@ -9,35 +9,35 @@ import { BazookaShell } from "../entities/phys/bazookaShell"; export default async function runScenario(game: Game) { const parent = game.viewport; - const composite = game.matterEngine.world; + const world = game.world; const { worldWidth, worldHeight } = game.viewport; const terrain = BitmapTerrain.create( worldWidth, worldHeight, - game.matterEngine.world, + game.world, Assets.get('terrain2') ); - const bg = await game.addEntity(Background.create(game.viewport.screenWidth, game.viewport.screenHeight, game.viewport, [20, 21, 50, 35], terrain)); - await game.addEntity(terrain); + const bg = await world.addEntity(Background.create(game.viewport.screenWidth, game.viewport.screenHeight, game.viewport, [20, 21, 50, 35], terrain)); + await world.addEntity(terrain); bg.addToWorld(game.pixiApp.stage, parent); terrain.addToWorld(parent); - const water = await game.addEntity(new Water(worldWidth,worldHeight)); - water.create(parent, composite); - const worm = game.addEntity(await Worm.create(parent, composite, {x: 900, y: 400} , terrain, async (worm, definition, duration) => { - const newProjectile = await definition.fireFn(game, parent, composite, worm, duration); - game.addEntity(newProjectile); + const water = await world.addEntity(new Water(worldWidth,worldHeight)); + water.create(parent, world); + const worm = world.addEntity(await Worm.create(parent, world, {x: 900, y: 400} , terrain, async (worm, definition, duration) => { + const newProjectile = await definition.fireFn(game, parent, world, worm, duration); + world.addEntity(newProjectile); })); game.viewport.follow(worm.sprite); game.viewport.on('clicked', async (evt) => { const position = { x: evt.world.x, y: evt.world.y }; - const entity = await Grenade.create(game, parent, composite, position, {x: 0.01, y: 0}); + const entity = await Grenade.create(parent, world, position, {x: 0.01, y: 0}); //const entity = await BazookaShell.create(game, parent, composite, position, 1, 0.01, 6); - game.addEntity(entity); + world.addEntity(entity); }); } \ No newline at end of file diff --git a/src/weapons/weapon.ts b/src/weapons/weapon.ts index 04efa11..bf49282 100644 --- a/src/weapons/weapon.ts +++ b/src/weapons/weapon.ts @@ -3,6 +3,7 @@ import { Container } from "pixi.js"; import { Composite } from "matter-js"; import { Worm } from "../entities/phys/worm"; import { Game } from "../game"; +import { GameWorld } from "../world"; export enum IWeaponCode { Grenade, @@ -11,5 +12,5 @@ export enum IWeaponCode { export interface IWeaponDefiniton { code: IWeaponCode, maxDuration: number, - fireFn: (game: Game, parent: Container, composite: Composite, worm: Worm, duration: number) => PromiseLike, + fireFn: (game: Game, parent: Container, world: GameWorld, worm: Worm, duration: number) => PromiseLike, } \ No newline at end of file diff --git a/src/world.ts b/src/world.ts new file mode 100644 index 0000000..6efd2f5 --- /dev/null +++ b/src/world.ts @@ -0,0 +1,96 @@ +import { Composite, Engine, Events, IEventCollision, Body, IEventComposite, Query } from "matter-js"; +import { IGameEntity, IMatterEntity } from "./entities/entity"; +import { Ticker, UPDATE_PRIORITY } from "pixi.js"; +import { Viewport } from "pixi-viewport"; + +/** + * Utility class holding the matterjs composite and entity map. + */ +export class GameWorld { + public readonly bodyEntityMap = new Map(); + public readonly entities = new Set(); + // TODO: Unsure if this is the best location. + + constructor(public readonly matterEngine: Engine, public readonly ticker: Ticker, public readonly viewport: Viewport) { + Events.on(this.matterEngine, 'collisionStart', this.onCollision.bind(this)); + Events.on(this.matterEngine, 'beforeRemove', this.beforeRemove.bind(this)); + } + + private beforeRemove(event: IEventComposite) { + console.log('body being removed', event); + } + + private onCollision(event: IEventCollision) { + const [pairA] = event.pairs; + const [entA, entB] = [ this.bodyEntityMap.get(pairA.bodyA), this.bodyEntityMap.get(pairA.bodyB)]; + + console.log(pairA.bodyB); + + if (!entA || !entB) { + console.warn(`Untracked collision between ${pairA.bodyA.id} ${pairA.bodyA.label} (${entA}) and ${pairA.bodyB.id} ${pairA.bodyB.label} (${entB})`); + return; + } + + const contact = pairA.contacts[0]; + + entA.onCollision?.(entB, contact.vertex); + entB.onCollision?.(entA, contact.vertex); + } + + public addEntity(entity: T): T { + if (this.entities.has(entity)) { + console.warn(`Tried to add entity twice to game world`, entity); + return entity; + } + this.entities.add(entity); + const tickerFn = (dt: Ticker) => { + entity.update?.(dt.deltaTime); + if (entity.destroyed) { + this.ticker.remove(tickerFn); + this.entities.delete(entity); + } + }; + this.ticker.add(tickerFn, undefined, entity.priority ? entity.priority : UPDATE_PRIORITY.LOW); + return entity; + } + + public addBody(entity: T, ...body: Body[]) { + Composite.add(this.matterEngine.world, body); + + console.log("Adding body", entity, body[0].id); + + body.forEach(b => { + if (this.bodyEntityMap.has(b)) { + console.warn(`Tried to add body twice to game world`, b, entity); + return; + } + this.bodyEntityMap.set(b, entity) + if (b.parts.length) { + b.parts.forEach((part) => this.bodyEntityMap.set(part, entity)); + } + }); + } + removeBody(body: Body) { + Composite.remove(this.matterEngine.world, body); + this.bodyEntityMap.delete(body); + body.parts.forEach((part) => this.bodyEntityMap.delete(part)); + } + + removeEntity(entity: IGameEntity) { + this.entities.delete(entity); + } + + public checkCollision(body: Body, ownEntity: IMatterEntity): IMatterEntity|undefined { + const hits = Query.collides(body, this.matterEngine.world.bodies).sort((a,b) => b.depth - a.depth); + for (const hitBody of hits) { + const ents = [ + this.bodyEntityMap.get(hitBody.bodyA), + this.bodyEntityMap.get(hitBody.bodyB.plugin) + ].filter(e => e && e !== ownEntity); + console.log(ents, hitBody.bodyA.position); + // TODO: Cheating massively + return ents[0]; + } + + } +} \ No newline at end of file