summaryrefslogtreecommitdiff
path: root/engine
diff options
context:
space:
mode:
Diffstat (limited to 'engine')
-rw-r--r--engine/Game.ts25
-rw-r--r--engine/components/BoundingBox.ts57
-rw-r--r--engine/components/Control.ts8
-rw-r--r--engine/components/Forces.ts2
-rw-r--r--engine/components/Sprite.ts6
-rw-r--r--engine/components/Velocity.ts10
-rw-r--r--engine/config/assets.ts10
-rw-r--r--engine/config/constants.ts8
-rw-r--r--engine/config/sprites.ts35
-rw-r--r--engine/entities/Entity.ts1
-rw-r--r--engine/entities/Floor.ts14
-rw-r--r--engine/entities/Player.ts15
-rw-r--r--engine/interfaces/LeaderBoardEntry.ts5
-rw-r--r--engine/interfaces/index.ts1
-rw-r--r--engine/structures/QuadTree.ts91
-rw-r--r--engine/systems/Collision.ts130
-rw-r--r--engine/systems/FacingDirection.ts25
-rw-r--r--engine/systems/Input.ts82
-rw-r--r--engine/systems/Physics.ts34
-rw-r--r--engine/systems/Render.ts57
-rw-r--r--engine/systems/System.ts1
-rw-r--r--engine/systems/WallBounds.ts27
-rw-r--r--engine/utils/index.ts1
-rw-r--r--engine/utils/normalizeVector.ts8
24 files changed, 338 insertions, 315 deletions
diff --git a/engine/Game.ts b/engine/Game.ts
index 3682fbd..07d06e8 100644
--- a/engine/Game.ts
+++ b/engine/Game.ts
@@ -12,6 +12,7 @@ export class Game {
public componentEntities: Map<string, Set<number>>;
constructor() {
+ this.lastTimeStamp = performance.now();
this.running = false;
this.systemOrder = [];
this.systems = new Map();
@@ -28,7 +29,7 @@ export class Game {
this.entities.set(entity.id, entity);
}
- public getEntity(id: number): Entity {
+ public getEntity(id: number): Entity | undefined {
return this.entities.get(id);
}
@@ -36,6 +37,18 @@ export class Game {
this.entities.delete(id);
}
+ public forEachEntityWithComponent(
+ componentName: string,
+ callback: (entity: Entity) => void,
+ ) {
+ this.componentEntities.get(componentName)?.forEach((entityId) => {
+ const entity = this.getEntity(entityId);
+ if (!entity) return;
+
+ callback(entity);
+ });
+ }
+
public addSystem(system: System) {
if (!this.systemOrder.includes(system.name)) {
this.systemOrder.push(system.name);
@@ -43,7 +56,7 @@ export class Game {
this.systems.set(system.name, system);
}
- public getSystem(name: string): System {
+ public getSystem(name: string): System | undefined {
return this.systems.get(name);
}
@@ -62,16 +75,16 @@ export class Game {
if (!this.componentEntities.has(component.name)) {
this.componentEntities.set(
component.name,
- new Set<number>([entity.id])
+ new Set<number>([entity.id]),
);
return;
}
- this.componentEntities.get(component.name).add(entity.id);
- })
+ this.componentEntities.get(component.name)?.add(entity.id);
+ }),
);
this.systemOrder.forEach((systemName) => {
- this.systems.get(systemName).update(dt, this);
+ this.systems.get(systemName)?.update(dt, this);
});
};
}
diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts
index 2b1d648..5e21b2f 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, normalizeVector } from "../utils";
+import { dotProduct, rotateVector } from "../utils";
export class BoundingBox extends Component {
public center: Coord2D;
@@ -15,10 +15,11 @@ export class BoundingBox extends Component {
this.rotation = rotation ?? 0;
}
+ // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
public isCollidingWith(box: BoundingBox): boolean {
const boxes = [this.getVertices(), box.getVertices()];
for (const poly of boxes) {
- for (let i = 0; i < poly.length; ++i) {
+ for (let i = 0; i < poly.length; i++) {
const [A, B] = [poly[i], poly[(i + 1) % poly.length]];
const normal: Coord2D = { x: B.y - A.y, y: A.x - B.x };
@@ -28,8 +29,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;
@@ -55,43 +56,29 @@ export class BoundingBox extends Component {
});
}
- private getAxes() {
- const corners: Coord2D[] = this.getVerticesRelativeToCenter();
- const axes: Coord2D[] = [];
-
- for (let i = 0; i < corners.length; ++i) {
- const [cornerA, cornerB] = [
- corners[i],
- corners[(i + 1) % corners.length],
- ].map((corner) => rotateVector(corner, this.rotation));
-
- axes.push(
- normalizeVector({
- x: cornerB.y - cornerA.y,
- y: -(cornerB.x - cornerA.x),
- })
- );
+ public getRotationInPiOfUnitCircle() {
+ let rads = this.rotation * (Math.PI / 180);
+ if (rads >= Math.PI) {
+ rads -= Math.PI;
}
-
- return axes;
+ return rads;
}
- private project(axis: Coord2D): [number, number] {
- const corners = this.getCornersRelativeToCenter();
- let [min, max] = [Infinity, -Infinity];
+ public getOutscribedBoxDims(): Dimension2D {
+ let rads = this.getRotationInPiOfUnitCircle();
+ const { width, height } = this.dimension;
- for (const corner of corners) {
- const rotated = rotateVector(corner, this.rotation);
- const translated = {
- x: rotated.x + this.center.x,
- y: rotated.y + this.center.y,
+ 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)),
};
- const projection = dotProduct(translated, axis);
-
- min = Math.min(projection, min);
- max = Math.max(projection, max);
}
- return [min, max];
+ 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)),
+ };
}
}
diff --git a/engine/components/Control.ts b/engine/components/Control.ts
index 094ef1c..1e782ee 100644
--- a/engine/components/Control.ts
+++ b/engine/components/Control.ts
@@ -1,7 +1,11 @@
-import { Component, ComponentNames } from ".";
+import { Component, ComponentNames, Velocity } from ".";
export class Control extends Component {
- constructor() {
+ public controlVelocity: Velocity;
+
+ constructor(controlVelocity: Velocity = new Velocity()) {
super(ComponentNames.Control);
+
+ this.controlVelocity = controlVelocity;
}
}
diff --git a/engine/components/Forces.ts b/engine/components/Forces.ts
index bf540a1..91ae1c1 100644
--- a/engine/components/Forces.ts
+++ b/engine/components/Forces.ts
@@ -1,4 +1,4 @@
-import type { Accel2D, Force2D } from "../interfaces";
+import type { Force2D } from "../interfaces";
import { Component } from "./Component";
import { ComponentNames } from ".";
diff --git a/engine/components/Sprite.ts b/engine/components/Sprite.ts
index 90e1389..bdb4982 100644
--- a/engine/components/Sprite.ts
+++ b/engine/components/Sprite.ts
@@ -17,7 +17,7 @@ export class Sprite extends Component {
spriteImgPos: Coord2D,
spriteImgDimensions: Dimension2D,
msPerFrame: number,
- numFrames: number
+ numFrames: number,
) {
super(ComponentNames.Sprite);
@@ -44,7 +44,7 @@ export class Sprite extends Component {
ctx.save();
ctx.translate(center.x, center.y);
- if (rotation != 0) {
+ if (rotation != undefined && rotation != 0) {
ctx.rotate(rotation * (Math.PI / 180));
}
ctx.translate(-center.x, -center.y);
@@ -56,7 +56,7 @@ export class Sprite extends Component {
ctx.drawImage(
this.sheet,
...this.getSpriteArgs(),
- ...this.getDrawArgs(drawArgs)
+ ...this.getDrawArgs(drawArgs),
);
if (tint) {
diff --git a/engine/components/Velocity.ts b/engine/components/Velocity.ts
index 119427d..068d8cd 100644
--- a/engine/components/Velocity.ts
+++ b/engine/components/Velocity.ts
@@ -6,10 +6,18 @@ export class Velocity extends Component {
public dCartesian: Velocity2D;
public dTheta: number;
- constructor(dCartesian: Velocity2D, dTheta: number) {
+ constructor(dCartesian: Velocity2D = { dx: 0, dy: 0 }, dTheta: number = 0) {
super(ComponentNames.Velocity);
this.dCartesian = dCartesian;
this.dTheta = dTheta;
}
+
+ public add(velocity?: Velocity) {
+ if (velocity) {
+ this.dCartesian.dx += velocity.dCartesian.dx;
+ this.dCartesian.dy += velocity.dCartesian.dy;
+ this.dTheta += velocity.dTheta;
+ }
+ }
}
diff --git a/engine/config/assets.ts b/engine/config/assets.ts
index 51a5303..173bab3 100644
--- a/engine/config/assets.ts
+++ b/engine/config/assets.ts
@@ -4,7 +4,7 @@ import { SPRITE_SPECS } from "./sprites";
export const IMAGES = new Map<string, HTMLImageElement>();
export const loadSpritesIntoImageElements = (
- spriteSpecs: Partial<SpriteSpec>[]
+ spriteSpecs: Partial<SpriteSpec>[],
): Promise<void>[] => {
const spritePromises: Promise<void>[] = [];
@@ -17,13 +17,13 @@ export const loadSpritesIntoImageElements = (
spritePromises.push(
new Promise((resolve) => {
img.onload = () => resolve();
- })
+ }),
);
}
if (spriteSpec.states) {
spritePromises.push(
- ...loadSpritesIntoImageElements(Object.values(spriteSpec.states))
+ ...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values())),
);
}
}
@@ -34,7 +34,9 @@ export const loadSpritesIntoImageElements = (
export const loadAssets = () =>
Promise.all([
...loadSpritesIntoImageElements(
- Array.from(SPRITE_SPECS.keys()).map((key) => SPRITE_SPECS.get(key))
+ Array.from(SPRITE_SPECS.keys()).map(
+ (key) => SPRITE_SPECS.get(key) as SpriteSpec,
+ ),
),
// TODO: Sound
]);
diff --git a/engine/config/constants.ts b/engine/config/constants.ts
index 9a3169b..3d536d3 100644
--- a/engine/config/constants.ts
+++ b/engine/config/constants.ts
@@ -11,12 +11,12 @@ export namespace KeyConstants {
};
export const ActionKeys: Map<Action, string[]> = Object.keys(
- KeyActions
+ KeyActions,
).reduce((acc: Map<Action, string[]>, key) => {
const action = KeyActions[key];
if (acc.has(action)) {
- acc.get(action).push(key);
+ acc.get(action)?.push(key);
return acc;
}
@@ -29,8 +29,8 @@ export namespace PhysicsConstants {
export const MAX_JUMP_TIME_MS = 150;
export const GRAVITY = 0.0075;
export const PLAYER_MOVE_VEL = 1;
- export const PLAYER_JUMP_ACC = -0.01;
- export const PLAYER_JUMP_INITIAL_VEL = -0.9;
+ export const PLAYER_JUMP_ACC = -0.008;
+ export const PLAYER_JUMP_INITIAL_VEL = -1;
}
export namespace Miscellaneous {
diff --git a/engine/config/sprites.ts b/engine/config/sprites.ts
index 18bec73..1f65c18 100644
--- a/engine/config/sprites.ts
+++ b/engine/config/sprites.ts
@@ -10,7 +10,7 @@ export interface SpriteSpec {
height: number;
frames: number;
msPerFrame: number;
- states?: Record<string | number, Partial<SpriteSpec>>;
+ states?: Map<string | number, Partial<SpriteSpec>>;
}
export const SPRITE_SPECS: Map<Sprites, Partial<SpriteSpec>> = new Map<
@@ -22,28 +22,27 @@ const floorSpriteSpec = {
height: 40,
frames: 3,
msPerFrame: 125,
- states: {},
+ states: new Map<number, Partial<SpriteSpec>>(),
};
-floorSpriteSpec.states = [40, 80, 120, 160].reduce((acc, cur) => {
- acc[cur] = {
- width: cur,
- sheet: `/assets/floor_tile_${cur}.png`,
- };
- return acc;
-}, {});
+[40, 80, 120, 160].forEach((width) => {
+ floorSpriteSpec.states.set(width, {
+ width,
+ sheet: `/assets/floor_tile_${width}.png`,
+ });
+});
SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec);
-SPRITE_SPECS.set(Sprites.COFFEE, {
+const coffeeSpriteSpec = {
msPerFrame: 100,
width: 60,
height: 45,
frames: 3,
- states: {
- LEFT: {
- sheet: "/assets/coffee_left.png",
- },
- RIGHT: {
- sheet: "/assets/coffee_right.png",
- },
- },
+ states: new Map<string, Partial<SpriteSpec>>(),
+};
+coffeeSpriteSpec.states.set("LEFT", {
+ sheet: "/assets/coffee_left.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 e57ccde..ca8d314 100644
--- a/engine/entities/Entity.ts
+++ b/engine/entities/Entity.ts
@@ -1,5 +1,4 @@
import type { Component } from "../components";
-import { ComponentNotFoundError } from "../exceptions";
export abstract class Entity {
private static ID = 0;
diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts
index d51badc..44587e6 100644
--- a/engine/entities/Floor.ts
+++ b/engine/entities/Floor.ts
@@ -4,26 +4,28 @@ import { TopCollidable } from "../components/TopCollidable";
import { Entity } from "../entities";
export class Floor extends Entity {
- private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(Sprites.FLOOR);
+ private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
+ Sprites.FLOOR,
+ ) as SpriteSpec;
constructor(width: number) {
super();
this.addComponent(
new Sprite(
- IMAGES.get(Floor.spriteSpec.states[width].sheet),
+ IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet),
{ x: 0, y: 0 },
{ width, height: Floor.spriteSpec.height },
Floor.spriteSpec.msPerFrame,
- Floor.spriteSpec.frames
- )
+ Floor.spriteSpec.frames,
+ ),
);
this.addComponent(
new BoundingBox(
{ x: 300, y: 300 },
- { width, height: Floor.spriteSpec.height }
- )
+ { width, height: Floor.spriteSpec.height },
+ ),
);
this.addComponent(new TopCollidable());
diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts
index 0ba5a41..45d7500 100644
--- a/engine/entities/Player.ts
+++ b/engine/entities/Player.ts
@@ -14,14 +14,15 @@ import {
Mass,
Moment,
} from "../components";
-import { PhysicsConstants } from "../config";
import { Direction } from "../interfaces";
export class Player extends Entity {
private static MASS: number = 10;
private static MOI: number = 1000;
- private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(Sprites.COFFEE);
+ private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
+ Sprites.COFFEE,
+ ) as SpriteSpec;
constructor() {
super();
@@ -30,8 +31,8 @@ export class Player extends Entity {
new BoundingBox(
{ x: 300, y: 100 },
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
- 0
- )
+ 0,
+ ),
);
this.addComponent(new Velocity({ dx: 0, dy: 0 }, 0));
@@ -54,12 +55,12 @@ export class Player extends Entity {
const [leftSprite, rightSprite] = [Direction.LEFT, Direction.RIGHT].map(
(direction) =>
new Sprite(
- IMAGES.get(Player.spriteSpec.states[direction].sheet),
+ IMAGES.get(Player.spriteSpec.states?.get(direction)?.sheet as string),
{ 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));
diff --git a/engine/interfaces/LeaderBoardEntry.ts b/engine/interfaces/LeaderBoardEntry.ts
deleted file mode 100644
index 1b1e7b3..0000000
--- a/engine/interfaces/LeaderBoardEntry.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface LeaderBoardEntry {
- name: string;
- score: number;
- avatar: string;
-}
diff --git a/engine/interfaces/index.ts b/engine/interfaces/index.ts
index 0398abd..8cdf4d8 100644
--- a/engine/interfaces/index.ts
+++ b/engine/interfaces/index.ts
@@ -1,4 +1,3 @@
-export * from "./LeaderBoardEntry";
export * from "./Vec2";
export * from "./Draw";
export * from "./Direction";
diff --git a/engine/structures/QuadTree.ts b/engine/structures/QuadTree.ts
index 7913e59..d1ff3b1 100644
--- a/engine/structures/QuadTree.ts
+++ b/engine/structures/QuadTree.ts
@@ -1,6 +1,4 @@
import type { Coord2D, Dimension2D } from "../interfaces";
-import { ComponentNames, BoundingBox } from "../components";
-import { Entity } from "../entities";
interface BoxedEntry {
id: number;
@@ -30,21 +28,26 @@ export class QuadTree {
dimension: Dimension2D,
maxLevels: number,
splitThreshold: number,
- level?: number
+ level?: number,
) {
- this.children = [];
+ this.children = new Map<Quadrant, QuadTree>();
this.objects = [];
this.maxLevels = maxLevels;
this.splitThreshold = splitThreshold;
this.level = level ?? 0;
+
+ this.topLeft = topLeft;
+ this.dimension = dimension;
}
public insert(id: number, dimension: Dimension2D, center: Coord2D): void {
+ const box: BoxedEntry = { id, center, dimension };
if (this.hasChildren()) {
- this.getIndices(boundingBox).forEach((i) =>
- this.children[i].insert(id, dimension, center)
- );
+ this.getQuadrants(box).forEach((quadrant) => {
+ const quadrantBox = this.children.get(quadrant);
+ quadrantBox?.insert(id, dimension, center);
+ });
return;
}
@@ -74,9 +77,10 @@ export class QuadTree {
if (this.hasChildren()) {
this.getQuadrants(boxedEntry).forEach((quadrant) => {
- this.children
- .get(quadrant)
- .getNeighborIds(boxedEntry)
+ const quadrantBox = this.children.get(quadrant);
+
+ quadrantBox
+ ?.getNeighborIds(boxedEntry)
.forEach((id) => neighbors.push(id));
});
}
@@ -88,15 +92,17 @@ export class QuadTree {
const halfWidth = this.dimension.width / 2;
const halfHeight = this.dimension.height / 2;
- [
- [Quadrant.I, { x: this.topLeft.x + halfWidth, y: this.topLeft.y }],
- [Quadrant.II, { ...this.topLeft }],
- [Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }],
+ (
[
- Quadrant.IV,
- { x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight },
- ],
- ].forEach(([quadrant, pos]) => {
+ [Quadrant.I, { x: this.topLeft.x + halfWidth, y: this.topLeft.y }],
+ [Quadrant.II, { ...this.topLeft }],
+ [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]]
+ ).forEach(([quadrant, pos]) => {
this.children.set(
quadrant,
new QuadTree(
@@ -104,34 +110,48 @@ export class QuadTree {
{ width: halfWidth, height: halfHeight },
this.maxLevels,
this.splitThreshold,
- this.level + 1
- )
+ this.level + 1,
+ ),
);
});
}
- private getQuandrants(boxedEntry: BoxedEntry): Quadrant[] {
+ private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] {
const treeCenter: Coord2D = {
x: this.topLeft.x + this.dimension.width / 2,
y: this.topLeft.y + this.dimension.height / 2,
};
- return [
- [Quadrant.I, (x, y) => x >= treeCenter.x && y < treeCenter.y],
- [Quadrant.II, (x, y) => x < treeCenter.x && y < treeCenter.y],
- [Quadrant.III, (x, y) => x < treeCenter.x && y >= treeCenter.y],
- [Quadrant.IV, (x, y) => x >= treeCenter.x && y >= treeCenter.y],
- ]
+ return (
+ [
+ [
+ Quadrant.I,
+ (x: number, y: number) => x >= treeCenter.x && y < treeCenter.y,
+ ],
+ [
+ Quadrant.II,
+ (x: number, y: number) => x < treeCenter.x && y < treeCenter.y,
+ ],
+ [
+ Quadrant.III,
+ (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]]
+ )
.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);
}
@@ -139,9 +159,12 @@ export class QuadTree {
private realignObjects(): void {
this.objects.forEach((boxedEntry) => {
this.getQuadrants(boxedEntry).forEach((direction) => {
- this.children
- .get(direction)
- .insert(boxedEntry.id, boxedEntry.dimension, boxedEntry.center);
+ const quadrant = this.children.get(direction);
+ quadrant?.insert(
+ boxedEntry.id,
+ boxedEntry.dimension,
+ boxedEntry.center,
+ );
});
});
@@ -149,6 +172,6 @@ export class QuadTree {
}
private hasChildren() {
- return this.children && this.children.length > 0;
+ return this.children && this.children.size > 0;
}
}
diff --git a/engine/systems/Collision.ts b/engine/systems/Collision.ts
index 846a95a..2bba03b 100644
--- a/engine/systems/Collision.ts
+++ b/engine/systems/Collision.ts
@@ -5,7 +5,7 @@ import {
ComponentNames,
Jump,
Velocity,
- Moment,
+ Forces,
} from "../components";
import { Game } from "../Game";
import { PhysicsConstants } from "../config";
@@ -30,60 +30,64 @@ export class Collision extends System {
{ x: 0, y: 0 },
screenDimensions,
Collision.QUADTREE_MAX_LEVELS,
- Collision.QUADTREE_SPLIT_THRESHOLD
+ Collision.QUADTREE_SPLIT_THRESHOLD,
);
}
- public update(dt: number, game: Game) {
+ public update(_dt: number, game: Game) {
// rebuild the quadtree
this.quadTree.clear();
const entitiesToAddToQuadtree: Entity[] = [];
Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) =>
- game.componentEntities.get(componentName)
- ).forEach((entityIds?: Set<number>) =>
- entityIds?.forEach((id) => {
- const entity = game.entities.get(id);
- if (!entity.hasComponent(ComponentNames.BoundingBox)) {
- return;
- }
- entitiesToAddToQuadtree.push(entity);
- })
+ game.componentEntities.get(componentName),
+ ).forEach(
+ (entityIds?: Set<number>) =>
+ entityIds?.forEach((id) => {
+ const entity = game.entities.get(id);
+ if (!entity || !entity.hasComponent(ComponentNames.BoundingBox)) {
+ return;
+ }
+ entitiesToAddToQuadtree.push(entity);
+ }),
);
entitiesToAddToQuadtree.forEach((entity) => {
const boundingBox = entity.getComponent<BoundingBox>(
- ComponentNames.BoundingBox
+ ComponentNames.BoundingBox,
);
- this.quadTree.insert(
- entity.id,
- boundingBox.dimension,
- boundingBox.center
- );
+ let dimension = { ...boundingBox.dimension };
+ if (boundingBox.rotation != 0) {
+ dimension = boundingBox.getOutscribedBoxDims();
+ }
+
+ this.quadTree.insert(entity.id, dimension, boundingBox.center);
});
// find colliding entities and perform collisions
const collidingEntities = this.getCollidingEntities(
entitiesToAddToQuadtree,
- game.entities
+ game,
);
collidingEntities.forEach(([entityAId, entityBId]) => {
const [entityA, entityB] = [entityAId, entityBId].map((id) =>
- game.entities.get(id)
+ game.entities.get(id),
);
- this.performCollision(entityA, entityB);
+ if (entityA && entityB) {
+ this.performCollision(entityA, entityB);
+ }
});
}
private performCollision(entityA: Entity, entityB: Entity) {
const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map(
- (entity) => entity.getComponent<BoundingBox>(ComponentNames.BoundingBox)
+ (entity) => entity.getComponent<BoundingBox>(ComponentNames.BoundingBox),
);
- let velocity: Velocity;
+ let velocity = new Velocity();
if (entityA.hasComponent(ComponentNames.Velocity)) {
velocity = entityA.getComponent<Velocity>(ComponentNames.Velocity);
}
@@ -92,17 +96,16 @@ export class Collision extends System {
entityA.hasComponent(ComponentNames.Collide) &&
entityB.hasComponent(ComponentNames.TopCollidable) &&
entityABoundingBox.center.y <= entityBBoundingBox.center.y &&
- velocity &&
velocity.dCartesian.dy >= 0 // don't apply "floor" logic when coming through the bottom
) {
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.`,
);
}
// remove previous velocity in the y axis
- velocity.dCartesian.dy = 0;
+ if (velocity) velocity.dCartesian.dy = 0;
// apply normal force
if (entityA.hasComponent(ComponentNames.Gravity)) {
@@ -110,7 +113,8 @@ export class Collision extends System {
const F_n = -mass * PhysicsConstants.GRAVITY;
entityA.getComponent<Forces>(ComponentNames.Forces).forces.push({
- fCartesian: { fy: F_n },
+ fCartesian: { fy: F_n, fx: 0 },
+ torque: 0,
});
}
@@ -128,31 +132,35 @@ export class Collision extends System {
private getCollidingEntities(
collidableEntities: Entity[],
- entityMap: Map<number, Entity>
+ game: Game,
): [number, number][] {
- const collidingEntityIds: [number, number] = [];
+ const collidingEntityIds: [number, number][] = [];
for (const entity of collidableEntities) {
const boundingBox = entity.getComponent<BoundingBox>(
- ComponentNames.BoundingBox
+ ComponentNames.BoundingBox,
);
- this.quadTree
+ const neighborIds = this.quadTree
.getNeighborIds({
id: entity.id,
dimension: boundingBox.dimension,
center: boundingBox.center,
})
- .filter((neighborId) => neighborId != entity.id)
- .forEach((neighborId) => {
- const neighborBoundingBox = entityMap
- .get(neighborId)
- .getComponent<BoundingBox>(ComponentNames.BoundingBox);
-
- if (boundingBox.isCollidingWith(neighborBoundingBox)) {
- collidingEntityIds.push([entity.id, neighborId]);
- }
- });
+ .filter((neighborId) => neighborId != entity.id);
+
+ neighborIds.forEach((neighborId) => {
+ const neighbor = game.getEntity(neighborId);
+ if (!neighbor) return;
+
+ const neighborBoundingBox = neighbor.getComponent<BoundingBox>(
+ ComponentNames.BoundingBox,
+ );
+
+ if (boundingBox.isCollidingWith(neighborBoundingBox)) {
+ collidingEntityIds.push([entity.id, neighborId]);
+ }
+ });
}
return collidingEntityIds;
@@ -161,55 +169,45 @@ export class Collision extends System {
// ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ
private getDyToPushOutOfFloor(
entityBoundingBox: BoundingBox,
- floorBoundingBox: BoundingBox
+ floorBoundingBox: BoundingBox,
): number {
const {
- rotation,
- center: { x, y },
dimension: { width, height },
+ center: { x },
} = entityBoundingBox;
- let rads = rotation * (Math.PI / 180);
- if (rads >= Math.PI) {
- rads -= Math.PI; // we have symmetry so we can skip two cases
- }
+ const outScribedRectangle = entityBoundingBox.getOutscribedBoxDims();
- let boundedCollisionX = 0; // bounded x on the surface from width
- let clippedX = 0; // x coordinate of the vertex below the surface
- let outScribedRectangleHeight, dy, dx;
-
- if (rads <= Math.PI / 2) {
- dx = (width * Math.cos(rads) - height * Math.sin(rads)) / 2;
- outScribedRectangleHeight =
- width * Math.sin(rads) + height * Math.cos(rads);
- } else if (rads <= Math.PI) {
+ let rads = entityBoundingBox.getRotationInPiOfUnitCircle();
+ let dx = (width * Math.cos(rads) - height * Math.sin(rads)) / 2;
+
+ if (rads >= Math.PI / 2) {
rads -= Math.PI / 2;
dx = (height * Math.cos(rads) - width * Math.sin(rads)) / 2;
- outScribedRectangleHeight =
- width * Math.cos(rads) + height * Math.sin(rads);
}
+ const clippedX = x + dx; // x coordinate of the vertex below the surface (if existant)
+ let boundedCollisionX = 0; // bounded x on the surface from width
+
if (x >= floorBoundingBox.center.x) {
- clippedX = x + dx;
boundedCollisionX = Math.min(
floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2,
- clippedX
+ clippedX,
);
return (
- outScribedRectangleHeight / 2 -
+ outScribedRectangle.height / 2 -
Math.max((clippedX - boundedCollisionX) * Math.tan(rads), 0)
);
}
- clippedX = x - dx;
boundedCollisionX = Math.max(
floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2,
- clippedX
+ clippedX,
);
return (
- outScribedRectangleHeight / 2 -
- Math.max((boundedCollisionX - clippedX) * Math.tan(rads), 0)
+ outScribedRectangle.height / 2 -
+ Math.max((boundedCollisionX - clippedX) * Math.tan(Math.PI / 2 - rads), 0)
);
}
}
diff --git a/engine/systems/FacingDirection.ts b/engine/systems/FacingDirection.ts
index c6513ac..4426ab6 100644
--- a/engine/systems/FacingDirection.ts
+++ b/engine/systems/FacingDirection.ts
@@ -2,9 +2,9 @@ import {
ComponentNames,
Velocity,
FacingDirection as FacingDirectionComponent,
+ Control,
} from "../components";
import { Game } from "../Game";
-import type { Entity } from "../entities";
import { System, SystemNames } from "./";
export class FacingDirection extends System {
@@ -13,24 +13,31 @@ export class FacingDirection extends System {
}
public update(_dt: number, game: Game) {
- game.componentEntities
- .get(ComponentNames.FacingDirection)
- ?.forEach((entityId) => {
- const entity = game.entities.get(entityId);
+ game.forEachEntityWithComponent(
+ ComponentNames.FacingDirection,
+ (entity) => {
if (!entity.hasComponent(ComponentNames.Velocity)) {
return;
}
+ const totalVelocity: Velocity = new Velocity();
+ const control = entity.getComponent<Control>(ComponentNames.Control);
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
+ totalVelocity.add(velocity);
+ if (control) {
+ totalVelocity.add(control.controlVelocity);
+ }
+
const facingDirection = entity.getComponent<FacingDirectionComponent>(
- ComponentNames.FacingDirection
+ ComponentNames.FacingDirection,
);
- if (velocity.dCartesian.dx > 0) {
+ if (totalVelocity.dCartesian.dx > 0) {
entity.addComponent(facingDirection.facingRightSprite);
- } else if (velocity.dCartesian.dx < 0) {
+ } else if (totalVelocity.dCartesian.dx < 0) {
entity.addComponent(facingDirection.facingLeftSprite);
}
- });
+ },
+ );
}
}
diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts
index 4272ec8..4aa9844 100644
--- a/engine/systems/Input.ts
+++ b/engine/systems/Input.ts
@@ -1,21 +1,16 @@
import {
Jump,
Forces,
- Acceleration,
ComponentNames,
Velocity,
Mass,
+ Control,
} from "../components";
import { Game } from "../Game";
import { KeyConstants, PhysicsConstants } from "../config";
-import type { Entity } from "../entities";
import { Action } from "../interfaces";
import { System, SystemNames } from "./";
-/**
- * TODO: Make velocities reset on each game loop (as similar to acceleration)
- * - Then, we can add / remove velocity on update instead of just setting it and praying it's not modified externally
- */
export class Input extends System {
private keys: Set<string>;
private actionTimeStamps: Map<Action, number>;
@@ -23,7 +18,7 @@ export class Input extends System {
constructor() {
super(SystemNames.Input);
- this.keys = new Set<number>();
+ this.keys = new Set<string>();
this.actionTimeStamps = new Map<Action, number>();
}
@@ -35,51 +30,52 @@ export class Input extends System {
this.keys.delete(key);
}
- private hasSomeKey(keys: string[]): boolean {
- return keys.some((key) => this.keys.has(key));
+ private hasSomeKey(keys?: string[]): boolean {
+ if (keys) {
+ return keys.some((key) => this.keys.has(key));
+ }
+ return false;
}
- public update(dt: number, game: Game) {
- game.componentEntities.get(ComponentNames.Control)?.forEach((entityId) => {
- const entity = game.entities.get(entityId);
- if (!entity.hasComponent(ComponentNames.Velocity)) {
- return;
- }
-
- const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
+ public update(_dt: number, game: Game) {
+ game.forEachEntityWithComponent(ComponentNames.Control, (entity) => {
+ const control = entity.getComponent<Control>(ComponentNames.Control);
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
- velocity.dCartesian.dx = PhysicsConstants.PLAYER_MOVE_VEL;
- } else if (
- this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))
- ) {
- velocity.dCartesian.dx = -PhysicsConstants.PLAYER_MOVE_VEL;
- } else {
- velocity.dCartesian.dx = 0;
+ control.controlVelocity.dCartesian.dx +=
+ PhysicsConstants.PLAYER_MOVE_VEL;
}
- });
- game.componentEntities.get(ComponentNames.Jump)?.forEach((entityId) => {
- const entity = game.entities.get(entityId);
- const jump = entity.getComponent<Jump>(ComponentNames.Jump);
- const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
+ if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
+ control.controlVelocity.dCartesian.dx +=
+ -PhysicsConstants.PLAYER_MOVE_VEL;
+ }
- if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) {
- if (jump.canJump) {
- this.actionTimeStamps.set(Action.JUMP, performance.now());
+ if (entity.hasComponent(ComponentNames.Jump)) {
+ const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
+ const jump = entity.getComponent<Jump>(ComponentNames.Jump);
- velocity.dCartesian.dy = PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
- jump.canJump = false;
- }
+ 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) <
- PhysicsConstants.MAX_JUMP_TIME_MS
- ) {
- const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
- entity.getComponent<Forces>(ComponentNames.Forces)?.forces.push({
- fCartesian: { fy: mass * PhysicsConstants.PLAYER_JUMP_ACC },
- });
+ if (
+ performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
+ PhysicsConstants.MAX_JUMP_TIME_MS
+ ) {
+ const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
+ entity.getComponent<Forces>(ComponentNames.Forces)?.forces.push({
+ fCartesian: {
+ fy: mass * PhysicsConstants.PLAYER_JUMP_ACC,
+ fx: 0,
+ },
+ torque: 0,
+ });
+ }
}
}
});
diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts
index 7edeb41..38962a6 100644
--- a/engine/systems/Physics.ts
+++ b/engine/systems/Physics.ts
@@ -1,6 +1,5 @@
import { System, SystemNames } from ".";
import {
- Acceleration,
BoundingBox,
ComponentNames,
Forces,
@@ -8,9 +7,10 @@ import {
Velocity,
Mass,
Jump,
+ Moment,
+ Control,
} from "../components";
import { PhysicsConstants } from "../config";
-import type { Entity } from "../entities";
import type { Force2D } from "../interfaces";
import { Game } from "../Game";
@@ -20,14 +20,12 @@ export class Physics extends System {
}
public update(dt: number, game: Game): void {
- game.componentEntities.get(ComponentNames.Forces)?.forEach((entityId) => {
- const entity = game.entities.get(entityId);
-
+ game.forEachEntityWithComponent(ComponentNames.Forces, (entity) => {
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
const forces = entity.getComponent<Forces>(ComponentNames.Forces).forces;
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
const inertia = entity.getComponent<Moment>(
- ComponentNames.Moment
+ ComponentNames.Moment,
).inertia;
// F_g = mg, applied only until terminal velocity is reached
@@ -37,7 +35,9 @@ export class Physics extends System {
forces.push({
fCartesian: {
fy: mass * PhysicsConstants.GRAVITY,
+ fx: 0,
},
+ torque: 0,
});
}
}
@@ -51,7 +51,7 @@ export class Physics extends System {
},
torque: accum.torque + (torque ?? 0),
}),
- { fCartesian: { fx: 0, fy: 0 }, torque: 0 }
+ { fCartesian: { fx: 0, fy: 0 }, torque: 0 },
);
// integrate accelerations
@@ -62,6 +62,7 @@ export class Physics extends System {
velocity.dCartesian.dx += ddx * dt;
velocity.dCartesian.dy += ddy * dt;
velocity.dTheta += (sumOfForces.torque * dt) / inertia;
+
// clear the forces
entity.getComponent<Forces>(ComponentNames.Forces).forces = [];
@@ -71,11 +72,17 @@ export class Physics extends System {
}
});
- game.componentEntities.get(ComponentNames.Velocity)?.forEach((entityId) => {
- const entity = game.entities.get(entityId);
- const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
+ game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => {
+ const velocity: Velocity = new Velocity();
+ const control = entity.getComponent<Control>(ComponentNames.Control);
+
+ velocity.add(entity.getComponent<Velocity>(ComponentNames.Velocity));
+ if (control) {
+ velocity.add(control.controlVelocity);
+ }
+
const boundingBox = entity.getComponent<BoundingBox>(
- ComponentNames.BoundingBox
+ ComponentNames.BoundingBox,
);
// integrate velocity
@@ -86,6 +93,11 @@ export class Physics extends System {
(boundingBox.rotation < 0
? 360 + boundingBox.rotation
: boundingBox.rotation) % 360;
+
+ // clear the control velocity
+ if (control) {
+ control.controlVelocity = new Velocity();
+ }
});
}
}
diff --git a/engine/systems/Render.ts b/engine/systems/Render.ts
index b5479e1..9bb4091 100644
--- a/engine/systems/Render.ts
+++ b/engine/systems/Render.ts
@@ -1,8 +1,6 @@
import { System, SystemNames } from ".";
import { BoundingBox, ComponentNames, Sprite } from "../components";
-import type { Entity } from "../entities";
import { Game } from "../Game";
-import type { DrawArgs } from "../interfaces";
import { clamp } from "../utils";
export class Render extends System {
@@ -16,39 +14,36 @@ export class Render extends System {
public update(dt: number, game: Game) {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
- game.componentEntities.get(ComponentNames.Sprite)?.forEach((entityId) => {
- const entity = game.entities.get(entityId);
+ game.forEachEntityWithComponent(ComponentNames.Sprite, (entity) => {
const sprite = entity.getComponent<Sprite>(ComponentNames.Sprite);
sprite.update(dt);
- let drawArgs: DrawArgs;
- if (entity.hasComponent(ComponentNames.BoundingBox)) {
- const boundingBox = entity.getComponent<BoundingBox>(
- ComponentNames.BoundingBox
- );
-
- // don't render if we're outside the screen
- if (
- clamp(
- boundingBox.center.y,
- -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
- ) != boundingBox.center.x
- ) {
- return;
- }
-
- drawArgs = {
- center: boundingBox.center,
- dimension: boundingBox.dimension,
- rotation: boundingBox.rotation,
- };
+ const boundingBox = entity.getComponent<BoundingBox>(
+ ComponentNames.BoundingBox,
+ );
+
+ // don't render if we're outside the screen
+ if (
+ clamp(
+ boundingBox.center.y,
+ -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,
+ ) != boundingBox.center.x
+ ) {
+ return;
}
+
+ const drawArgs = {
+ center: boundingBox.center,
+ dimension: boundingBox.dimension,
+ rotation: boundingBox.rotation,
+ };
+
sprite.draw(this.ctx, drawArgs);
});
}
diff --git a/engine/systems/System.ts b/engine/systems/System.ts
index 1aad285..8b00dc5 100644
--- a/engine/systems/System.ts
+++ b/engine/systems/System.ts
@@ -1,4 +1,3 @@
-import { Entity } from "../entities";
import { Game } from "../Game";
export abstract class System {
diff --git a/engine/systems/WallBounds.ts b/engine/systems/WallBounds.ts
index 6ea2267..a0d4a9c 100644
--- a/engine/systems/WallBounds.ts
+++ b/engine/systems/WallBounds.ts
@@ -14,23 +14,16 @@ export class WallBounds extends System {
}
public update(_dt: number, game: Game) {
- game.componentEntities
- .get(ComponentNames.WallBounded)
- ?.forEach((entityId) => {
- const entity = game.entities.get(entityId);
- if (!entity.hasComponent(ComponentNames.BoundingBox)) {
- return;
- }
+ game.forEachEntityWithComponent(ComponentNames.WallBounded, (entity) => {
+ const boundingBox = entity.getComponent<BoundingBox>(
+ ComponentNames.BoundingBox,
+ );
- const boundingBox = entity.getComponent<BoundingBox>(
- ComponentNames.BoundingBox
- );
-
- boundingBox.center.x = clamp(
- boundingBox.center.x,
- boundingBox.dimension.width / 2,
- this.screenWidth - boundingBox.dimension.width / 2
- );
- });
+ boundingBox.center.x = clamp(
+ boundingBox.center.x,
+ boundingBox.dimension.width / 2,
+ this.screenWidth - boundingBox.dimension.width / 2,
+ );
+ });
}
}
diff --git a/engine/utils/index.ts b/engine/utils/index.ts
index 4e465c2..82a0d05 100644
--- a/engine/utils/index.ts
+++ b/engine/utils/index.ts
@@ -1,4 +1,3 @@
export * from "./rotateVector";
-export * from "./normalizeVector";
export * from "./dotProduct";
export * from "./clamp";
diff --git a/engine/utils/normalizeVector.ts b/engine/utils/normalizeVector.ts
deleted file mode 100644
index e6dfd7f..0000000
--- a/engine/utils/normalizeVector.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { Coord2D } from "../interfaces";
-
-export const normalizeVector = (vector: Coord2D): Coord2D => {
- const { x, y } = vector;
- const length = Math.sqrt(x * x + y * y);
-
- return { x: x / length, y: y / length };
-};