the great engine refactor

This commit is contained in:
Elizabeth Hunt 2023-08-12 13:49:16 -06:00
parent b67ffb57c1
commit c6e9baa000
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
29 changed files with 345 additions and 334 deletions

View File

@ -17,11 +17,6 @@ export class JumpStorm {
this.game = new Game(); this.game = new Game();
this.socket = new WebSocket("ws://localhost:8080"); this.socket = new WebSocket("ws://localhost:8080");
this.socket.onopen = () => {
this.socket.send("gaming");
console.log("OPENED SOCKET");
};
[ [
this.createInputSystem(), this.createInputSystem(),
new FacingDirection(), new FacingDirection(),
@ -32,7 +27,7 @@ export class JumpStorm {
].forEach((system) => this.game.addSystem(system)); ].forEach((system) => this.game.addSystem(system));
[new Floor(160), new Player()].forEach((entity) => [new Floor(160), new Player()].forEach((entity) =>
this.game.addEntity(entity) this.game.addEntity(entity),
); );
} }

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { loadAssets } from "@engine/config"; import { loadAssets } from "@engine/config";
import { JumpStorm} from "../Jumpstorm"; import { JumpStorm } from "../JumpStorm";
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D; let ctx: CanvasRenderingContext2D;

View File

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { type LeaderBoardEntry } from "@engine/interfaces";
import LeaderBoardCard from "./LeaderBoardCard.svelte"; import LeaderBoardCard from "./LeaderBoardCard.svelte";
const MAX_ENTRIES = 8; const MAX_ENTRIES = 8;
export let entries: LeaderBoardEntry[] = []; export let entries: { name: string, score: number }[] = [];
</script> </script>
<div class="leaderboard"> <div class="leaderboard">

View File

@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import { type LeaderBoardEntry } from "@engine/interfaces"; export let entry = {
export let entry: LeaderBoardEntry = {
name: "simponic", name: "simponic",
score: 100, score: 100,
}; };

View File

@ -12,6 +12,7 @@ export class Game {
public componentEntities: Map<string, Set<number>>; public componentEntities: Map<string, Set<number>>;
constructor() { constructor() {
this.lastTimeStamp = performance.now();
this.running = false; this.running = false;
this.systemOrder = []; this.systemOrder = [];
this.systems = new Map(); this.systems = new Map();
@ -28,7 +29,7 @@ export class Game {
this.entities.set(entity.id, entity); this.entities.set(entity.id, entity);
} }
public getEntity(id: number): Entity { public getEntity(id: number): Entity | undefined {
return this.entities.get(id); return this.entities.get(id);
} }
@ -36,6 +37,18 @@ export class Game {
this.entities.delete(id); 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) { public addSystem(system: System) {
if (!this.systemOrder.includes(system.name)) { if (!this.systemOrder.includes(system.name)) {
this.systemOrder.push(system.name); this.systemOrder.push(system.name);
@ -43,7 +56,7 @@ export class Game {
this.systems.set(system.name, system); this.systems.set(system.name, system);
} }
public getSystem(name: string): System { public getSystem(name: string): System | undefined {
return this.systems.get(name); return this.systems.get(name);
} }
@ -62,16 +75,16 @@ export class Game {
if (!this.componentEntities.has(component.name)) { if (!this.componentEntities.has(component.name)) {
this.componentEntities.set( this.componentEntities.set(
component.name, component.name,
new Set<number>([entity.id]) new Set<number>([entity.id]),
); );
return; return;
} }
this.componentEntities.get(component.name).add(entity.id); this.componentEntities.get(component.name)?.add(entity.id);
}) }),
); );
this.systemOrder.forEach((systemName) => { this.systemOrder.forEach((systemName) => {
this.systems.get(systemName).update(dt, this); this.systems.get(systemName)?.update(dt, this);
}); });
}; };
} }

View File

@ -1,6 +1,6 @@
import { Component, ComponentNames } from "."; import { Component, ComponentNames } from ".";
import type { Coord2D, Dimension2D } from "../interfaces"; import type { Coord2D, Dimension2D } from "../interfaces";
import { dotProduct, rotateVector, normalizeVector } from "../utils"; import { dotProduct, rotateVector } from "../utils";
export class BoundingBox extends Component { export class BoundingBox extends Component {
public center: Coord2D; public center: Coord2D;
@ -15,10 +15,11 @@ export class BoundingBox extends Component {
this.rotation = rotation ?? 0; this.rotation = rotation ?? 0;
} }
// https://en.wikipedia.org/wiki/Hyperplane_separation_theorem
public isCollidingWith(box: BoundingBox): boolean { public isCollidingWith(box: BoundingBox): boolean {
const boxes = [this.getVertices(), box.getVertices()]; const boxes = [this.getVertices(), box.getVertices()];
for (const poly of boxes) { 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 [A, B] = [poly[i], poly[(i + 1) % poly.length]];
const normal: Coord2D = { x: B.y - A.y, y: A.x - B.x }; 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); const projection = dotProduct(normal, vertex);
return [Math.min(min, projection), Math.max(max, projection)]; return [Math.min(min, projection), Math.max(max, projection)];
}, },
[Infinity, -Infinity] [Infinity, -Infinity],
) ),
); );
if (maxThis < minBox || maxBox < minThis) return false; if (maxThis < minBox || maxBox < minThis) return false;
@ -55,43 +56,29 @@ export class BoundingBox extends Component {
}); });
} }
private getAxes() { public getRotationInPiOfUnitCircle() {
const corners: Coord2D[] = this.getVerticesRelativeToCenter(); let rads = this.rotation * (Math.PI / 180);
const axes: Coord2D[] = []; if (rads >= Math.PI) {
rads -= Math.PI;
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),
})
);
} }
return rads;
return axes;
} }
private project(axis: Coord2D): [number, number] { public getOutscribedBoxDims(): Dimension2D {
const corners = this.getCornersRelativeToCenter(); let rads = this.getRotationInPiOfUnitCircle();
let [min, max] = [Infinity, -Infinity]; const { width, height } = this.dimension;
for (const corner of corners) { if (rads <= Math.PI / 2) {
const rotated = rotateVector(corner, this.rotation); return {
const translated = { width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)),
x: rotated.x + this.center.x, height: Math.abs(width * Math.sin(rads) + height * Math.cos(rads)),
y: rotated.y + this.center.y,
}; };
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)),
};
} }
} }

