diff --git a/client/src/main.ts b/client/src/main.ts
index 5332616..aa7431f 100644
--- a/client/src/main.ts
+++ b/client/src/main.ts
@@ -1,7 +1,7 @@
-import App from "./App.svelte";
+import App from './App.svelte';
const app = new App({
- target: document.getElementById("app"),
+ target: document.getElementById('app')
});
export default app;
diff --git a/client/src/routes/Home.svelte b/client/src/routes/Home.svelte
index 9ada10e..71ad324 100644
--- a/client/src/routes/Home.svelte
+++ b/client/src/routes/Home.svelte
@@ -3,10 +3,9 @@
import LeaderBoard from "../components/LeaderBoard.svelte";
import { Miscellaneous } from "@engine/config";
-
+
let width: number = Miscellaneous.WIDTH;
let height: number = Miscellaneous.HEIGHT;
-
diff --git a/client/svelte.config.js b/client/svelte.config.js
index b0683fd..db735be 100644
--- a/client/svelte.config.js
+++ b/client/svelte.config.js
@@ -1,7 +1,7 @@
-import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
- preprocess: vitePreprocess(),
-}
+ preprocess: vitePreprocess()
+};
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 781d1b3..fadebb0 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": "@tsconfig/svelte/tsconfig.json",
+ "extends": ["@tsconfig/svelte/tsconfig.json", "../tsconfig.engine.json"],
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
@@ -24,8 +24,5 @@
"src/**/*.js",
"src/**/*.svelte"
],
- "paths": {
- "@engine/*": ["../engine/*"]
- },
"references": [{ "path": "./tsconfig.node.json" }]
}
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 0307338..6f0e1d0 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -1,13 +1,24 @@
-import { defineConfig } from "vite";
-import { svelte } from "@sveltejs/vite-plugin-svelte";
-import { fileURLToPath, URL } from "node:url";
+import { defineConfig } from 'vite';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import { fileURLToPath, URL } from 'node:url';
// https://vitejs.dev/config/
export default defineConfig({
+ server: {
+ host: '0.0.0.0',
+ proxy: {
+ '/api': {
+ target: 'http://10.0.0.237:8080',
+ ws: true,
+ rewrite: (path) => path.replace(/^\/api/, '')
+ }
+ }
+ },
+ cors: true,
plugins: [svelte()],
resolve: {
alias: {
- "@engine": fileURLToPath(new URL("../engine", import.meta.url)),
- },
- },
+ '@engine': fileURLToPath(new URL('../engine', import.meta.url))
+ }
+ }
});
diff --git a/engine/Game.ts b/engine/Game.ts
index 07d06e8..cdd3507 100644
--- a/engine/Game.ts
+++ b/engine/Game.ts
@@ -1,5 +1,5 @@
-import { Entity } from "./entities";
-import { System } from "./systems";
+import { Entity } from './entities';
+import { System } from './systems';
export class Game {
private systemOrder: string[];
@@ -7,9 +7,9 @@ export class Game {
private running: boolean;
private lastTimeStamp: number;
- public entities: Map
;
+ public entities: Map;
public systems: Map;
- public componentEntities: Map>;
+ public componentEntities: Map>;
constructor() {
this.lastTimeStamp = performance.now();
@@ -29,17 +29,17 @@ export class Game {
this.entities.set(entity.id, entity);
}
- public getEntity(id: number): Entity | undefined {
+ public getEntity(id: string): Entity | undefined {
return this.entities.get(id);
}
- public removeEntity(id: number) {
+ public removeEntity(id: string) {
this.entities.delete(id);
}
public forEachEntityWithComponent(
componentName: string,
- callback: (entity: Entity) => void,
+ callback: (entity: Entity) => void
) {
this.componentEntities.get(componentName)?.forEach((entityId) => {
const entity = this.getEntity(entityId);
@@ -60,7 +60,7 @@ export class Game {
return this.systems.get(name);
}
- public doGameLoop = (timeStamp: number) => {
+ public doGameLoop(timeStamp: number) {
if (!this.running) {
return;
}
@@ -75,16 +75,16 @@ export class Game {
if (!this.componentEntities.has(component.name)) {
this.componentEntities.set(
component.name,
- new Set([entity.id]),
+ new Set([entity.id])
);
return;
}
this.componentEntities.get(component.name)?.add(entity.id);
- }),
+ })
);
this.systemOrder.forEach((systemName) => {
this.systems.get(systemName)?.update(dt, this);
});
- };
+ }
}
diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts
index 5e21b2f..921feb9 100644
--- a/engine/components/BoundingBox.ts
+++ b/engine/components/BoundingBox.ts
@@ -1,6 +1,6 @@
-import { Component, ComponentNames } from ".";
-import type { Coord2D, Dimension2D } from "../interfaces";
-import { dotProduct, rotateVector } from "../utils";
+import { Component, ComponentNames } from '.';
+import type { Coord2D, Dimension2D } from '../interfaces';
+import { dotProduct, rotateVector } from '../utils';
export class BoundingBox extends Component {
public center: Coord2D;
@@ -15,8 +15,27 @@ export class BoundingBox extends Component {
this.rotation = rotation ?? 0;
}
- // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
public isCollidingWith(box: BoundingBox): boolean {
+ if (this.rotation == 0 && box.rotation == 0) {
+ const thisTopLeft = this.getTopLeft();
+ const thisBottomRight = this.getBottomRight();
+
+ const thatTopLeft = box.getTopLeft();
+ const thatBottomRight = box.getBottomRight();
+
+ if (
+ thisBottomRight.x <= thatTopLeft.x ||
+ thisTopLeft.x >= thatBottomRight.x ||
+ thisBottomRight.y <= thatTopLeft.y ||
+ thisTopLeft.y >= thatBottomRight.y
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
const boxes = [this.getVertices(), box.getVertices()];
for (const poly of boxes) {
for (let i = 0; i < poly.length; i++) {
@@ -29,8 +48,8 @@ export class BoundingBox extends Component {
const projection = dotProduct(normal, vertex);
return [Math.min(min, projection), Math.max(max, projection)];
},
- [Infinity, -Infinity],
- ),
+ [Infinity, -Infinity]
+ )
);
if (maxThis < minBox || maxBox < minThis) return false;
@@ -45,20 +64,22 @@ export class BoundingBox extends Component {
{ x: -this.dimension.width / 2, y: -this.dimension.height / 2 },
{ x: -this.dimension.width / 2, y: this.dimension.height / 2 },
{ x: this.dimension.width / 2, y: this.dimension.height / 2 },
- { x: this.dimension.width / 2, y: -this.dimension.height / 2 },
+ { x: this.dimension.width / 2, y: -this.dimension.height / 2 }
]
- .map((vertex) => rotateVector(vertex, this.rotation))
+ .map((vertex) => rotateVector(vertex, this.rotation)) // rotate
.map((vertex) => {
+ // translate
return {
x: vertex.x + this.center.x,
- y: vertex.y + this.center.y,
+ y: vertex.y + this.center.y
};
});
}
- public getRotationInPiOfUnitCircle() {
+ public getRotationInPiOfUnitCircle(): number {
let rads = this.rotation * (Math.PI / 180);
if (rads >= Math.PI) {
+ // Physics system guarantees rotation \in [0, 360)
rads -= Math.PI;
}
return rads;
@@ -68,17 +89,33 @@ export class BoundingBox extends Component {
let rads = this.getRotationInPiOfUnitCircle();
const { width, height } = this.dimension;
+ if (rads == 0) return this.dimension;
+
if (rads <= Math.PI / 2) {
return {
width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)),
- height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads)),
+ height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads))
};
}
rads -= Math.PI / 2;
return {
width: Math.abs(height * Math.cos(rads) + width * Math.sin(rads)),
- height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads)),
+ height: Math.abs(width * Math.cos(rads) + height * Math.sin(rads))
+ };
+ }
+
+ public getTopLeft(): Coord2D {
+ return {
+ x: this.center.x - this.dimension.width / 2,
+ y: this.center.y - this.dimension.height / 2
+ };
+ }
+
+ public getBottomRight(): Coord2D {
+ return {
+ x: this.center.x + this.dimension.width / 2,
+ y: this.center.y + this.dimension.height / 2
};
}
}
diff --git a/engine/components/Collide.ts b/engine/components/Collide.ts
index 889ecf8..ed72b92 100644
--- a/engine/components/Collide.ts
+++ b/engine/components/Collide.ts
@@ -1,4 +1,4 @@
-import { Component, ComponentNames } from ".";
+import { Component, ComponentNames } from '.';
export class Collide extends Component {
constructor() {
diff --git a/engine/components/Control.ts b/engine/components/Control.ts
index 1e782ee..d3987d7 100644
--- a/engine/components/Control.ts
+++ b/engine/components/Control.ts
@@ -1,11 +1,18 @@
-import { Component, ComponentNames, Velocity } from ".";
+import { Component, ComponentNames, Velocity } from '.';
export class Control extends Component {
- public controlVelocity: Velocity;
+ public controlVelocityComponent: Velocity;
+ public controllableBy: string;
+ public isControllable: boolean; // computed each update in the input system
- constructor(controlVelocity: Velocity = new Velocity()) {
+ constructor(
+ controllableBy: string,
+ controlVelocityComponent: Velocity = new Velocity()
+ ) {
super(ComponentNames.Control);
- this.controlVelocity = controlVelocity;
+ this.controllableBy = controllableBy;
+ this.controlVelocityComponent = controlVelocityComponent;
+ this.isControllable = false;
}
}
diff --git a/engine/components/FacingDirection.ts b/engine/components/FacingDirection.ts
index 1c701a3..8c2a9d2 100644
--- a/engine/components/FacingDirection.ts
+++ b/engine/components/FacingDirection.ts
@@ -1,4 +1,4 @@
-import { Component, ComponentNames, Sprite } from ".";
+import { Component, ComponentNames, Sprite } from '.';
export class FacingDirection extends Component {
public readonly facingLeftSprite: Sprite;
diff --git a/engine/components/Forces.ts b/engine/components/Forces.ts
index 91ae1c1..e397985 100644
--- a/engine/components/Forces.ts
+++ b/engine/components/Forces.ts
@@ -1,6 +1,6 @@
-import type { Force2D } from "../interfaces";
-import { Component } from "./Component";
-import { ComponentNames } from ".";
+import type { Force2D } from '../interfaces';
+import { Component } from './Component';
+import { ComponentNames } from '.';
/**
* A list of forces and torque, (in newtons, and newton-meters respectively)
diff --git a/engine/components/Gravity.ts b/engine/components/Gravity.ts
index 89fcb67..dd6dd2e 100644
--- a/engine/components/Gravity.ts
+++ b/engine/components/Gravity.ts
@@ -1,7 +1,7 @@
-import { ComponentNames, Component } from ".";
+import { ComponentNames, Component } from '.';
export class Gravity extends Component {
- private static DEFAULT_TERMINAL_VELOCITY = 5;
+ private static DEFAULT_TERMINAL_VELOCITY = 4.5;
public terminalVelocity: number;
diff --git a/engine/components/Jump.ts b/engine/components/Jump.ts
index 0b40767..6cbfb08 100644
--- a/engine/components/Jump.ts
+++ b/engine/components/Jump.ts
@@ -1,4 +1,4 @@
-import { Component, ComponentNames } from ".";
+import { Component, ComponentNames } from '.';
export class Jump extends Component {
public canJump: boolean;
diff --git a/engine/components/Mass.ts b/engine/components/Mass.ts
index daa2d71..a7f98fd 100644
--- a/engine/components/Mass.ts
+++ b/engine/components/Mass.ts
@@ -1,4 +1,4 @@
-import { Component, ComponentNames } from ".";
+import { Component, ComponentNames } from '.';
export class Mass extends Component {
public mass: number;
diff --git a/engine/components/Moment.ts b/engine/components/Moment.ts
index 3d0dd2f..cd76294 100644
--- a/engine/components/Moment.ts
+++ b/engine/components/Moment.ts
@@ -1,4 +1,4 @@
-import { Component, ComponentNames } from ".";
+import { Component, ComponentNames } from '.';
export class Moment extends Component {
public inertia: number;
diff --git a/engine/components/NetworkUpdateable.ts b/engine/components/NetworkUpdateable.ts
new file mode 100644
index 0000000..014270c
--- /dev/null
+++ b/engine/components/NetworkUpdateable.ts
@@ -0,0 +1,7 @@
+import { Component, ComponentNames } from '.';
+
+export class NetworkUpdateable extends Component {
+ constructor() {
+ super(ComponentNames.NetworkUpdateable);
+ }
+}
diff --git a/engine/components/Sprite.ts b/engine/components/Sprite.ts
index bdb4982..36b944e 100644
--- a/engine/components/Sprite.ts
+++ b/engine/components/Sprite.ts
@@ -1,5 +1,5 @@
-import { Component, ComponentNames } from ".";
-import type { Dimension2D, DrawArgs, Coord2D } from "../interfaces";
+import { Component, ComponentNames } from '.';
+import type { Dimension2D, DrawArgs, Coord2D } from '../interfaces';
export class Sprite extends Component {
private sheet: HTMLImageElement;
@@ -17,7 +17,7 @@ export class Sprite extends Component {
spriteImgPos: Coord2D,
spriteImgDimensions: Dimension2D,
msPerFrame: number,
- numFrames: number,
+ numFrames: number
) {
super(ComponentNames.Sprite);
@@ -56,12 +56,12 @@ export class Sprite extends Component {
ctx.drawImage(
this.sheet,
...this.getSpriteArgs(),
- ...this.getDrawArgs(drawArgs),
+ ...this.getDrawArgs(drawArgs)
);
if (tint) {
ctx.globalAlpha = 0.5;
- ctx.globalCompositeOperation = "source-atop";
+ ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = tint;
ctx.fillRect(...this.getDrawArgs(drawArgs));
}
@@ -74,19 +74,23 @@ export class Sprite extends Component {
this.spriteImgPos.x + this.currentFrame * this.spriteImgDimensions.width,
this.spriteImgPos.y,
this.spriteImgDimensions.width,
- this.spriteImgDimensions.height,
+ this.spriteImgDimensions.height
];
}
private getDrawArgs({
center,
- dimension,
+ dimension
}: DrawArgs): [dx: number, dy: number, dw: number, dh: number] {
return [
center.x - dimension.width / 2,
center.y - dimension.height / 2,
dimension.width,
- dimension.height,
+ dimension.height
];
}
+
+ public getSpriteDimensions() {
+ return this.spriteImgDimensions;
+ }
}
diff --git a/engine/components/TopCollidable.ts b/engine/components/TopCollidable.ts
index 7fb147d..05ce484 100644
--- a/engine/components/TopCollidable.ts
+++ b/engine/components/TopCollidable.ts
@@ -1,4 +1,4 @@
-import { Component, ComponentNames } from ".";
+import { Component, ComponentNames } from '.';
export class TopCollidable extends Component {
constructor() {
diff --git a/engine/components/Velocity.ts b/engine/components/Velocity.ts
index 068d8cd..0071891 100644
--- a/engine/components/Velocity.ts
+++ b/engine/components/Velocity.ts
@@ -1,23 +1,23 @@
-import type { Velocity2D } from "../interfaces";
-import { Component } from "./Component";
-import { ComponentNames } from ".";
+import type { Velocity2D } from '../interfaces';
+import { Component } from './Component';
+import { ComponentNames } from '.';
export class Velocity extends Component {
- public dCartesian: Velocity2D;
- public dTheta: number;
+ public velocity: Velocity2D;
- constructor(dCartesian: Velocity2D = { dx: 0, dy: 0 }, dTheta: number = 0) {
+ constructor(
+ velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }
+ ) {
super(ComponentNames.Velocity);
- this.dCartesian = dCartesian;
- this.dTheta = dTheta;
+ this.velocity = velocity;
}
- public add(velocity?: Velocity) {
+ public add(velocity?: Velocity2D) {
if (velocity) {
- this.dCartesian.dx += velocity.dCartesian.dx;
- this.dCartesian.dy += velocity.dCartesian.dy;
- this.dTheta += velocity.dTheta;
+ this.velocity.dCartesian.dx += velocity.dCartesian.dx;
+ this.velocity.dCartesian.dy += velocity.dCartesian.dy;
+ this.velocity.dTheta += velocity.dTheta;
}
}
}
diff --git a/engine/components/WallBounded.ts b/engine/components/WallBounded.ts
index 5f787e1..c1745a8 100644
--- a/engine/components/WallBounded.ts
+++ b/engine/components/WallBounded.ts
@@ -1,4 +1,4 @@
-import { Component, ComponentNames } from ".";
+import { Component, ComponentNames } from '.';
export class WallBounded extends Component {
constructor() {
diff --git a/engine/components/index.ts b/engine/components/index.ts
index 67f1259..6d7c1e5 100644
--- a/engine/components/index.ts
+++ b/engine/components/index.ts
@@ -1,15 +1,16 @@
-export * from "./Component";
-export * from "./BoundingBox";
-export * from "./Velocity";
-export * from "./Forces";
-export * from "./Sprite";
-export * from "./FacingDirection";
-export * from "./Jump";
-export * from "./TopCollidable";
-export * from "./Collide";
-export * from "./Control";
-export * from "./WallBounded";
-export * from "./Gravity";
-export * from "./Mass";
-export * from "./Moment";
-export * from "./names";
+export * from './Component';
+export * from './BoundingBox';
+export * from './Velocity';
+export * from './Forces';
+export * from './Sprite';
+export * from './FacingDirection';
+export * from './Jump';
+export * from './TopCollidable';
+export * from './Collide';
+export * from './Control';
+export * from './WallBounded';
+export * from './Gravity';
+export * from './Mass';
+export * from './Moment';
+export * from './NetworkUpdateable';
+export * from './names';
diff --git a/engine/components/names.ts b/engine/components/names.ts
index e2ee3d3..97b4edd 100644
--- a/engine/components/names.ts
+++ b/engine/components/names.ts
@@ -1,15 +1,16 @@
export namespace ComponentNames {
- export const Sprite = "Sprite";
- export const BoundingBox = "BoundingBox";
- export const Velocity = "Velocity";
- export const FacingDirection = "FacingDirection";
- export const Control = "Control";
- export const Jump = "Jump";
- export const TopCollidable = "TopCollidable";
- export const Collide = "Collide";
- export const WallBounded = "WallBounded";
- export const Gravity = "Gravity";
- export const Forces = "Forces";
- export const Mass = "Mass";
- export const Moment = "Moment";
+ export const Sprite = 'Sprite';
+ export const BoundingBox = 'BoundingBox';
+ export const Velocity = 'Velocity';
+ export const FacingDirection = 'FacingDirection';
+ export const Control = 'Control';
+ export const Jump = 'Jump';
+ export const TopCollidable = 'TopCollidable';
+ export const Collide = 'Collide';
+ export const WallBounded = 'WallBounded';
+ export const Gravity = 'Gravity';
+ export const Forces = 'Forces';
+ export const Mass = 'Mass';
+ export const Moment = 'Moment';
+ export const NetworkUpdateable = 'NetworkUpdateable';
}
diff --git a/engine/config/assets.ts b/engine/config/assets.ts
index 173bab3..289f181 100644
--- a/engine/config/assets.ts
+++ b/engine/config/assets.ts
@@ -1,10 +1,10 @@
-import type { SpriteSpec } from "./sprites";
-import { SPRITE_SPECS } from "./sprites";
+import type { SpriteSpec } from './sprites';
+import { SPRITE_SPECS } from './sprites';
export const IMAGES = new Map();
export const loadSpritesIntoImageElements = (
- spriteSpecs: Partial[],
+ spriteSpecs: Partial[]
): Promise[] => {
const spritePromises: Promise[] = [];
@@ -17,13 +17,13 @@ export const loadSpritesIntoImageElements = (
spritePromises.push(
new Promise((resolve) => {
img.onload = () => resolve();
- }),
+ })
);
}
if (spriteSpec.states) {
spritePromises.push(
- ...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values())),
+ ...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values()))
);
}
}
@@ -35,8 +35,8 @@ export const loadAssets = () =>
Promise.all([
...loadSpritesIntoImageElements(
Array.from(SPRITE_SPECS.keys()).map(
- (key) => SPRITE_SPECS.get(key) as SpriteSpec,
- ),
- ),
+ (key) => SPRITE_SPECS.get(key) as SpriteSpec
+ )
+ )
// TODO: Sound
]);
diff --git a/engine/config/constants.ts b/engine/config/constants.ts
index 3d536d3..dc98ad0 100644
--- a/engine/config/constants.ts
+++ b/engine/config/constants.ts
@@ -1,34 +1,39 @@
-import { Action } from "../interfaces";
+import { Action } from '../interfaces';
export namespace KeyConstants {
export const KeyActions: Record = {
a: Action.MOVE_LEFT,
- ArrowLeft: Action.MOVE_LEFT,
+ arrowleft: Action.MOVE_LEFT,
+
d: Action.MOVE_RIGHT,
- ArrowRight: Action.MOVE_RIGHT,
+ arrowright: Action.MOVE_RIGHT,
+
w: Action.JUMP,
- ArrowUp: Action.JUMP,
+ arrowup: Action.JUMP,
+
+ ' ': Action.JUMP
};
+ // value -> [key] from KeyActions
export const ActionKeys: Map = Object.keys(
- KeyActions,
+ KeyActions
).reduce((acc: Map, key) => {
- const action = KeyActions[key];
+ const action = KeyActions[key.toLowerCase()];
if (acc.has(action)) {
- acc.get(action)?.push(key);
+ acc.get(action)!.push(key);
return acc;
}
acc.set(action, [key]);
return acc;
- }, new Map());
+ }, new Map());
}
export namespace PhysicsConstants {
export const MAX_JUMP_TIME_MS = 150;
export const GRAVITY = 0.0075;
- export const PLAYER_MOVE_VEL = 1;
+ export const PLAYER_MOVE_VEL = 0.8;
export const PLAYER_JUMP_ACC = -0.008;
export const PLAYER_JUMP_INITIAL_VEL = -1;
}
@@ -36,4 +41,7 @@ export namespace PhysicsConstants {
export namespace Miscellaneous {
export const WIDTH = 600;
export const HEIGHT = 800;
+
+ export const DEFAULT_GRID_WIDTH = 30;
+ export const DEFAULT_GRID_HEIGHT = 30;
}
diff --git a/engine/config/index.ts b/engine/config/index.ts
index 7a1052a..03b2246 100644
--- a/engine/config/index.ts
+++ b/engine/config/index.ts
@@ -1,3 +1,3 @@
-export * from "./constants";
-export * from "./assets.ts";
-export * from "./sprites.ts";
+export * from './constants';
+export * from './assets.ts';
+export * from './sprites.ts';
diff --git a/engine/config/sprites.ts b/engine/config/sprites.ts
index 1f65c18..e5fcd31 100644
--- a/engine/config/sprites.ts
+++ b/engine/config/sprites.ts
@@ -1,7 +1,7 @@
export enum Sprites {
FLOOR,
TRAMPOLINE,
- COFFEE,
+ COFFEE
}
export interface SpriteSpec {
@@ -22,12 +22,12 @@ const floorSpriteSpec = {
height: 40,
frames: 3,
msPerFrame: 125,
- states: new Map>(),
+ states: new Map>()
};
[40, 80, 120, 160].forEach((width) => {
floorSpriteSpec.states.set(width, {
width,
- sheet: `/assets/floor_tile_${width}.png`,
+ sheet: `/assets/floor_tile_${width}.png`
});
});
SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec);
@@ -37,12 +37,12 @@ const coffeeSpriteSpec = {
width: 60,
height: 45,
frames: 3,
- states: new Map>(),
+ states: new Map>()
};
-coffeeSpriteSpec.states.set("LEFT", {
- sheet: "/assets/coffee_left.png",
+coffeeSpriteSpec.states.set('LEFT', {
+ sheet: '/assets/coffee_left.png'
});
-coffeeSpriteSpec.states.set("RIGHT", {
- sheet: "/assets/coffee_right.png",
+coffeeSpriteSpec.states.set('RIGHT', {
+ sheet: '/assets/coffee_right.png'
});
SPRITE_SPECS.set(Sprites.COFFEE, coffeeSpriteSpec);
diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts
index ca8d314..63fb370 100644
--- a/engine/entities/Entity.ts
+++ b/engine/entities/Entity.ts
@@ -1,13 +1,17 @@
-import type { Component } from "../components";
+import { EntityNames, Floor, Player } from '.';
+import { type Component } from '../components';
+
+const randomId = () =>
+ (performance.now() + Math.random() * 10_000_000).toString();
export abstract class Entity {
- private static ID = 0;
+ public id: string;
+ public components: Map;
+ public name: string;
- public readonly id: number;
- public readonly components: Map;
-
- constructor() {
- this.id = Entity.ID++;
+ constructor(name: string, id: string = randomId()) {
+ this.name = name;
+ this.id = id;
this.components = new Map();
}
@@ -17,7 +21,7 @@ export abstract class Entity {
public getComponent(name: string): T {
if (!this.hasComponent(name)) {
- throw new Error("Entity does not have component " + name);
+ throw new Error('Entity does not have component ' + name);
}
return this.components.get(name) as T;
}
@@ -29,4 +33,30 @@ export abstract class Entity {
public hasComponent(name: string): boolean {
return this.components.has(name);
}
+
+ public static from(entityName: string, id: string, args: any): Entity {
+ let entity: Entity;
+
+ switch (entityName) {
+ case EntityNames.Player:
+ const player = new Player();
+ player.setFrom(args);
+ entity = player;
+ break;
+ case EntityNames.Floor:
+ const floor = new Floor(args.floorWidth);
+ floor.setFrom(args);
+ entity = floor;
+ break;
+ default:
+ throw new Error('.from() Entity type not implemented: ' + entityName);
+ }
+
+ entity.id = id;
+ return entity;
+ }
+
+ public abstract setFrom(args: Record): void;
+
+ public abstract serialize(): Record;
}
diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts
index 44587e6..b4f48e5 100644
--- a/engine/entities/Floor.ts
+++ b/engine/entities/Floor.ts
@@ -1,15 +1,19 @@
-import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
-import { BoundingBox, Sprite } from "../components";
-import { TopCollidable } from "../components/TopCollidable";
-import { Entity } from "../entities";
+import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config';
+import { BoundingBox, ComponentNames, Sprite } from '../components';
+import { TopCollidable } from '../components/TopCollidable';
+import { Entity, EntityNames } from '../entities';
export class Floor extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
- Sprites.FLOOR,
+ Sprites.FLOOR
) as SpriteSpec;
+ private width: number;
+
constructor(width: number) {
- super();
+ super(EntityNames.Floor);
+
+ this.width = width;
this.addComponent(
new Sprite(
@@ -17,17 +21,28 @@ export class Floor extends Entity {
{ x: 0, y: 0 },
{ width, height: Floor.spriteSpec.height },
Floor.spriteSpec.msPerFrame,
- Floor.spriteSpec.frames,
- ),
- );
-
- this.addComponent(
- new BoundingBox(
- { x: 300, y: 300 },
- { width, height: Floor.spriteSpec.height },
- ),
+ Floor.spriteSpec.frames
+ )
);
this.addComponent(new TopCollidable());
}
+
+ public serialize() {
+ return {
+ floorWidth: this.width,
+ boundingBox: this.getComponent(ComponentNames.BoundingBox)
+ };
+ }
+
+ public setFrom(args: any) {
+ const { boundingBox } = args;
+ this.addComponent(
+ new BoundingBox(
+ boundingBox.center,
+ boundingBox.dimension,
+ boundingBox.rotation
+ )
+ );
+ }
}
diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts
index 45d7500..4d91c6f 100644
--- a/engine/entities/Player.ts
+++ b/engine/entities/Player.ts
@@ -1,5 +1,5 @@
-import { Entity } from ".";
-import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
+import { Entity, EntityNames } from '.';
+import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config';
import {
Jump,
FacingDirection,
@@ -10,32 +10,38 @@ import {
WallBounded,
Forces,
Collide,
- Control,
Mass,
Moment,
-} from "../components";
-import { Direction } from "../interfaces";
+ ComponentNames,
+ Control
+} from '../components';
+import { Direction } from '../interfaces';
export class Player extends Entity {
private static MASS: number = 10;
- private static MOI: number = 1000;
+ private static MOI: number = 100;
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
- Sprites.COFFEE,
+ Sprites.COFFEE
) as SpriteSpec;
constructor() {
- super();
+ super(EntityNames.Player);
this.addComponent(
new BoundingBox(
- { x: 300, y: 100 },
+ {
+ x: 0,
+ y: 0
+ },
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
- 0,
- ),
+ 0
+ )
);
- this.addComponent(new Velocity({ dx: 0, dy: 0 }, 0));
+ this.addComponent(
+ new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 })
+ );
this.addComponent(new Mass(Player.MASS));
this.addComponent(new Moment(Player.MOI));
@@ -43,7 +49,6 @@ export class Player extends Entity {
this.addComponent(new Gravity());
this.addComponent(new Jump());
- this.addComponent(new Control());
this.addComponent(new Collide());
this.addComponent(new WallBounded());
@@ -59,11 +64,41 @@ export class Player extends Entity {
{ x: 0, y: 0 },
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
Player.spriteSpec.msPerFrame,
- Player.spriteSpec.frames,
- ),
+ Player.spriteSpec.frames
+ )
);
this.addComponent(new FacingDirection(leftSprite, rightSprite));
- this.addComponent(leftSprite); // face Left by default
+ this.addComponent(leftSprite); // face left by default
+ }
+
+ public serialize(): Record {
+ return {
+ control: this.getComponent(ComponentNames.Control),
+ boundingBox: this.getComponent(ComponentNames.BoundingBox),
+ velocity: this.getComponent(ComponentNames.Velocity),
+ forces: this.getComponent(ComponentNames.Forces)
+ };
+ }
+
+ public setFrom(args: Record) {
+ const { control, velocity, forces, boundingBox } = args;
+
+ let center = boundingBox.center;
+
+ const myCenter = this.getComponent(
+ ComponentNames.BoundingBox
+ ).center;
+ const distance = Math.sqrt(
+ Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2)
+ );
+ if (distance < 30) center = myCenter;
+
+ [
+ Object.assign(new Control(control.controllableBy), control),
+ new Velocity(velocity.velocity),
+ new Forces(forces.forces),
+ new BoundingBox(center, boundingBox.dimension, boundingBox.rotation)
+ ].forEach((component) => this.addComponent(component));
}
}
diff --git a/engine/entities/index.ts b/engine/entities/index.ts
index a921512..8aee83c 100644
--- a/engine/entities/index.ts
+++ b/engine/entities/index.ts
@@ -1,3 +1,4 @@
-export * from "./Entity";
-export * from "./Floor";
-export * from "./Player";
+export * from './Entity';
+export * from './Floor';
+export * from './Player';
+export * from './names';
diff --git a/engine/entities/names.ts b/engine/entities/names.ts
new file mode 100644
index 0000000..cf65f9f
--- /dev/null
+++ b/engine/entities/names.ts
@@ -0,0 +1,4 @@
+export namespace EntityNames {
+ export const Player = 'Player';
+ export const Floor = 'Floor';
+}
diff --git a/engine/interfaces/Action.ts b/engine/interfaces/Action.ts
index 61c89e1..f0e6a66 100644
--- a/engine/interfaces/Action.ts
+++ b/engine/interfaces/Action.ts
@@ -1,5 +1,5 @@
export enum Action {
MOVE_LEFT,
MOVE_RIGHT,
- JUMP,
+ JUMP
}
diff --git a/engine/interfaces/Direction.ts b/engine/interfaces/Direction.ts
index 0bc6ef3..af1c7ac 100644
--- a/engine/interfaces/Direction.ts
+++ b/engine/interfaces/Direction.ts
@@ -1,6 +1,6 @@
export enum Direction {
- UP = "UP",
- DOWN = "DOWN",
- LEFT = "LEFT",
- RIGHT = "RIGHT",
+ UP = 'UP',
+ DOWN = 'DOWN',
+ LEFT = 'LEFT',
+ RIGHT = 'RIGHT'
}
diff --git a/engine/interfaces/Draw.ts b/engine/interfaces/Draw.ts
index 6561a01..8479fe4 100644
--- a/engine/interfaces/Draw.ts
+++ b/engine/interfaces/Draw.ts
@@ -1,4 +1,4 @@
-import type { Coord2D, Dimension2D } from "./";
+import type { Coord2D, Dimension2D } from './';
export interface DrawArgs {
center: Coord2D;
diff --git a/engine/interfaces/Vec2.ts b/engine/interfaces/Vec2.ts
index b2bae37..04be4be 100644
--- a/engine/interfaces/Vec2.ts
+++ b/engine/interfaces/Vec2.ts
@@ -9,8 +9,11 @@ export interface Dimension2D {
}
export interface Velocity2D {
- dx: number;
- dy: number;
+ dCartesian: {
+ dx: number;
+ dy: number;
+ };
+ dTheta: number;
}
export interface Force2D {
diff --git a/engine/interfaces/index.ts b/engine/interfaces/index.ts
index 8cdf4d8..c2f6896 100644
--- a/engine/interfaces/index.ts
+++ b/engine/interfaces/index.ts
@@ -1,4 +1,4 @@
-export * from "./Vec2";
-export * from "./Draw";
-export * from "./Direction";
-export * from "./Action";
+export * from './Vec2';
+export * from './Draw';
+export * from './Direction';
+export * from './Action';
diff --git a/engine/network/index.ts b/engine/network/index.ts
new file mode 100644
index 0000000..5dc7ece
--- /dev/null
+++ b/engine/network/index.ts
@@ -0,0 +1,37 @@
+export enum MessageType {
+ NEW_ENTITIES = 'NEW_ENTITIES',
+ REMOVE_ENTITIES = 'REMOVE_ENTITIES',
+ UPDATE_ENTITIES = 'UPDATE_ENTITIES',
+ NEW_INPUT = 'NEW_INPUT',
+ REMOVE_INPUT = 'REMOVE_INPUT'
+}
+
+export type EntityAddBody = {
+ entityName: string;
+ id: string;
+ args: Record;
+};
+
+export type EntityUpdateBody = {
+ id: string;
+ args: Record;
+};
+
+export type Message = {
+ type: MessageType;
+ body: any;
+};
+
+export interface MessageQueueProvider {
+ getNewMessages(): Message[];
+ clearMessages(): void;
+}
+
+export interface MessagePublisher {
+ addMessage(message: Message): void;
+ publish(): void;
+}
+
+export interface MessageProcessor {
+ process(message: Message): void;
+}
diff --git a/engine/structures/Grid.ts b/engine/structures/Grid.ts
new file mode 100644
index 0000000..5f0e053
--- /dev/null
+++ b/engine/structures/Grid.ts
@@ -0,0 +1,104 @@
+import type { Coord2D, Dimension2D } from '../interfaces';
+import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.';
+import { Miscellaneous } from '../config/constants';
+
+export class Grid implements RefreshingCollisionFinderBehavior {
+ private cellEntities: Map;
+
+ private gridDimension: Dimension2D;
+ private cellDimension: Dimension2D;
+ private topLeft: Coord2D;
+
+ constructor(
+ gridDimension: Dimension2D = {
+ width: Miscellaneous.WIDTH,
+ height: Miscellaneous.HEIGHT
+ },
+ cellDimension: Dimension2D = {
+ width: Miscellaneous.DEFAULT_GRID_WIDTH,
+ height: Miscellaneous.DEFAULT_GRID_HEIGHT
+ },
+ topLeft = { x: 0, y: 0 }
+ ) {
+ this.gridDimension = gridDimension;
+ this.cellDimension = cellDimension;
+ this.topLeft = topLeft;
+
+ this.cellEntities = new Map();
+ }
+
+ public insert(boxedEntry: BoxedEntry) {
+ this.getOverlappingCells(boxedEntry).forEach((gridIdx) => {
+ if (!this.cellEntities.has(gridIdx)) {
+ this.cellEntities.set(gridIdx, []);
+ }
+ this.cellEntities.get(gridIdx)!.push(boxedEntry.id);
+ });
+ }
+
+ public getNeighborIds(boxedEntry: BoxedEntry): Set {
+ const neighborIds: Set = new Set();
+ this.getOverlappingCells(boxedEntry).forEach((gridIdx) => {
+ if (this.cellEntities.has(gridIdx)) {
+ this.cellEntities.get(gridIdx)!.forEach((id) => neighborIds.add(id));
+ }
+ });
+ return neighborIds;
+ }
+
+ public clear() {
+ this.cellEntities.clear();
+ }
+
+ public setTopLeft(topLeft: Coord2D) {
+ this.topLeft = topLeft;
+ }
+
+ public setDimension(dimension: Dimension2D) {
+ this.gridDimension = dimension;
+ }
+
+ public setCellDimension(cellDimension: Dimension2D) {
+ this.cellDimension = cellDimension;
+ }
+
+ private getOverlappingCells(boxedEntry: BoxedEntry): number[] {
+ const { center, dimension } = boxedEntry;
+ const yBoxes = Math.ceil(
+ this.gridDimension.height / this.cellDimension.height
+ );
+ const xBoxes = Math.ceil(
+ this.gridDimension.width / this.cellDimension.width
+ );
+
+ const translated: Coord2D = {
+ y: center.y - this.topLeft.y,
+ x: center.x - this.topLeft.x
+ };
+
+ const topLeftBox = {
+ x: Math.floor(
+ (translated.x - dimension.width / 2) / this.cellDimension.width
+ ),
+ y: Math.floor(
+ (translated.y - dimension.height / 2) / this.cellDimension.height
+ )
+ };
+ const bottomRightBox = {
+ x: Math.floor(
+ (translated.x + dimension.width / 2) / this.cellDimension.width
+ ),
+ y: Math.floor(
+ (translated.y + dimension.height / 2) / this.cellDimension.height
+ )
+ };
+
+ const cells: number[] = [];
+
+ for (let y = topLeftBox.y; y <= bottomRightBox.y; ++y)
+ for (let x = topLeftBox.x; x <= bottomRightBox.x; ++x)
+ cells.push(yBoxes * y + x);
+
+ return cells;
+ }
+}
diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts
index d1ff3b1..93702d0 100644
--- a/engine/structures/QuadTree.ts
+++ b/engine/structures/QuadTree.ts
@@ -1,19 +1,21 @@
-import type { Coord2D, Dimension2D } from "../interfaces";
-
-interface BoxedEntry {
- id: number;
- dimension: Dimension2D;
- center: Coord2D;
-}
+import type { Coord2D, Dimension2D } from '../interfaces';
+import type { BoxedEntry, RefreshingCollisionFinderBehavior } from '.';
enum Quadrant {
I,
II,
III,
- IV,
+ IV
}
-export class QuadTree {
+/*
+ unused due to performance problems. here anyways, in case it _really_ is necessary at some point
+ (and to justify the amount of time i spent here).
+*/
+export class QuadTree implements RefreshingCollisionFinderBehavior {
+ private static readonly QUADTREE_MAX_LEVELS = 3;
+ private static readonly QUADTREE_SPLIT_THRESHOLD = 2000;
+
private maxLevels: number;
private splitThreshold: number;
private level: number;
@@ -24,34 +26,33 @@ export class QuadTree {
private objects: BoxedEntry[];
constructor(
- topLeft: Coord2D,
+ topLeft: Coord2D = { x: 0, y: 0 },
dimension: Dimension2D,
- maxLevels: number,
- splitThreshold: number,
- level?: number,
+ maxLevels: number = QuadTree.QUADTREE_MAX_LEVELS,
+ splitThreshold: number = QuadTree.QUADTREE_SPLIT_THRESHOLD,
+ level: number = 0
) {
this.children = new Map();
this.objects = [];
this.maxLevels = maxLevels;
this.splitThreshold = splitThreshold;
- this.level = level ?? 0;
+ this.level = level;
this.topLeft = topLeft;
this.dimension = dimension;
}
- public insert(id: number, dimension: Dimension2D, center: Coord2D): void {
- const box: BoxedEntry = { id, center, dimension };
+ public insert(boxedEntry: BoxedEntry): void {
if (this.hasChildren()) {
- this.getQuadrants(box).forEach((quadrant) => {
+ this.getQuadrants(boxedEntry).forEach((quadrant) => {
const quadrantBox = this.children.get(quadrant);
- quadrantBox?.insert(id, dimension, center);
+ quadrantBox!.insert(boxedEntry);
});
return;
}
- this.objects.push({ id, dimension, center });
+ this.objects.push(boxedEntry);
if (
this.objects.length > this.splitThreshold &&
@@ -66,22 +67,24 @@ export class QuadTree {
public clear(): void {
this.objects = [];
+
if (this.hasChildren()) {
this.children.forEach((child) => child.clear());
this.children.clear();
}
}
- public getNeighborIds(boxedEntry: BoxedEntry): number[] {
- const neighbors: number[] = this.objects.map(({ id }) => id);
+ public getNeighborIds(boxedEntry: BoxedEntry): Set {
+ const neighbors = new Set(
+ this.objects.map(({ id }) => id).filter((id) => id != boxedEntry.id)
+ );
if (this.hasChildren()) {
this.getQuadrants(boxedEntry).forEach((quadrant) => {
const quadrantBox = this.children.get(quadrant);
-
quadrantBox
?.getNeighborIds(boxedEntry)
- .forEach((id) => neighbors.push(id));
+ .forEach((id) => neighbors.add(id));
});
}
@@ -99,9 +102,9 @@ export class QuadTree {
[Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }],
[
Quadrant.IV,
- { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight },
- ],
- ] as [[Quadrant, Coord2D]]
+ { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }
+ ]
+ ] as [Quadrant, Coord2D][]
).forEach(([quadrant, pos]) => {
this.children.set(
quadrant,
@@ -110,8 +113,8 @@ export class QuadTree {
{ width: halfWidth, height: halfHeight },
this.maxLevels,
this.splitThreshold,
- this.level + 1,
- ),
+ this.level + 1
+ )
);
});
}
@@ -119,52 +122,48 @@ export class QuadTree {
private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] {
const treeCenter: Coord2D = {
x: this.topLeft.x + this.dimension.width / 2,
- y: this.topLeft.y + this.dimension.height / 2,
+ y: this.topLeft.y + this.dimension.height / 2
};
return (
[
[
Quadrant.I,
- (x: number, y: number) => x >= treeCenter.x && y < treeCenter.y,
+ (x: number, y: number) => x >= treeCenter.x && y < treeCenter.y
],
[
Quadrant.II,
- (x: number, y: number) => x < treeCenter.x && y < treeCenter.y,
+ (x: number, y: number) => x < treeCenter.x && y < treeCenter.y
],
[
Quadrant.III,
- (x: number, y: number) => x < treeCenter.x && y >= treeCenter.y,
+ (x: number, y: number) => x < treeCenter.x && y >= treeCenter.y
],
[
Quadrant.IV,
- (x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y,
- ],
- ] as [[Quadrant, (x: number, y: number) => boolean]]
+ (x: number, y: number) => x >= treeCenter.x && y >= treeCenter.y
+ ]
+ ] as [Quadrant, (x: number, y: number) => boolean][]
)
.filter(
([_quadrant, condition]) =>
condition(
boxedEntry.center.x + boxedEntry.dimension.width / 2,
- boxedEntry.center.y + boxedEntry.dimension.height / 2,
+ boxedEntry.center.y + boxedEntry.dimension.height / 2
) ||
condition(
boxedEntry.center.x - boxedEntry.dimension.width / 2,
- boxedEntry.center.y - boxedEntry.dimension.height / 2,
- ),
+ boxedEntry.center.y - boxedEntry.dimension.height / 2
+ )
)
.map(([quadrant]) => quadrant);
}
private realignObjects(): void {
this.objects.forEach((boxedEntry) => {
- this.getQuadrants(boxedEntry).forEach((direction) => {
- const quadrant = this.children.get(direction);
- quadrant?.insert(
- boxedEntry.id,
- boxedEntry.dimension,
- boxedEntry.center,
- );
+ this.getQuadrants(boxedEntry).forEach((quadrant) => {
+ const quadrantBox = this.children.get(quadrant);
+ quadrantBox!.insert(boxedEntry);
});
});
@@ -174,4 +173,12 @@ export class QuadTree {
private hasChildren() {
return this.children && this.children.size > 0;
}
+
+ public setTopLeft(topLeft: Coord2D) {
+ this.topLeft = topLeft;
+ }
+
+ public setDimension(dimension: Dimension2D) {
+ this.dimension = dimension;
+ }
}
diff --git a/engine/structures/RefreshingCollisionFinderBehavior.ts b/engine/structures/RefreshingCollisionFinderBehavior.ts
new file mode 100644
index 0000000..573ddd8
--- /dev/null
+++ b/engine/structures/RefreshingCollisionFinderBehavior.ts
@@ -0,0 +1,14 @@
+import type { Coord2D, Dimension2D } from '../interfaces';
+
+export interface BoxedEntry {
+ id: string;
+ dimension: Dimension2D;
+ center: Coord2D;
+}
+
+export interface RefreshingCollisionFinderBehavior {
+ clear(): void;
+ insert(boxedEntry: BoxedEntry): void;
+ getNeighborIds(boxedEntry: BoxedEntry): Set;
+ setTopLeft(topLeft: Coord2D): void;
+}
diff --git a/engine/structures/index.ts b/engine/structures/index.ts
index 605a82a..679dbd4 100644
--- a/engine/structures/index.ts
+++ b/engine/structures/index.ts
@@ -1 +1,3 @@
-export * from "./QuadTree";
+export * from './RefreshingCollisionFinderBehavior';
+export * from './QuadTree';
+export * from './Grid';
diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts
index 2bba03b..4a838dd 100644
--- a/engine/systems/Collision.ts
+++ b/engine/systems/Collision.ts
@@ -1,61 +1,59 @@
-import { SystemNames, System } from ".";
+import { SystemNames, System } from '.';
import {
Mass,
BoundingBox,
ComponentNames,
Jump,
Velocity,
- Forces,
-} from "../components";
-import { Game } from "../Game";
-import { PhysicsConstants } from "../config";
-import { Entity } from "../entities";
-import type { Dimension2D } from "../interfaces";
-import { QuadTree } from "../structures";
+ Forces
+} from '../components';
+import { Game } from '../Game';
+import { Miscellaneous, PhysicsConstants } from '../config';
+import { Entity } from '../entities';
+import type { Coord2D, Dimension2D, Velocity2D } from '../interfaces';
+import { BoxedEntry, RefreshingCollisionFinderBehavior } from '../structures';
export class Collision extends System {
private static readonly COLLIDABLE_COMPONENT_NAMES = [
ComponentNames.Collide,
- ComponentNames.TopCollidable,
+ ComponentNames.TopCollidable
];
- private static readonly QUADTREE_MAX_LEVELS = 10;
- private static readonly QUADTREE_SPLIT_THRESHOLD = 10;
- private quadTree: QuadTree;
+ private collisionFinder: RefreshingCollisionFinderBehavior;
- constructor(screenDimensions: Dimension2D) {
+ constructor(refreshingCollisionFinder: RefreshingCollisionFinderBehavior) {
super(SystemNames.Collision);
- this.quadTree = new QuadTree(
- { x: 0, y: 0 },
- screenDimensions,
- Collision.QUADTREE_MAX_LEVELS,
- Collision.QUADTREE_SPLIT_THRESHOLD,
- );
+ this.collisionFinder = refreshingCollisionFinder;
}
public update(_dt: number, game: Game) {
- // rebuild the quadtree
- this.quadTree.clear();
+ this.collisionFinder.clear();
- const entitiesToAddToQuadtree: Entity[] = [];
+ const entitiesToAddToCollisionFinder: Entity[] = [];
Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) =>
- game.componentEntities.get(componentName),
- ).forEach(
- (entityIds?: Set) =>
- entityIds?.forEach((id) => {
- const entity = game.entities.get(id);
- if (!entity || !entity.hasComponent(ComponentNames.BoundingBox)) {
- return;
- }
- entitiesToAddToQuadtree.push(entity);
- }),
+ game.forEachEntityWithComponent(componentName, (entity) => {
+ if (!entity.hasComponent(ComponentNames.BoundingBox)) {
+ return;
+ }
+ entitiesToAddToCollisionFinder.push(entity);
+ })
);
- entitiesToAddToQuadtree.forEach((entity) => {
+ this.insertEntitiesAndUpdateBounds(entitiesToAddToCollisionFinder);
+ this.findCollidingEntitiesAndCollide(entitiesToAddToCollisionFinder, game);
+ }
+
+ private insertEntitiesAndUpdateBounds(entities: Entity[]) {
+ const collisionFinderInsertions: BoxedEntry[] = [];
+
+ const topLeft: Coord2D = { x: Infinity, y: Infinity };
+ const bottomRight: Coord2D = { x: -Infinity, y: -Infinity };
+
+ entities.forEach((entity) => {
const boundingBox = entity.getComponent(
- ComponentNames.BoundingBox,
+ ComponentNames.BoundingBox
);
let dimension = { ...boundingBox.dimension };
@@ -63,18 +61,43 @@ export class Collision extends System {
dimension = boundingBox.getOutscribedBoxDims();
}
- this.quadTree.insert(entity.id, dimension, boundingBox.center);
+ const { center } = boundingBox;
+ const topLeftBoundingBox = boundingBox.getTopLeft();
+ const bottomRightBoundingBox = boundingBox.getBottomRight();
+
+ topLeft.x = Math.min(topLeftBoundingBox.x, topLeft.x);
+ topLeft.y = Math.min(topLeftBoundingBox.y, topLeft.y);
+ bottomRight.x = Math.max(bottomRightBoundingBox.x, bottomRight.x);
+ bottomRight.y = Math.max(bottomRightBoundingBox.y, bottomRight.y);
+
+ collisionFinderInsertions.push({
+ id: entity.id,
+ dimension,
+ center
+ });
});
- // find colliding entities and perform collisions
- const collidingEntities = this.getCollidingEntities(
- entitiesToAddToQuadtree,
- game,
+ // set bounds first
+ if (entities.length > 0) {
+ this.collisionFinder.setTopLeft(topLeft);
+ this.collisionFinder.setDimension({
+ width: bottomRight.x - topLeft.x,
+ height: bottomRight.y - topLeft.y
+ });
+ }
+
+ // then, begin insertions
+ collisionFinderInsertions.forEach((boxedEntry: BoxedEntry) =>
+ this.collisionFinder.insert(boxedEntry)
);
+ }
+
+ private findCollidingEntitiesAndCollide(entities: Entity[], game: Game) {
+ const collidingEntities = this.getCollidingEntities(entities, game);
collidingEntities.forEach(([entityAId, entityBId]) => {
const [entityA, entityB] = [entityAId, entityBId].map((id) =>
- game.entities.get(id),
+ game.entities.get(id)
);
if (entityA && entityB) {
this.performCollision(entityA, entityB);
@@ -84,12 +107,14 @@ export class Collision extends System {
private performCollision(entityA: Entity, entityB: Entity) {
const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map(
- (entity) => entity.getComponent(ComponentNames.BoundingBox),
+ (entity) => entity.getComponent(ComponentNames.BoundingBox)
);
- let velocity = new Velocity();
+ let velocity: Velocity2D = { dCartesian: { dx: 0, dy: 0 }, dTheta: 0 };
if (entityA.hasComponent(ComponentNames.Velocity)) {
- velocity = entityA.getComponent(ComponentNames.Velocity);
+ velocity = entityA.getComponent(
+ ComponentNames.Velocity
+ ).velocity;
}
if (
@@ -100,7 +125,7 @@ export class Collision extends System {
) {
if (entityBBoundingBox.rotation != 0) {
throw new Error(
- `entity with id ${entityB.id} has TopCollidable component and a non-zero rotation. that is not (yet) supported.`,
+ `entity with id ${entityB.id} has TopCollidable component and a non-zero rotation. that is not (yet) supported.`
);
}
@@ -114,7 +139,7 @@ export class Collision extends System {
entityA.getComponent(ComponentNames.Forces).forces.push({
fCartesian: { fy: F_n, fx: 0 },
- torque: 0,
+ torque: 0
});
}
@@ -132,35 +157,33 @@ export class Collision extends System {
private getCollidingEntities(
collidableEntities: Entity[],
- game: Game,
- ): [number, number][] {
- const collidingEntityIds: [number, number][] = [];
+ game: Game
+ ): [string, string][] {
+ const collidingEntityIds: [string, string][] = [];
for (const entity of collidableEntities) {
const boundingBox = entity.getComponent(
- ComponentNames.BoundingBox,
+ ComponentNames.BoundingBox
);
- const neighborIds = this.quadTree
- .getNeighborIds({
- id: entity.id,
- dimension: boundingBox.dimension,
- center: boundingBox.center,
- })
- .filter((neighborId) => neighborId != entity.id);
+ const neighborIds = this.collisionFinder.getNeighborIds({
+ id: entity.id,
+ dimension: boundingBox.dimension,
+ center: boundingBox.center
+ });
- neighborIds.forEach((neighborId) => {
+ for (const neighborId of neighborIds) {
const neighbor = game.getEntity(neighborId);
if (!neighbor) return;
const neighborBoundingBox = neighbor.getComponent(
- ComponentNames.BoundingBox,
+ ComponentNames.BoundingBox
);
if (boundingBox.isCollidingWith(neighborBoundingBox)) {
collidingEntityIds.push([entity.id, neighborId]);
}
- });
+ }
}
return collidingEntityIds;
@@ -169,11 +192,11 @@ export class Collision extends System {
// ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ
private getDyToPushOutOfFloor(
entityBoundingBox: BoundingBox,
- floorBoundingBox: BoundingBox,
+ floorBoundingBox: BoundingBox
): number {
const {
dimension: { width, height },
- center: { x },
+ center: { x }
} = entityBoundingBox;
const outScribedRectangle = entityBoundingBox.getOutscribedBoxDims();
@@ -192,7 +215,7 @@ export class Collision extends System {
if (x >= floorBoundingBox.center.x) {
boundedCollisionX = Math.min(
floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2,
- clippedX,
+ clippedX
);
return (
outScribedRectangle.height / 2 -
@@ -202,7 +225,7 @@ export class Collision extends System {
boundedCollisionX = Math.max(
floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2,
- clippedX,
+ clippedX
);
return (
diff --git a/engine/systems/FacingDirection.ts b/engine/systems/FacingDirection.ts
index 4426ab6..01f32cf 100644
--- a/engine/systems/FacingDirection.ts
+++ b/engine/systems/FacingDirection.ts
@@ -2,10 +2,10 @@ import {
ComponentNames,
Velocity,
FacingDirection as FacingDirectionComponent,
- Control,
-} from "../components";
-import { Game } from "../Game";
-import { System, SystemNames } from "./";
+ Control
+} from '../components';
+import { Game } from '../Game';
+import { System, SystemNames } from './';
export class FacingDirection extends System {
constructor() {
@@ -20,24 +20,27 @@ export class FacingDirection extends System {
return;
}
- const totalVelocity: Velocity = new Velocity();
+ const totalVelocityComponent = new Velocity();
const control = entity.getComponent(ComponentNames.Control);
- const velocity = entity.getComponent(ComponentNames.Velocity);
- totalVelocity.add(velocity);
+ const velocity = entity.getComponent(
+ ComponentNames.Velocity
+ ).velocity;
+
+ totalVelocityComponent.add(velocity);
if (control) {
- totalVelocity.add(control.controlVelocity);
+ totalVelocityComponent.add(control.controlVelocityComponent.velocity);
}
const facingDirection = entity.getComponent(
- ComponentNames.FacingDirection,
+ ComponentNames.FacingDirection
);
- if (totalVelocity.dCartesian.dx > 0) {
+ if (totalVelocityComponent.velocity.dCartesian.dx > 0) {
entity.addComponent(facingDirection.facingRightSprite);
- } else if (totalVelocity.dCartesian.dx < 0) {
+ } else if (totalVelocityComponent.velocity.dCartesian.dx < 0) {
entity.addComponent(facingDirection.facingLeftSprite);
}
- },
+ }
);
}
}
diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts
index 4aa9844..9afd1ab 100644
--- a/engine/systems/Input.ts
+++ b/engine/systems/Input.ts
@@ -4,30 +4,117 @@ import {
ComponentNames,
Velocity,
Mass,
- Control,
-} from "../components";
-import { Game } from "../Game";
-import { KeyConstants, PhysicsConstants } from "../config";
-import { Action } from "../interfaces";
-import { System, SystemNames } from "./";
+ Control
+} from '../components';
+import { Game } from '../Game';
+import { KeyConstants, PhysicsConstants } from '../config';
+import { Action } from '../interfaces';
+import { System, SystemNames } from '.';
+import { MessagePublisher, MessageType } from '../network';
+import { Entity } from '../entities';
export class Input extends System {
+ public clientId: string;
+
private keys: Set;
private actionTimeStamps: Map;
+ private messagePublisher?: MessagePublisher;
- constructor() {
+ constructor(clientId: string, messagePublisher?: MessagePublisher) {
super(SystemNames.Input);
- this.keys = new Set();
- this.actionTimeStamps = new Map();
+ this.clientId = clientId;
+ this.keys = new Set();
+ this.actionTimeStamps = new Map();
+
+ this.messagePublisher = messagePublisher;
}
public keyPressed(key: string) {
this.keys.add(key);
+
+ if (this.messagePublisher) {
+ this.messagePublisher.addMessage({
+ type: MessageType.NEW_INPUT,
+ body: key
+ });
+ }
}
public keyReleased(key: string) {
this.keys.delete(key);
+
+ if (this.messagePublisher) {
+ this.messagePublisher.addMessage({
+ type: MessageType.REMOVE_INPUT,
+ body: key
+ });
+ }
+ }
+
+ public update(_dt: number, game: Game) {
+ game.forEachEntityWithComponent(ComponentNames.Control, (entity) =>
+ this.handleInput(entity)
+ );
+ }
+
+ public handleInput(entity: Entity) {
+ const controlComponent = entity.getComponent(
+ ComponentNames.Control
+ );
+ controlComponent.isControllable =
+ controlComponent.controllableBy === this.clientId;
+
+ if (!controlComponent.isControllable) return;
+
+ if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
+ controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
+ PhysicsConstants.PLAYER_MOVE_VEL;
+ }
+
+ if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
+ controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
+ -PhysicsConstants.PLAYER_MOVE_VEL;
+ }
+
+ if (
+ entity.hasComponent(ComponentNames.Jump) &&
+ this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))
+ ) {
+ this.performJump(entity);
+ }
+ }
+
+ private performJump(entity: Entity) {
+ const velocity = entity.getComponent(
+ ComponentNames.Velocity
+ ).velocity;
+ const jump = entity.getComponent(ComponentNames.Jump);
+
+ if (jump.canJump) {
+ this.actionTimeStamps.set(Action.JUMP, performance.now());
+
+ velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
+ jump.canJump = false;
+ }
+
+ if (
+ performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
+ PhysicsConstants.MAX_JUMP_TIME_MS
+ ) {
+ const mass = entity.getComponent(ComponentNames.Mass).mass;
+
+ const jumpForce = {
+ fCartesian: {
+ fy: mass * PhysicsConstants.PLAYER_JUMP_ACC,
+ fx: 0
+ },
+ torque: 0
+ };
+ entity
+ .getComponent(ComponentNames.Forces)
+ ?.forces.push(jumpForce);
+ }
}
private hasSomeKey(keys?: string[]): boolean {
@@ -36,48 +123,4 @@ export class Input extends System {
}
return false;
}
-
- public update(_dt: number, game: Game) {
- game.forEachEntityWithComponent(ComponentNames.Control, (entity) => {
- const control = entity.getComponent(ComponentNames.Control);
-
- if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
- control.controlVelocity.dCartesian.dx +=
- PhysicsConstants.PLAYER_MOVE_VEL;
- }
-
- if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
- control.controlVelocity.dCartesian.dx +=
- -PhysicsConstants.PLAYER_MOVE_VEL;
- }
-
- if (entity.hasComponent(ComponentNames.Jump)) {
- const velocity = entity.getComponent(ComponentNames.Velocity);
- const jump = entity.getComponent(ComponentNames.Jump);
-
- if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) {
- if (jump.canJump) {
- this.actionTimeStamps.set(Action.JUMP, performance.now());
-
- velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
- jump.canJump = false;
- }
-
- if (
- performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
- PhysicsConstants.MAX_JUMP_TIME_MS
- ) {
- const mass = entity.getComponent(ComponentNames.Mass).mass;
- entity.getComponent(ComponentNames.Forces)?.forces.push({
- fCartesian: {
- fy: mass * PhysicsConstants.PLAYER_JUMP_ACC,
- fx: 0,
- },
- torque: 0,
- });
- }
- }
- }
- });
- }
}
diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts
new file mode 100644
index 0000000..6d13574
--- /dev/null
+++ b/engine/systems/NetworkUpdate.ts
@@ -0,0 +1,72 @@
+import { System, SystemNames } from '.';
+import { Game } from '../Game';
+import { ComponentNames } from '../components';
+import {
+ type MessageQueueProvider,
+ type MessagePublisher,
+ type MessageProcessor,
+ MessageType,
+ EntityUpdateBody
+} from '../network';
+
+export class NetworkUpdate extends System {
+ private queueProvider: MessageQueueProvider;
+ private publisher: MessagePublisher;
+ private messageProcessor: MessageProcessor;
+
+ private entityUpdateTimers: Map;
+
+ constructor(
+ queueProvider: MessageQueueProvider,
+ publisher: MessagePublisher,
+ messageProcessor: MessageProcessor
+ ) {
+ super(SystemNames.NetworkUpdate);
+
+ this.queueProvider = queueProvider;
+ this.publisher = publisher;
+ this.messageProcessor = messageProcessor;
+
+ this.entityUpdateTimers = new Map();
+ }
+
+ public update(dt: number, game: Game) {
+ // 1. process new messages
+ this.queueProvider
+ .getNewMessages()
+ .forEach((message) => this.messageProcessor.process(message));
+ this.queueProvider.clearMessages();
+
+ // 2. send entity updates
+ const updateMessages: EntityUpdateBody[] = [];
+ game.forEachEntityWithComponent(
+ ComponentNames.NetworkUpdateable,
+ (entity) => {
+ let timer = this.entityUpdateTimers.get(entity.id) ?? dt;
+ timer -= dt;
+ this.entityUpdateTimers.set(entity.id, timer);
+
+ if (timer > 0) return;
+ this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs());
+
+ if (entity.hasComponent(ComponentNames.NetworkUpdateable)) {
+ updateMessages.push({
+ id: entity.id,
+ args: entity.serialize()
+ });
+ }
+ }
+ );
+ this.publisher.addMessage({
+ type: MessageType.UPDATE_ENTITIES,
+ body: updateMessages
+ });
+
+ // 3. publish changes
+ this.publisher.publish();
+ }
+
+ private getNextUpdateTimeMs() {
+ return Math.random() * 70 + 50;
+ }
+}
diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts
index 38962a6..b5df459 100644
--- a/engine/systems/Physics.ts
+++ b/engine/systems/Physics.ts
@@ -1,4 +1,4 @@
-import { System, SystemNames } from ".";
+import { System, SystemNames } from '.';
import {
BoundingBox,
ComponentNames,
@@ -8,11 +8,11 @@ import {
Mass,
Jump,
Moment,
- Control,
-} from "../components";
-import { PhysicsConstants } from "../config";
-import type { Force2D } from "../interfaces";
-import { Game } from "../Game";
+ Control
+} from '../components';
+import { PhysicsConstants } from '../config';
+import type { Force2D, Velocity2D } from '../interfaces';
+import { Game } from '../Game';
export class Physics extends System {
constructor() {
@@ -23,9 +23,11 @@ export class Physics extends System {
game.forEachEntityWithComponent(ComponentNames.Forces, (entity) => {
const mass = entity.getComponent(ComponentNames.Mass).mass;
const forces = entity.getComponent(ComponentNames.Forces).forces;
- const velocity = entity.getComponent(ComponentNames.Velocity);
+ const velocity = entity.getComponent(
+ ComponentNames.Velocity
+ ).velocity;
const inertia = entity.getComponent(
- ComponentNames.Moment,
+ ComponentNames.Moment
).inertia;
// F_g = mg, applied only until terminal velocity is reached
@@ -35,9 +37,9 @@ export class Physics extends System {
forces.push({
fCartesian: {
fy: mass * PhysicsConstants.GRAVITY,
- fx: 0,
+ fx: 0
},
- torque: 0,
+ torque: 0
});
}
}
@@ -47,17 +49,17 @@ export class Physics extends System {
(accum: Force2D, { fCartesian, torque }: Force2D) => ({
fCartesian: {
fx: accum.fCartesian.fx + (fCartesian?.fx ?? 0),
- fy: accum.fCartesian.fy + (fCartesian?.fy ?? 0),
+ fy: accum.fCartesian.fy + (fCartesian?.fy ?? 0)
},
- torque: accum.torque + (torque ?? 0),
+ torque: accum.torque + (torque ?? 0)
}),
- { fCartesian: { fx: 0, fy: 0 }, torque: 0 },
+ { fCartesian: { fx: 0, fy: 0 }, torque: 0 }
);
// integrate accelerations
const [ddy, ddx] = [
sumOfForces.fCartesian.fy,
- sumOfForces.fCartesian.fx,
+ sumOfForces.fCartesian.fx
].map((x) => x / mass);
velocity.dCartesian.dx += ddx * dt;
velocity.dCartesian.dy += ddy * dt;
@@ -73,30 +75,32 @@ export class Physics extends System {
});
game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => {
- const velocity: Velocity = new Velocity();
+ const velocityComponent: Velocity = new Velocity();
const control = entity.getComponent(ComponentNames.Control);
- velocity.add(entity.getComponent(ComponentNames.Velocity));
+ velocityComponent.add(
+ entity.getComponent(ComponentNames.Velocity).velocity
+ );
if (control) {
- velocity.add(control.controlVelocity);
+ velocityComponent.add(control.controlVelocityComponent.velocity);
}
const boundingBox = entity.getComponent(
- ComponentNames.BoundingBox,
+ ComponentNames.BoundingBox
);
// integrate velocity
- boundingBox.center.x += velocity.dCartesian.dx * dt;
- boundingBox.center.y += velocity.dCartesian.dy * dt;
- boundingBox.rotation += velocity.dTheta * dt;
+ boundingBox.center.x += velocityComponent.velocity.dCartesian.dx * dt;
+ boundingBox.center.y += velocityComponent.velocity.dCartesian.dy * dt;
+ boundingBox.rotation += velocityComponent.velocity.dTheta * dt;
boundingBox.rotation =
(boundingBox.rotation < 0
? 360 + boundingBox.rotation
: boundingBox.rotation) % 360;
// clear the control velocity
- if (control) {
- control.controlVelocity = new Velocity();
+ if (control && control.isControllable) {
+ control.controlVelocityComponent = new Velocity();
}
});
}
diff --git a/engine/systems/Render.ts b/engine/systems/Render.ts
index 9bb4091..4a4500d 100644
--- a/engine/systems/Render.ts
+++ b/engine/systems/Render.ts
@@ -1,7 +1,7 @@
-import { System, SystemNames } from ".";
-import { BoundingBox, ComponentNames, Sprite } from "../components";
-import { Game } from "../Game";
-import { clamp } from "../utils";
+import { System, SystemNames } from '.';
+import { BoundingBox, ComponentNames, Sprite } from '../components';
+import { Game } from '../Game';
+import { clamp } from '../utils';
export class Render extends System {
private ctx: CanvasRenderingContext2D;
@@ -19,7 +19,7 @@ export class Render extends System {
sprite.update(dt);
const boundingBox = entity.getComponent(
- ComponentNames.BoundingBox,
+ ComponentNames.BoundingBox
);
// don't render if we're outside the screen
@@ -27,12 +27,12 @@ export class Render extends System {
clamp(
boundingBox.center.y,
-boundingBox.dimension.height / 2,
- this.ctx.canvas.height + boundingBox.dimension.height / 2,
+ this.ctx.canvas.height + boundingBox.dimension.height / 2
) != boundingBox.center.y ||
clamp(
boundingBox.center.x,
-boundingBox.dimension.width / 2,
- this.ctx.canvas.width + boundingBox.dimension.width / 2,
+ this.ctx.canvas.width + boundingBox.dimension.width / 2
) != boundingBox.center.x
) {
return;
@@ -41,7 +41,7 @@ export class Render extends System {
const drawArgs = {
center: boundingBox.center,
dimension: boundingBox.dimension,
- rotation: boundingBox.rotation,
+ rotation: boundingBox.rotation
};
sprite.draw(this.ctx, drawArgs);
diff --git a/engine/systems/System.ts b/engine/systems/System.ts
index 8b00dc5..de41988 100644
--- a/engine/systems/System.ts
+++ b/engine/systems/System.ts
@@ -1,4 +1,4 @@
-import { Game } from "../Game";
+import { Game } from '../Game';
export abstract class System {
public readonly name: string;
diff --git a/engine/systems/WallBounds.ts b/engine/systems/WallBounds.ts
index a0d4a9c..7da84e4 100644
--- a/engine/systems/WallBounds.ts
+++ b/engine/systems/WallBounds.ts
@@ -1,28 +1,24 @@
-import { System, SystemNames } from ".";
-import { BoundingBox, ComponentNames } from "../components";
-import { Game } from "../Game";
-import type { Entity } from "../entities";
-import { clamp } from "../utils";
+import { System, SystemNames } from '.';
+import { BoundingBox, ComponentNames } from '../components';
+import { Game } from '../Game';
+import { clamp } from '../utils';
+import { Miscellaneous } from '../config';
export class WallBounds extends System {
- private screenWidth: number;
-
- constructor(screenWidth: number) {
+ constructor() {
super(SystemNames.WallBounds);
-
- this.screenWidth = screenWidth;
}
public update(_dt: number, game: Game) {
game.forEachEntityWithComponent(ComponentNames.WallBounded, (entity) => {
const boundingBox = entity.getComponent(
- ComponentNames.BoundingBox,
+ ComponentNames.BoundingBox
);
boundingBox.center.x = clamp(
boundingBox.center.x,
boundingBox.dimension.width / 2,
- this.screenWidth - boundingBox.dimension.width / 2,
+ Miscellaneous.WIDTH - boundingBox.dimension.width / 2
);
});
}
diff --git a/engine/systems/index.ts b/engine/systems/index.ts
index 6cb6f35..43181e9 100644
--- a/engine/systems/index.ts
+++ b/engine/systems/index.ts
@@ -1,8 +1,9 @@
-export * from "./names";
-export * from "./System";
-export * from "./Render";
-export * from "./Physics";
-export * from "./Input";
-export * from "./FacingDirection";
-export * from "./Collision";
-export * from "./WallBounds";
+export * from './names';
+export * from './System';
+export * from './Render';
+export * from './Physics';
+export * from './Input';
+export * from './FacingDirection';
+export * from './Collision';
+export * from './WallBounds';
+export * from './NetworkUpdate';
diff --git a/engine/systems/names.ts b/engine/systems/names.ts
index 23f31fc..ddf6f19 100644
--- a/engine/systems/names.ts
+++ b/engine/systems/names.ts
@@ -1,8 +1,9 @@
export namespace SystemNames {
- export const Render = "Render";
- export const Physics = "Physics";
- export const FacingDirection = "FacingDirection";
- export const Input = "Input";
- export const Collision = "Collision";
- export const WallBounds = "WallBounds";
+ export const Render = 'Render';
+ export const Physics = 'Physics';
+ export const FacingDirection = 'FacingDirection';
+ export const Input = 'Input';
+ export const Collision = 'Collision';
+ export const WallBounds = 'WallBounds';
+ export const NetworkUpdate = 'NetworkUpdate';
}
diff --git a/engine/utils/coding.ts b/engine/utils/coding.ts
new file mode 100644
index 0000000..3f78889
--- /dev/null
+++ b/engine/utils/coding.ts
@@ -0,0 +1,27 @@
+const replacer = (_key: any, value: any) => {
+ if (value instanceof Map) {
+ return {
+ dataType: 'Map',
+ value: Array.from(value.entries())
+ };
+ } else {
+ return value;
+ }
+};
+
+const reviver = (_key: any, value: any) => {
+ if (typeof value === 'object' && value !== null) {
+ if (value.dataType === 'Map') {
+ return new Map(value.value);
+ }
+ }
+ return value;
+};
+
+export const stringify = (obj: any) => {
+ return JSON.stringify(obj, replacer);
+};
+
+export const parse = (str: string) => {
+ return JSON.parse(str, reviver) as unknown as T;
+};
diff --git a/engine/utils/dotProduct.ts b/engine/utils/dotProduct.ts
index 59f8857..82bcdea 100644
--- a/engine/utils/dotProduct.ts
+++ b/engine/utils/dotProduct.ts
@@ -1,4 +1,4 @@
-import type { Coord2D } from "../interfaces";
+import type { Coord2D } from '../interfaces';
export const dotProduct = (vector1: Coord2D, vector2: Coord2D): number =>
vector1.x * vector2.x + vector1.y * vector2.y;
diff --git a/engine/utils/index.ts b/engine/utils/index.ts
index 82a0d05..65446d1 100644
--- a/engine/utils/index.ts
+++ b/engine/utils/index.ts
@@ -1,3 +1,4 @@
-export * from "./rotateVector";
-export * from "./dotProduct";
-export * from "./clamp";
+export * from './rotateVector';
+export * from './dotProduct';
+export * from './clamp';
+export * from './coding';
diff --git a/engine/utils/rotateVector.ts b/engine/utils/rotateVector.ts
index 82bb54d..221ffb2 100644
--- a/engine/utils/rotateVector.ts
+++ b/engine/utils/rotateVector.ts
@@ -1,4 +1,4 @@
-import type { Coord2D } from "../interfaces";
+import type { Coord2D } from '../interfaces';
/**
* ([[cos(θ), -sin(θ),]) ([x,)
@@ -10,6 +10,6 @@ export const rotateVector = (vector: Coord2D, theta: number): Coord2D => {
return {
x: vector.x * cos - vector.y * sin,
- y: vector.x * sin + vector.y * cos,
+ y: vector.x * sin + vector.y * cos
};
};
diff --git a/server/bun.lockb b/server/bun.lockb
index 7f8b5ce..28b67ce 100755
Binary files a/server/bun.lockb and b/server/bun.lockb differ
diff --git a/server/package.json b/server/package.json
index 17d3c25..388cff2 100644
--- a/server/package.json
+++ b/server/package.json
@@ -8,6 +8,5 @@
"peerDependencies": {
"typescript": "^5.0.0"
},
- "dependencies": {
- }
+ "dependencies": {}
}
diff --git a/server/src/constants.ts b/server/src/constants.ts
new file mode 100644
index 0000000..a2b3d12
--- /dev/null
+++ b/server/src/constants.ts
@@ -0,0 +1,6 @@
+export namespace Constants {
+ export const SERVER_PORT = 8080;
+ export const SERVER_TICK_RATE = (1 / 60) * 1000;
+ export const GAME_TOPIC = 'game';
+ export const MAX_PLAYERS = 8;
+}
diff --git a/server/src/main.ts b/server/src/main.ts
new file mode 100644
index 0000000..0e47491
--- /dev/null
+++ b/server/src/main.ts
@@ -0,0 +1,59 @@
+import { Grid } from '@engine/structures';
+import {
+ ServerMessageProcessor,
+ ServerSocketMessagePublisher,
+ ServerSocketMessageReceiver,
+ MemorySessionManager,
+ SessionInputSystem
+} from './network';
+import { Collision, NetworkUpdate, Physics, WallBounds } from '@engine/systems';
+import { Game } from '@engine/Game';
+import { Constants } from './constants';
+import { GameServer } from './server';
+import { Floor } from '@engine/entities';
+import { BoundingBox } from '@engine/components';
+import { Miscellaneous } from '@engine/config';
+
+const game = new Game();
+
+const sessionManager = new MemorySessionManager();
+
+const messageReceiver = new ServerSocketMessageReceiver();
+const messagePublisher = new ServerSocketMessagePublisher();
+const messageProcessor = new ServerMessageProcessor(game, sessionManager);
+
+const server = new GameServer(
+ game,
+ messageReceiver,
+ messagePublisher,
+ sessionManager
+);
+
+[
+ new SessionInputSystem(sessionManager),
+ new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
+ new Physics(),
+ new Collision(new Grid()),
+ new WallBounds()
+].forEach((system) => game.addSystem(system));
+
+const floor = new Floor(160);
+const floorHeight = 200;
+
+floor.addComponent(
+ new BoundingBox(
+ {
+ x: Miscellaneous.WIDTH / 2,
+ y: Miscellaneous.HEIGHT + floorHeight / 2
+ },
+ { width: Miscellaneous.WIDTH, height: floorHeight }
+ )
+);
+game.addEntity(floor);
+
+game.start();
+setInterval(() => {
+ game.doGameLoop(performance.now());
+}, Constants.SERVER_TICK_RATE);
+
+server.serve();
diff --git a/server/src/network/MessageProcessor.ts b/server/src/network/MessageProcessor.ts
new file mode 100644
index 0000000..2d9f11f
--- /dev/null
+++ b/server/src/network/MessageProcessor.ts
@@ -0,0 +1,36 @@
+import {
+ EntityUpdateBody,
+ MessageProcessor,
+ MessageType
+} from '@engine/network';
+import { ServerMessage, SessionManager } from '.';
+import { Game } from '@engine/Game';
+
+export class ServerMessageProcessor implements MessageProcessor {
+ private game: Game;
+ private sessionManager: SessionManager;
+
+ constructor(game: Game, sessionManager: SessionManager) {
+ this.game = game;
+ this.sessionManager = sessionManager;
+ }
+
+ public process(message: ServerMessage) {
+ switch (message.type) {
+ case MessageType.NEW_INPUT: {
+ const { sessionId } = message.sessionData;
+ const session = this.sessionManager.getSession(sessionId);
+ session?.inputSystem.keyPressed(message.body as string);
+ break;
+ }
+ case MessageType.REMOVE_INPUT: {
+ const { sessionId } = message.sessionData;
+ const session = this.sessionManager.getSession(sessionId);
+ session?.inputSystem.keyReleased(message.body as string);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+}
diff --git a/server/src/network/MessagePublisher.ts b/server/src/network/MessagePublisher.ts
new file mode 100644
index 0000000..9c6011f
--- /dev/null
+++ b/server/src/network/MessagePublisher.ts
@@ -0,0 +1,31 @@
+import { Message, MessagePublisher } from '@engine/network';
+import { Server } from 'bun';
+import { Constants } from '../constants';
+import { stringify } from '@engine/utils';
+
+export class ServerSocketMessagePublisher implements MessagePublisher {
+ private server?: Server;
+ private messages: Message[];
+
+ constructor(server?: Server) {
+ this.messages = [];
+
+ if (server) this.setServer(server);
+ }
+
+ public setServer(server: Server) {
+ this.server = server;
+ }
+
+ public addMessage(message: Message) {
+ this.messages.push(message);
+ }
+
+ public publish() {
+ if (this.messages.length) {
+ this.server?.publish(Constants.GAME_TOPIC, stringify(this.messages));
+
+ this.messages = [];
+ }
+ }
+}
diff --git a/server/src/network/MessageReceiver.ts b/server/src/network/MessageReceiver.ts
new file mode 100644
index 0000000..fcac0a4
--- /dev/null
+++ b/server/src/network/MessageReceiver.ts
@@ -0,0 +1,22 @@
+import { MessageQueueProvider } from '@engine/network';
+import type { ServerMessage } from '.';
+
+export class ServerSocketMessageReceiver implements MessageQueueProvider {
+ private messages: ServerMessage[];
+
+ constructor() {
+ this.messages = [];
+ }
+
+ public addMessage(message: ServerMessage) {
+ this.messages.push(message);
+ }
+
+ public getNewMessages() {
+ return this.messages;
+ }
+
+ public clearMessages() {
+ this.messages = [];
+ }
+}
diff --git a/server/src/network/SessionInputSystem.ts b/server/src/network/SessionInputSystem.ts
new file mode 100644
index 0000000..44fba54
--- /dev/null
+++ b/server/src/network/SessionInputSystem.ts
@@ -0,0 +1,32 @@
+import { Game } from '@engine/Game';
+import { SessionManager } from '.';
+import { System } from '@engine/systems';
+import { BoundingBox, ComponentNames, Control } from '@engine/components';
+
+export class SessionInputSystem extends System {
+ private sessionManager: SessionManager;
+
+ constructor(sessionManager: SessionManager) {
+ super('SessionInputSystem');
+
+ this.sessionManager = sessionManager;
+ }
+
+ public update(_dt: number, game: Game) {
+ this.sessionManager.getSessions().forEach((sessionId) => {
+ const session = this.sessionManager.getSession(sessionId);
+
+ if (!session) return;
+
+ const { inputSystem } = session;
+ session.controllableEntities.forEach((entityId) => {
+ const entity = game.getEntity(entityId);
+ if (!entity) return;
+
+ if (entity.hasComponent(ComponentNames.Control)) {
+ inputSystem.handleInput(entity);
+ }
+ });
+ });
+ }
+}
diff --git a/server/src/network/SessionManager.ts b/server/src/network/SessionManager.ts
new file mode 100644
index 0000000..dbd4364
--- /dev/null
+++ b/server/src/network/SessionManager.ts
@@ -0,0 +1,33 @@
+import { Session, SessionManager } from '.';
+
+export class MemorySessionManager implements SessionManager {
+ private sessions: Map;
+
+ constructor() {
+ this.sessions = new Map();
+ }
+
+ public getSessions() {
+ return Array.from(this.sessions.keys());
+ }
+
+ public uniqueSessionId() {
+ return crypto.randomUUID();
+ }
+
+ public getSession(id: string) {
+ return this.sessions.get(id);
+ }
+
+ public putSession(id: string, session: Session) {
+ return this.sessions.set(id, session);
+ }
+
+ public numSessions() {
+ return this.sessions.size;
+ }
+
+ public removeSession(id: string) {
+ this.sessions.delete(id);
+ }
+}
diff --git a/server/src/network/index.ts b/server/src/network/index.ts
new file mode 100644
index 0000000..3cbf0ac
--- /dev/null
+++ b/server/src/network/index.ts
@@ -0,0 +1,29 @@
+import { Message } from '@engine/network';
+import { Input } from '@engine/systems';
+
+export * from './MessageProcessor';
+export * from './MessagePublisher';
+export * from './MessageReceiver';
+export * from './SessionManager';
+export * from './SessionInputSystem';
+
+export type SessionData = { sessionId: string };
+
+export type Session = {
+ sessionId: string;
+ controllableEntities: Set;
+ inputSystem: Input;
+};
+
+export interface ServerMessage extends Message {
+ sessionData: SessionData;
+}
+
+export interface SessionManager {
+ uniqueSessionId(): string;
+ getSession(id: string): Session | undefined;
+ getSessions(): string[];
+ putSession(id: string, session: Session): void;
+ removeSession(id: string): void;
+ numSessions(): number;
+}
diff --git a/server/src/server.ts b/server/src/server.ts
index 74d901b..575e916 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -1,37 +1,174 @@
-import { Game } from "../../engine/Game";
-import { Floor, Player } from "../../engine/entities";
-import { WallBounds, Physics, Collision } from "../../engine/systems";
-import { Miscellaneous } from "../../engine/config";
+import { Game } from '@engine/Game';
+import { Player } from '@engine/entities';
+import { Message, MessageType } from '@engine/network';
+import { Constants } from './constants';
+import {
+ ServerSocketMessageReceiver,
+ ServerSocketMessagePublisher,
+ SessionData,
+ ServerMessage,
+ Session,
+ SessionManager
+} from './network';
+import { parse } from '@engine/utils';
+import { Server, ServerWebSocket } from 'bun';
+import { Input } from '@engine/systems';
+import { Control, NetworkUpdateable } from '@engine/components';
+import { stringify } from '@engine/utils';
-const TICK_RATE = 60 / 1000;
+export class GameServer {
+ private server?: Server;
+ private game: Game;
+ private messageReceiver: ServerSocketMessageReceiver;
+ private messagePublisher: ServerSocketMessagePublisher;
+ private sessionManager: SessionManager;
-const game = new Game();
+ constructor(
+ game: Game,
+ messageReceiver: ServerSocketMessageReceiver,
+ messagePublisher: ServerSocketMessagePublisher,
+ sessionManager: SessionManager
+ ) {
+ this.game = game;
+ this.messageReceiver = messageReceiver;
+ this.messagePublisher = messagePublisher;
+ this.sessionManager = sessionManager;
+ }
-[
- new Physics(),
- new Collision({ width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }),
- new WallBounds(Miscellaneous.WIDTH),
-].forEach((system) => game.addSystem(system));
+ public serve() {
+ if (!this.server)
+ this.server = Bun.serve({
+ port: Constants.SERVER_PORT,
+ fetch: (req, srv) => this.fetchHandler(req, srv),
+ websocket: {
+ open: (ws) => this.openWebsocket(ws),
+ message: (ws, msg) => this.websocketMessage(ws, msg),
+ close: (ws) => this.closeWebsocket(ws)
+ }
+ });
-[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity));
+ this.messagePublisher.setServer(this.server);
-game.start();
-setInterval(() => {
- game.doGameLoop(performance.now());
-}, TICK_RATE);
+ console.log(`Listening on ${this.server.hostname}:${this.server.port}`);
+ }
-const server = Bun.serve<>({
- port: 8080,
- fetch(req, server) {
- server.upgrade(req, {
- data: {},
+ private websocketMessage(
+ websocket: ServerWebSocket,
+ message: string | Uint8Array
+ ) {
+ if (typeof message == 'string') {
+ const receivedMessage = parse(message);
+ receivedMessage.sessionData = websocket.data;
+
+ this.messageReceiver.addMessage(receivedMessage);
+ }
+ }
+
+ private closeWebsocket(websocket: ServerWebSocket) {
+ const { sessionId } = websocket.data;
+
+ const sessionEntities =
+ this.sessionManager.getSession(sessionId)!.controllableEntities;
+ this.sessionManager.removeSession(sessionId);
+
+ if (!sessionEntities) return;
+ sessionEntities.forEach((id) => this.game.removeEntity(id));
+
+ this.messagePublisher.addMessage({
+ type: MessageType.REMOVE_ENTITIES,
+ body: Array.from(sessionEntities)
});
- },
- websocket: {
- // handler called when a message is received
- async message(ws, message) {
- console.log(`Received ${message}`);
- },
- },
-});
-console.log(`Listening on localhost:${server.port}`);
+ }
+
+ private openWebsocket(websocket: ServerWebSocket) {
+ websocket.subscribe(Constants.GAME_TOPIC);
+
+ const { sessionId } = websocket.data;
+ if (this.sessionManager.getSession(sessionId)) {
+ return;
+ }
+
+ const newSession: Session = {
+ sessionId,
+ controllableEntities: new Set(),
+ inputSystem: new Input(sessionId)
+ };
+
+ const player = new Player();
+ player.addComponent(new Control(sessionId));
+ player.addComponent(new NetworkUpdateable());
+ this.game.addEntity(player);
+
+ newSession.controllableEntities.add(player.id);
+ this.sessionManager.putSession(sessionId, newSession);
+
+ const addCurrentEntities: Message[] = [
+ {
+ type: MessageType.NEW_ENTITIES,
+ body: Array.from(this.game.entities.values())
+ .filter((entity) => entity.id != player.id)
+ .map((entity) => {
+ return {
+ id: entity.id,
+ entityName: entity.name,
+ args: entity.serialize()
+ };
+ })
+ }
+ ];
+ websocket.sendText(stringify(addCurrentEntities));
+
+ const addNewPlayer: Message = {
+ type: MessageType.NEW_ENTITIES,
+ body: [
+ {
+ id: player.id,
+ entityName: player.name,
+ args: player.serialize()
+ }
+ ]
+ };
+ this.messagePublisher.addMessage(addNewPlayer);
+ }
+
+ private fetchHandler(req: Request, server: Server): Response {
+ const url = new URL(req.url);
+
+ const headers = new Headers();
+ headers.set('Access-Control-Allow-Origin', '*');
+
+ if (url.pathname == '/assign') {
+ if (this.sessionManager.numSessions() > Constants.MAX_PLAYERS)
+ return new Response('too many players', { headers, status: 400 });
+
+ const sessionId = crypto.randomUUID();
+ headers.set('Set-Cookie', `SessionId=${sessionId};`);
+
+ return new Response(sessionId, { headers });
+ }
+
+ const cookie = req.headers.get('cookie');
+ if (!cookie) {
+ return new Response('No session', { headers, status: 401 });
+ }
+
+ const sessionId = cookie.split(';').at(0)!.split('SessionId=').at(1);
+
+ if (url.pathname == '/game') {
+ server.upgrade(req, {
+ headers,
+ data: {
+ sessionId
+ }
+ });
+
+ return new Response('upgraded to ws', { headers });
+ }
+
+ if (url.pathname == '/me') {
+ return new Response(sessionId, { headers });
+ }
+
+ return new Response('Not found', { headers, status: 404 });
+ }
+}
diff --git a/server/tsconfig.json b/server/tsconfig.json
index 29f8aa0..52f0ddc 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -1,21 +1,27 @@
{
+ "extends": "../tsconfig.engine.json",
"compilerOptions": {
- "lib": ["ESNext"],
+ // add Bun type definitions
+ "types": ["bun-types"],
+
+ // enable latest features
+ "lib": ["esnext"],
"module": "esnext",
"target": "esnext",
+
+ // if TS 5.x+
"moduleResolution": "bundler",
- "moduleDetection": "force",
- "allowImportingTsExtensions": true,
- "strict": true,
- "downlevelIteration": true,
- "skipLibCheck": true,
- "jsx": "preserve",
- "allowSyntheticDefaultImports": true,
- "forceConsistentCasingInFileNames": true,
- "allowJs": true,
"noEmit": true,
- "types": [
- "bun-types" // add Bun global
- ]
+ "allowImportingTsExtensions": true,
+ "moduleDetection": "force",
+
+ "jsx": "react-jsx", // support JSX
+ "allowJs": true, // allow importing `.js` from `.ts`
+ "esModuleInterop": true, // allow default imports for CommonJS modules
+
+ // best practices
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true
}
}
diff --git a/tsconfig.engine.json b/tsconfig.engine.json
new file mode 100644
index 0000000..52482a2
--- /dev/null
+++ b/tsconfig.engine.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "@engine/*": ["./engine/*"],
+ "@engine/components": ["./engine/components"],
+ "@engine/config": ["./engine/config"],
+ "@engine/entities": ["./engine/entities"],
+ "@engine/interfaces": ["./engine/interfaces"],
+ "@engine/structures": ["./engine/structures"],
+ "@engine/systems": ["./engine/systems"],
+ "@engine/utils": ["./engine/utils"],
+ "@engine/network": ["./engine/network"]
+ }
+ }
+}