View File

@ -1,7 +1,11 @@
import { Component, ComponentNames } from "."; import { Component, ComponentNames, Velocity } from ".";
export class Control extends Component { export class Control extends Component {
constructor() { public controlVelocity: Velocity;
constructor(controlVelocity: Velocity = new Velocity()) {
super(ComponentNames.Control); super(ComponentNames.Control);
this.controlVelocity = controlVelocity;
} }
} }

View File

@ -1,4 +1,4 @@
import type { Accel2D, Force2D } from "../interfaces"; import type { Force2D } from "../interfaces";
import { Component } from "./Component"; import { Component } from "./Component";
import { ComponentNames } from "."; import { ComponentNames } from ".";

View File

@ -17,7 +17,7 @@ export class Sprite extends Component {
spriteImgPos: Coord2D, spriteImgPos: Coord2D,
spriteImgDimensions: Dimension2D, spriteImgDimensions: Dimension2D,
msPerFrame: number, msPerFrame: number,
numFrames: number numFrames: number,
) { ) {
super(ComponentNames.Sprite); super(ComponentNames.Sprite);
@ -44,7 +44,7 @@ export class Sprite extends Component {
ctx.save(); ctx.save();
ctx.translate(center.x, center.y); ctx.translate(center.x, center.y);
if (rotation != 0) { if (rotation != undefined && rotation != 0) {
ctx.rotate(rotation * (Math.PI / 180)); ctx.rotate(rotation * (Math.PI / 180));
} }
ctx.translate(-center.x, -center.y); ctx.translate(-center.x, -center.y);
@ -56,7 +56,7 @@ export class Sprite extends Component {
ctx.drawImage( ctx.drawImage(
this.sheet, this.sheet,
...this.getSpriteArgs(), ...this.getSpriteArgs(),
...this.getDrawArgs(drawArgs) ...this.getDrawArgs(drawArgs),
); );
if (tint) { if (tint) {

View File

@ -6,10 +6,18 @@ export class Velocity extends Component {
public dCartesian: Velocity2D; public dCartesian: Velocity2D;
public dTheta: number; public dTheta: number;
constructor(dCartesian: Velocity2D, dTheta: number) { constructor(dCartesian: Velocity2D = { dx: 0, dy: 0 }, dTheta: number = 0) {
super(ComponentNames.Velocity); super(ComponentNames.Velocity);
this.dCartesian = dCartesian; this.dCartesian = dCartesian;
this.dTheta = dTheta; 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;
}
}
} }

View File

@ -4,7 +4,7 @@ import { SPRITE_SPECS } from "./sprites";
export const IMAGES = new Map<string, HTMLImageElement>(); export const IMAGES = new Map<string, HTMLImageElement>();
export const loadSpritesIntoImageElements = ( export const loadSpritesIntoImageElements = (
spriteSpecs: Partial<SpriteSpec>[] spriteSpecs: Partial<SpriteSpec>[],
): Promise<void>[] => { ): Promise<void>[] => {
const spritePromises: Promise<void>[] = []; const spritePromises: Promise<void>[] = [];
@ -17,13 +17,13 @@ export const loadSpritesIntoImageElements = (
spritePromises.push( spritePromises.push(
new Promise((resolve) => { new Promise((resolve) => {
img.onload = () => resolve(); img.onload = () => resolve();
}) }),
); );
} }
if (spriteSpec.states) { if (spriteSpec.states) {
spritePromises.push( spritePromises.push(
...loadSpritesIntoImageElements(Object.values(spriteSpec.states)) ...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values())),
); );
} }
} }
@ -34,7 +34,9 @@ export const loadSpritesIntoImageElements = (
export const loadAssets = () => export const loadAssets = () =>
Promise.all([ Promise.all([
...loadSpritesIntoImageElements( ...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 // TODO: Sound
]); ]);

View File

@ -11,12 +11,12 @@ export namespace KeyConstants {
}; };
export const ActionKeys: Map<Action, string[]> = Object.keys( export const ActionKeys: Map<Action, string[]> = Object.keys(
KeyActions KeyActions,
).reduce((acc: Map<Action, string[]>, key) => { ).reduce((acc: Map<Action, string[]>, key) => {
const action = KeyActions[key]; const action = KeyActions[key];
if (acc.has(action)) { if (acc.has(action)) {
acc.get(action).push(key); acc.get(action)?.push(key);
return acc; return acc;
} }
@ -29,8 +29,8 @@ export namespace PhysicsConstants {
export const MAX_JUMP_TIME_MS = 150; export const MAX_JUMP_TIME_MS = 150;
export const GRAVITY = 0.0075; export const GRAVITY = 0.0075;
export const PLAYER_MOVE_VEL = 1; export const PLAYER_MOVE_VEL = 1;
export const PLAYER_JUMP_ACC = -0.01; export const PLAYER_JUMP_ACC = -0.008;
export const PLAYER_JUMP_INITIAL_VEL = -0.9; export const PLAYER_JUMP_INITIAL_VEL = -1;
} }
export namespace Miscellaneous { export namespace Miscellaneous {

View File

@ -10,7 +10,7 @@ export interface SpriteSpec {
height: number; height: number;
frames: number; frames: number;
msPerFrame: 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< export const SPRITE_SPECS: Map<Sprites, Partial<SpriteSpec>> = new Map<
@ -22,28 +22,27 @@ const floorSpriteSpec = {
height: 40, height: 40,
frames: 3, frames: 3,
msPerFrame: 125, msPerFrame: 125,
states: {}, states: new Map<number, Partial<SpriteSpec>>(),
}; };
floorSpriteSpec.states = [40, 80, 120, 160].reduce((acc, cur) => { [40, 80, 120, 160].forEach((width) => {
acc[cur] = { floorSpriteSpec.states.set(width, {
width: cur, width,
sheet: `/assets/floor_tile_${cur}.png`, sheet: `/assets/floor_tile_${width}.png`,
}; });
return acc; });
}, {});
SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec); SPRITE_SPECS.set(Sprites.FLOOR, floorSpriteSpec);
SPRITE_SPECS.set(Sprites.COFFEE, { const coffeeSpriteSpec = {
msPerFrame: 100, msPerFrame: 100,
width: 60, width: 60,
height: 45, height: 45,
frames: 3, frames: 3,
states: { states: new Map<string, Partial<SpriteSpec>>(),
LEFT: { };
sheet: "/assets/coffee_left.png", coffeeSpriteSpec.states.set("LEFT", {
}, sheet: "/assets/coffee_left.png",
RIGHT: {
sheet: "/assets/coffee_right.png",
},
},
}); });
coffeeSpriteSpec.states.set("RIGHT", {
sheet: "/assets/coffee_right.png",
});
SPRITE_SPECS.set(Sprites.COFFEE, coffeeSpriteSpec);

View File

@ -1,5 +1,4 @@
import type { Component } from "../components"; import type { Component } from "../components";
import { ComponentNotFoundError } from "../exceptions";
export abstract class Entity { export abstract class Entity {
private static ID = 0; private static ID = 0;

View File

@ -4,26 +4,28 @@ import { TopCollidable } from "../components/TopCollidable";
import { Entity } from "../entities"; import { Entity } from "../entities";
export class Floor extends Entity { 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) { constructor(width: number) {
super(); super();
this.addComponent( this.addComponent(
new Sprite( new Sprite(
IMAGES.get(Floor.spriteSpec.states[width].sheet), IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet),
{ x: 0, y: 0 }, { x: 0, y: 0 },
{ width, height: Floor.spriteSpec.height }, { width, height: Floor.spriteSpec.height },
Floor.spriteSpec.msPerFrame, Floor.spriteSpec.msPerFrame,
Floor.spriteSpec.frames Floor.spriteSpec.frames,
) ),
); );
this.addComponent( this.addComponent(
new BoundingBox( new BoundingBox(
{ x: 300, y: 300 }, { x: 300, y: 300 },
{ width, height: Floor.spriteSpec.height } { width, height: Floor.spriteSpec.height },
) ),
); );
this.addComponent(new TopCollidable()); this.addComponent(new TopCollidable());

View File

@ -14,14 +14,15 @@ import {
Mass, Mass,
Moment, Moment,
} from "../components"; } from "../components";
import { PhysicsConstants } from "../config";
import { Direction } from "../interfaces"; import { Direction } from "../interfaces";
export class Player extends Entity { export class Player extends Entity {
private static MASS: number = 10; private static MASS: number = 10;
private static MOI: number = 1000; 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() { constructor() {
super(); super();
@ -30,8 +31,8 @@ export class Player extends Entity {
new BoundingBox( new BoundingBox(
{ x: 300, y: 100 }, { x: 300, y: 100 },
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height },
0 0,
) ),
); );
this.addComponent(new Velocity({ dx: 0, dy: 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( const [leftSprite, rightSprite] = [Direction.LEFT, Direction.RIGHT].map(
(direction) => (direction) =>
new Sprite( new Sprite(
IMAGES.get(Player.spriteSpec.states[direction].sheet), IMAGES.get(Player.spriteSpec.states?.get(direction)?.sheet as string),
{ x: 0, y: 0 }, { x: 0, y: 0 },
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height },
Player.spriteSpec.msPerFrame, Player.spriteSpec.msPerFrame,
Player.spriteSpec.frames Player.spriteSpec.frames,
) ),
); );
this.addComponent(new FacingDirection(leftSprite, rightSprite)); this.addComponent(new FacingDirection(leftSprite, rightSprite));

View File

@ -1,5 +0,0 @@
export interface LeaderBoardEntry {
name: string;
score: number;
avatar: string;
}

View File

@ -1,4 +1,3 @@
export * from "./LeaderBoardEntry";
export * from "./Vec2"; export * from "./Vec2";
export * from "./Draw"; export * from "./Draw";
export * from "./Direction"; export * from "./Direction";

View File

@ -1,6 +1,4 @@
import type { Coord2D, Dimension2D } from "../interfaces"; import type { Coord2D, Dimension2D } from "../interfaces";
import { ComponentNames, BoundingBox } from "../components";
import { Entity } from "../entities";
interface BoxedEntry { interface BoxedEntry {
id: number; id: number;
@ -30,21 +28,26 @@ export class QuadTree {
dimension: Dimension2D, dimension: Dimension2D,
maxLevels: number, maxLevels: number,
splitThreshold: number, splitThreshold: number,
level?: number level?: number,
) { ) {
this.children = []; this.children = new Map<Quadrant, QuadTree>();
this.objects = []; this.objects = [];
this.maxLevels = maxLevels; this.maxLevels = maxLevels;
this.splitThreshold = splitThreshold; this.splitThreshold = splitThreshold;
this.level = level ?? 0; this.level = level ?? 0;
this.topLeft = topLeft;
this.dimension = dimension;
} }
public insert(id: number, dimension: Dimension2D, center: Coord2D): void { public insert(id: number, dimension: Dimension2D, center: Coord2D): void {
const box: BoxedEntry = { id, center, dimension };
if (this.hasChildren()) { if (this.hasChildren()) {
this.getIndices(boundingBox).forEach((i) => this.getQuadrants(box).forEach((quadrant) => {
this.children[i].insert(id, dimension, center) const quadrantBox = this.children.get(quadrant);
); quadrantBox?.insert(id, dimension, center);
});
return; return;
} }
@ -74,9 +77,10 @@ export class QuadTree {
if (this.hasChildren()) { if (this.hasChildren()) {
this.getQuadrants(boxedEntry).forEach((quadrant) => { this.getQuadrants(boxedEntry).forEach((quadrant) => {
this.children const quadrantBox = this.children.get(quadrant);
.get(quadrant)
.getNeighborIds(boxedEntry) quadrantBox
?.getNeighborIds(boxedEntry)
.forEach((id) => neighbors.push(id)); .forEach((id) => neighbors.push(id));
}); });
} }
@ -88,15 +92,17 @@ export class QuadTree {
const halfWidth = this.dimension.width / 2; const halfWidth = this.dimension.width / 2;
const halfHeight = this.dimension.height / 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, [Quadrant.I, { x: this.topLeft.x + halfWidth, y: this.topLeft.y }],
{ x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight }, [Quadrant.II, { ...this.topLeft }],
], [Quadrant.III, { x: this.topLeft.x, y: this.topLeft.y + halfHeight }],
].forEach(([quadrant, pos]) => { [
Quadrant.IV,
{ x: this.topLeft.x + halfWidth, y: this.topLeft.y + halfHeight },
],
] as [[Quadrant, Coord2D]]
).forEach(([quadrant, pos]) => {
this.children.set( this.children.set(
quadrant, quadrant,
new QuadTree( new QuadTree(
@ -104,34 +110,48 @@ export class QuadTree {
{ width: halfWidth, height: halfHeight }, { width: halfWidth, height: halfHeight },
this.maxLevels, this.maxLevels,
this.splitThreshold, this.splitThreshold,
this.level + 1 this.level + 1,
) ),
); );
}); });
} }
private getQuandrants(boxedEntry: BoxedEntry): Quadrant[] { private getQuadrants(boxedEntry: BoxedEntry): Quadrant[] {
const treeCenter: Coord2D = { const treeCenter: Coord2D = {
x: this.topLeft.x + this.dimension.width / 2, 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 [ 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.I,
[Quadrant.IV, (x, y) => 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,
],
[
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( .filter(
([_quadrant, condition]) => ([_quadrant, condition]) =>
condition( condition(
boxedEntry.center.x + boxedEntry.dimension.width / 2, boxedEntry.center.x + boxedEntry.dimension.width / 2,
boxedEntry.center.y + boxedEntry.dimension.height / 2 boxedEntry.center.y + boxedEntry.dimension.height / 2,
) || ) ||
condition( condition(
boxedEntry.center.x - boxedEntry.dimension.width / 2, boxedEntry.center.x - boxedEntry.dimension.width / 2,
boxedEntry.center.y - boxedEntry.dimension.height / 2 boxedEntry.center.y - boxedEntry.dimension.height / 2,
) ),
) )
.map(([quadrant]) => quadrant); .map(([quadrant]) => quadrant);
} }
@ -139,9 +159,12 @@ export class QuadTree {
private realignObjects(): void { private realignObjects(): void {
this.objects.forEach((boxedEntry) => { this.objects.forEach((boxedEntry) => {
this.getQuadrants(boxedEntry).forEach((direction) => { this.getQuadrants(boxedEntry).forEach((direction) => {
this.children const quadrant = this.children.get(direction);
.get(direction) quadrant?.insert(
.insert(boxedEntry.id, boxedEntry.dimension, boxedEntry.center); boxedEntry.id,
boxedEntry.dimension,
boxedEntry.center,
);
}); });
}); });
@ -149,6 +172,6 @@ export class QuadTree {
} }
private hasChildren() { private hasChildren() {
return this.children && this.children.length > 0; return this.children && this.children.size > 0;
} }
} }

View File

@ -5,7 +5,7 @@ import {
ComponentNames, ComponentNames,
Jump, Jump,
Velocity, Velocity,
Moment, Forces,
} from "../components"; } from "../components";
import { Game } from "../Game"; import { Game } from "../Game";
import { PhysicsConstants } from "../config"; import { PhysicsConstants } from "../config";
@ -30,60 +30,64 @@ export class Collision extends System {
{ x: 0, y: 0 }, { x: 0, y: 0 },
screenDimensions, screenDimensions,
Collision.QUADTREE_MAX_LEVELS, 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 // rebuild the quadtree
this.quadTree.clear(); this.quadTree.clear();
const entitiesToAddToQuadtree: Entity[] = []; const entitiesToAddToQuadtree: Entity[] = [];
Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) => Collision.COLLIDABLE_COMPONENT_NAMES.map((componentName) =>
game.componentEntities.get(componentName) game.componentEntities.get(componentName),
).forEach((entityIds?: Set<number>) => ).forEach(
entityIds?.forEach((id) => { (entityIds?: Set<number>) =>
const entity = game.entities.get(id); entityIds?.forEach((id) => {
if (!entity.hasComponent(ComponentNames.BoundingBox)) { const entity = game.entities.get(id);
return; if (!entity || !entity.hasComponent(ComponentNames.BoundingBox)) {
} return;
entitiesToAddToQuadtree.push(entity); }
}) entitiesToAddToQuadtree.push(entity);
}),
); );
entitiesToAddToQuadtree.forEach((entity) => { entitiesToAddToQuadtree.forEach((entity) => {
const boundingBox = entity.getComponent<BoundingBox>( const boundingBox = entity.getComponent<BoundingBox>(
ComponentNames.BoundingBox ComponentNames.BoundingBox,
); );
this.quadTree.insert( let dimension = { ...boundingBox.dimension };
entity.id, if (boundingBox.rotation != 0) {
boundingBox.dimension, dimension = boundingBox.getOutscribedBoxDims();
boundingBox.center }
);
this.quadTree.insert(entity.id, dimension, boundingBox.center);
}); });
// find colliding entities and perform collisions // find colliding entities and perform collisions
const collidingEntities = this.getCollidingEntities( const collidingEntities = this.getCollidingEntities(
entitiesToAddToQuadtree, entitiesToAddToQuadtree,
game.entities game,
); );
collidingEntities.forEach(([entityAId, entityBId]) => { collidingEntities.forEach(([entityAId, entityBId]) => {
const [entityA, entityB] = [entityAId, entityBId].map((id) => 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) { private performCollision(entityA: Entity, entityB: Entity) {
const [entityABoundingBox, entityBBoundingBox] = [entityA, entityB].map( 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)) { if (entityA.hasComponent(ComponentNames.Velocity)) {
velocity = entityA.getComponent<Velocity>(ComponentNames.Velocity); velocity = entityA.getComponent<Velocity>(ComponentNames.Velocity);
} }
@ -92,17 +96,16 @@ export class Collision extends System {
entityA.hasComponent(ComponentNames.Collide) && entityA.hasComponent(ComponentNames.Collide) &&
entityB.hasComponent(ComponentNames.TopCollidable) && entityB.hasComponent(ComponentNames.TopCollidable) &&
entityABoundingBox.center.y <= entityBBoundingBox.center.y && entityABoundingBox.center.y <= entityBBoundingBox.center.y &&
velocity &&
velocity.dCartesian.dy >= 0 // don't apply "floor" logic when coming through the bottom velocity.dCartesian.dy >= 0 // don't apply "floor" logic when coming through the bottom
) { ) {
if (entityBBoundingBox.rotation != 0) { if (entityBBoundingBox.rotation != 0) {
throw new Error( 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 // remove previous velocity in the y axis
velocity.dCartesian.dy = 0; if (velocity) velocity.dCartesian.dy = 0;
// apply normal force // apply normal force
if (entityA.hasComponent(ComponentNames.Gravity)) { if (entityA.hasComponent(ComponentNames.Gravity)) {
@ -110,7 +113,8 @@ export class Collision extends System {
const F_n = -mass * PhysicsConstants.GRAVITY; const F_n = -mass * PhysicsConstants.GRAVITY;
entityA.getComponent<Forces>(ComponentNames.Forces).forces.push({ 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( private getCollidingEntities(
collidableEntities: Entity[], collidableEntities: Entity[],
entityMap: Map<number, Entity> game: Game,
): [number, number][] { ): [number, number][] {
const collidingEntityIds: [number, number] = []; const collidingEntityIds: [number, number][] = [];
for (const entity of collidableEntities) { for (const entity of collidableEntities) {
const boundingBox = entity.getComponent<BoundingBox>( const boundingBox = entity.getComponent<BoundingBox>(
ComponentNames.BoundingBox ComponentNames.BoundingBox,
); );
this.quadTree const neighborIds = this.quadTree
.getNeighborIds({ .getNeighborIds({
id: entity.id, id: entity.id,
dimension: boundingBox.dimension, dimension: boundingBox.dimension,
center: boundingBox.center, center: boundingBox.center,
}) })
.filter((neighborId) => neighborId != entity.id) .filter((neighborId) => neighborId != entity.id);
.forEach((neighborId) => {
const neighborBoundingBox = entityMap
.get(neighborId)
.getComponent<BoundingBox>(ComponentNames.BoundingBox);
if (boundingBox.isCollidingWith(neighborBoundingBox)) { neighborIds.forEach((neighborId) => {
collidingEntityIds.push([entity.id, 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; return collidingEntityIds;
@ -161,55 +169,45 @@ export class Collision extends System {
// ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ // ramblings: https://excalidraw.com/#json=z-xD86Za4a3duZuV2Oky0,KaGe-5iHJu1Si8inEo4GLQ
private getDyToPushOutOfFloor( private getDyToPushOutOfFloor(
entityBoundingBox: BoundingBox, entityBoundingBox: BoundingBox,
floorBoundingBox: BoundingBox floorBoundingBox: BoundingBox,
): number { ): number {
const { const {
rotation,
center: { x, y },
dimension: { width, height }, dimension: { width, height },
center: { x },
} = entityBoundingBox; } = entityBoundingBox;
let rads = rotation * (Math.PI / 180); const outScribedRectangle = entityBoundingBox.getOutscribedBoxDims();
if (rads >= Math.PI) {
rads -= Math.PI; // we have symmetry so we can skip two cases
}
let boundedCollisionX = 0; // bounded x on the surface from width let rads = entityBoundingBox.getRotationInPiOfUnitCircle();
let clippedX = 0; // x coordinate of the vertex below the surface let dx = (width * Math.cos(rads) - height * Math.sin(rads)) / 2;
let outScribedRectangleHeight, dy, dx;
if (rads <= Math.PI / 2) { 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) {
rads -= Math.PI / 2; rads -= Math.PI / 2;
dx = (height * Math.cos(rads) - width * Math.sin(rads)) / 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) { if (x >= floorBoundingBox.center.x) {
clippedX = x + dx;
boundedCollisionX = Math.min( boundedCollisionX = Math.min(
floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2, floorBoundingBox.center.x + floorBoundingBox.dimension.width / 2,
clippedX clippedX,
); );
return ( return (
outScribedRectangleHeight / 2 - outScribedRectangle.height / 2 -
Math.max((clippedX - boundedCollisionX) * Math.tan(rads), 0) Math.max((clippedX - boundedCollisionX) * Math.tan(rads), 0)
); );
} }
clippedX = x - dx;
boundedCollisionX = Math.max( boundedCollisionX = Math.max(
floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2, floorBoundingBox.center.x - floorBoundingBox.dimension.width / 2,
clippedX clippedX,
); );
return ( return (
outScribedRectangleHeight / 2 - outScribedRectangle.height / 2 -
Math.max((boundedCollisionX - clippedX) * Math.tan(rads), 0) Math.max((boundedCollisionX - clippedX) * Math.tan(Math.PI / 2 - rads), 0)
); );
} }
} }

View File

@ -2,9 +2,9 @@ import {
ComponentNames, ComponentNames,
Velocity, Velocity,
FacingDirection as FacingDirectionComponent, FacingDirection as FacingDirectionComponent,
Control,
} from "../components"; } from "../components";
import { Game } from "../Game"; import { Game } from "../Game";
import type { Entity } from "../entities";
import { System, SystemNames } from "./"; import { System, SystemNames } from "./";
export class FacingDirection extends System { export class FacingDirection extends System {
@ -13,24 +13,31 @@ export class FacingDirection extends System {
} }
public update(_dt: number, game: Game) { public update(_dt: number, game: Game) {
game.componentEntities game.forEachEntityWithComponent(
.get(ComponentNames.FacingDirection) ComponentNames.FacingDirection,
?.forEach((entityId) => { (entity) => {
const entity = game.entities.get(entityId);
if (!entity.hasComponent(ComponentNames.Velocity)) { if (!entity.hasComponent(ComponentNames.Velocity)) {
return; return;
} }
const totalVelocity: Velocity = new Velocity();
const control = entity.getComponent<Control>(ComponentNames.Control);
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
totalVelocity.add(velocity);
if (control) {
totalVelocity.add(control.controlVelocity);
}
const facingDirection = entity.getComponent<FacingDirectionComponent>( const facingDirection = entity.getComponent<FacingDirectionComponent>(
ComponentNames.FacingDirection ComponentNames.FacingDirection,
); );
if (velocity.dCartesian.dx > 0) { if (totalVelocity.dCartesian.dx > 0) {
entity.addComponent(facingDirection.facingRightSprite); entity.addComponent(facingDirection.facingRightSprite);
} else if (velocity.dCartesian.dx < 0) { } else if (totalVelocity.dCartesian.dx < 0) {
entity.addComponent(facingDirection.facingLeftSprite); entity.addComponent(facingDirection.facingLeftSprite);
} }
}); },
);
} }
} }

View File

@ -1,21 +1,16 @@
import { import {
Jump, Jump,
Forces, Forces,
Acceleration,
ComponentNames, ComponentNames,
Velocity, Velocity,
Mass, Mass,
Control,
} from "../components"; } from "../components";
import { Game } from "../Game"; import { Game } from "../Game";
import { KeyConstants, PhysicsConstants } from "../config"; import { KeyConstants, PhysicsConstants } from "../config";
import type { Entity } from "../entities";
import { Action } from "../interfaces"; import { Action } from "../interfaces";
import { System, SystemNames } from "./"; 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 { export class Input extends System {
private keys: Set<string>; private keys: Set<string>;
private actionTimeStamps: Map<Action, number>; private actionTimeStamps: Map<Action, number>;
@ -23,7 +18,7 @@ export class Input extends System {
constructor() { constructor() {
super(SystemNames.Input); super(SystemNames.Input);
this.keys = new Set<number>(); this.keys = new Set<string>();
this.actionTimeStamps = new Map<Action, number>(); this.actionTimeStamps = new Map<Action, number>();
} }
@ -35,51 +30,52 @@ export class Input extends System {
this.keys.delete(key); this.keys.delete(key);
} }
private hasSomeKey(keys: string[]): boolean { private hasSomeKey(keys?: string[]): boolean {
return keys.some((key) => this.keys.has(key)); if (keys) {
return keys.some((key) => this.keys.has(key));
}
return false;
} }
public update(dt: number, game: Game) { public update(_dt: number, game: Game) {
game.componentEntities.get(ComponentNames.Control)?.forEach((entityId) => { game.forEachEntityWithComponent(ComponentNames.Control, (entity) => {
const entity = game.entities.get(entityId); const control = entity.getComponent<Control>(ComponentNames.Control);
if (!entity.hasComponent(ComponentNames.Velocity)) {
return;
}
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) { if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
velocity.dCartesian.dx = PhysicsConstants.PLAYER_MOVE_VEL; control.controlVelocity.dCartesian.dx +=
} else if ( PhysicsConstants.PLAYER_MOVE_VEL;
this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))
) {
velocity.dCartesian.dx = -PhysicsConstants.PLAYER_MOVE_VEL;
} else {
velocity.dCartesian.dx = 0;
} }
});
game.componentEntities.get(ComponentNames.Jump)?.forEach((entityId) => { if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) {
const entity = game.entities.get(entityId); control.controlVelocity.dCartesian.dx +=
const jump = entity.getComponent<Jump>(ComponentNames.Jump); -PhysicsConstants.PLAYER_MOVE_VEL;
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); }
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) { if (entity.hasComponent(ComponentNames.Jump)) {
if (jump.canJump) { const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
this.actionTimeStamps.set(Action.JUMP, performance.now()); const jump = entity.getComponent<Jump>(ComponentNames.Jump);
velocity.dCartesian.dy = PhysicsConstants.PLAYER_JUMP_INITIAL_VEL; if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) {
jump.canJump = false; if (jump.canJump) {
} this.actionTimeStamps.set(Action.JUMP, performance.now());
if ( velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL;
performance.now() - this.actionTimeStamps.get(Action.JUMP) < jump.canJump = false;
PhysicsConstants.MAX_JUMP_TIME_MS }
) {
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; if (
entity.getComponent<Forces>(ComponentNames.Forces)?.forces.push({ performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) <
fCartesian: { fy: mass * PhysicsConstants.PLAYER_JUMP_ACC }, 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,
});
}
} }
} }
}); });

View File

@ -1,6 +1,5 @@
import { System, SystemNames } from "."; import { System, SystemNames } from ".";
import { import {
Acceleration,
BoundingBox, BoundingBox,
ComponentNames, ComponentNames,
Forces, Forces,
@ -8,9 +7,10 @@ import {
Velocity, Velocity,
Mass, Mass,
Jump, Jump,
Moment,
Control,
} from "../components"; } from "../components";
import { PhysicsConstants } from "../config"; import { PhysicsConstants } from "../config";
import type { Entity } from "../entities";
import type { Force2D } from "../interfaces"; import type { Force2D } from "../interfaces";
import { Game } from "../Game"; import { Game } from "../Game";
@ -20,14 +20,12 @@ export class Physics extends System {
} }
public update(dt: number, game: Game): void { public update(dt: number, game: Game): void {
game.componentEntities.get(ComponentNames.Forces)?.forEach((entityId) => { game.forEachEntityWithComponent(ComponentNames.Forces, (entity) => {
const entity = game.entities.get(entityId);
const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass;
const forces = entity.getComponent<Forces>(ComponentNames.Forces).forces; const forces = entity.getComponent<Forces>(ComponentNames.Forces).forces;
const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity); const velocity = entity.getComponent<Velocity>(ComponentNames.Velocity);
const inertia = entity.getComponent<Moment>( const inertia = entity.getComponent<Moment>(
ComponentNames.Moment ComponentNames.Moment,
).inertia; ).inertia;
// F_g = mg, applied only until terminal velocity is reached // F_g = mg, applied only until terminal velocity is reached
@ -37,7 +35,9 @@ export class Physics extends System {
forces.push({ forces.push({
fCartesian: { fCartesian: {
fy: mass * PhysicsConstants.GRAVITY, fy: mass * PhysicsConstants.GRAVITY,
fx: 0,
}, },
torque: 0,
}); });
} }
} }
@ -51,7 +51,7 @@ export class Physics extends System {
}, },
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 // integrate accelerations
@ -62,6 +62,7 @@ export class Physics extends System {
velocity.dCartesian.dx += ddx * dt; velocity.dCartesian.dx += ddx * dt;
velocity.dCartesian.dy += ddy * dt; velocity.dCartesian.dy += ddy * dt;
velocity.dTheta += (sumOfForces.torque * dt) / inertia; velocity.dTheta += (sumOfForces.torque * dt) / inertia;
// clear the forces // clear the forces
entity.getComponent<Forces>(ComponentNames.Forces).forces = []; entity.getComponent<Forces>(ComponentNames.Forces).forces = [];
@ -71,11 +72,17 @@ export class Physics extends System {
} }
}); });
game.componentEntities.get(ComponentNames.Velocity)?.forEach((entityId) => { game.forEachEntityWithComponent(ComponentNames.Velocity, (entity) => {
const entity = game.entities.get(entityId); const velocity: Velocity = new Velocity();
const velocity = entity.getComponent<Velocity>(ComponentNames.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>( const boundingBox = entity.getComponent<BoundingBox>(
ComponentNames.BoundingBox ComponentNames.BoundingBox,
); );
// integrate velocity // integrate velocity
@ -86,6 +93,11 @@ export class Physics extends System {
(boundingBox.rotation < 0 (boundingBox.rotation < 0
? 360 + boundingBox.rotation ? 360 + boundingBox.rotation
: boundingBox.rotation) % 360; : boundingBox.rotation) % 360;
// clear the control velocity
if (control) {
control.controlVelocity = new Velocity();
}
}); });
} }
} }

View File

@ -1,8 +1,6 @@
import { System, SystemNames } from "."; import { System, SystemNames } from ".";
import { BoundingBox, ComponentNames, Sprite } from "../components"; import { BoundingBox, ComponentNames, Sprite } from "../components";
import type { Entity } from "../entities";
import { Game } from "../Game"; import { Game } from "../Game";
import type { DrawArgs } from "../interfaces";
import { clamp } from "../utils"; import { clamp } from "../utils";
export class Render extends System { export class Render extends System {
@ -16,39 +14,36 @@ export class Render extends System {
public update(dt: number, game: Game) { public update(dt: number, game: Game) {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
game.componentEntities.get(ComponentNames.Sprite)?.forEach((entityId) => { game.forEachEntityWithComponent(ComponentNames.Sprite, (entity) => {
const entity = game.entities.get(entityId);
const sprite = entity.getComponent<Sprite>(ComponentNames.Sprite); const sprite = entity.getComponent<Sprite>(ComponentNames.Sprite);
sprite.update(dt); sprite.update(dt);
let drawArgs: DrawArgs; const boundingBox = entity.getComponent<BoundingBox>(
if (entity.hasComponent(ComponentNames.BoundingBox)) { ComponentNames.BoundingBox,
const boundingBox = entity.getComponent<BoundingBox>( );
ComponentNames.BoundingBox
);
// don't render if we're outside the screen // don't render if we're outside the screen
if ( if (
clamp( clamp(
boundingBox.center.y, boundingBox.center.y,
-boundingBox.dimension.height / 2, -boundingBox.dimension.height / 2,
this.ctx.canvas.height + boundingBox.dimension.height / 2 this.ctx.canvas.height + boundingBox.dimension.height / 2,
) != boundingBox.center.y || ) != boundingBox.center.y ||
clamp( clamp(
boundingBox.center.x, boundingBox.center.x,
-boundingBox.dimension.width / 2, -boundingBox.dimension.width / 2,
this.ctx.canvas.width + boundingBox.dimension.width / 2 this.ctx.canvas.width + boundingBox.dimension.width / 2,
) != boundingBox.center.x ) != boundingBox.center.x
) { ) {
return; return;
}
drawArgs = {
center: boundingBox.center,
dimension: boundingBox.dimension,
rotation: boundingBox.rotation,
};
} }
const drawArgs = {
center: boundingBox.center,
dimension: boundingBox.dimension,
rotation: boundingBox.rotation,
};
sprite.draw(this.ctx, drawArgs); sprite.draw(this.ctx, drawArgs);
}); });
} }

View File

@ -1,4 +1,3 @@
import { Entity } from "../entities";
import { Game } from "../Game"; import { Game } from "../Game";
export abstract class System { export abstract class System {

View File

@ -14,23 +14,16 @@ export class WallBounds extends System {
} }
public update(_dt: number, game: Game) { public update(_dt: number, game: Game) {
game.componentEntities game.forEachEntityWithComponent(ComponentNames.WallBounded, (entity) => {
.get(ComponentNames.WallBounded) const boundingBox = entity.getComponent<BoundingBox>(
?.forEach((entityId) => { ComponentNames.BoundingBox,
const entity = game.entities.get(entityId); );
if (!entity.hasComponent(ComponentNames.BoundingBox)) {
return;
}
const boundingBox = entity.getComponent<BoundingBox>( boundingBox.center.x = clamp(
ComponentNames.BoundingBox 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
);
});
} }
} }

View File

@ -1,4 +1,3 @@
export * from "./rotateVector"; export * from "./rotateVector";
export * from "./normalizeVector";
export * from "./dotProduct"; export * from "./dotProduct";
export * from "./clamp"; export * from "./clamp";

View File

@ -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 };
};

View File

@ -1,21 +1,17 @@
import { Game } from "../../engine/Game"; import { Game } from "../../engine/Game";
import { Floor, Player } from "../../engine/entities"; import { Floor, Player } from "../../engine/entities";
import { import { WallBounds, Physics, Collision } from "../../engine/systems";
WallBounds,
FacingDirection,
Physics,
Input,
Collision,
} from "../../engine/systems";
import { Miscellaneous } from "../../engine/config"; import { Miscellaneous } from "../../engine/config";
const TICK_RATE = 60 / 1000; const TICK_RATE = 60 / 1000;
const game = new Game(); const game = new Game();
[new Physics(), new Collision(), new WallBounds(Miscellaneous.WIDTH)].forEach( [
(system) => game.addSystem(system) new Physics(),
); new Collision({ width: Miscellaneous.WIDTH, height: Miscellaneous.HEIGHT }),
new WallBounds(Miscellaneous.WIDTH),
].forEach((system) => game.addSystem(system));
[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity)); [new Floor(160), new Player()].forEach((entity) => game.addEntity(entity));