boundingbox + draw player
This commit is contained in:
parent
aa08a8943a
commit
a8d07a7903
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB |
BIN
public/assets/lambda/down.png
Normal file
BIN
public/assets/lambda/down.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
public/assets/lambda/left.png
Normal file
BIN
public/assets/lambda/left.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
public/assets/lambda/neutral.png
Normal file
BIN
public/assets/lambda/neutral.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
public/assets/lambda/right.png
Normal file
BIN
public/assets/lambda/right.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
public/assets/lambda/up.png
Normal file
BIN
public/assets/lambda/up.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -23,6 +23,10 @@ export const App = () => {
|
||||
className="tf"
|
||||
>
|
||||
simponic
|
||||
</a>{" "}
|
||||
| inspired by{" "}
|
||||
<a href="https://hempuli.com/baba/" target="_blank" className="tf">
|
||||
baba is you
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useRef } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { TheAbstractionEngine, Game } from "../engine";
|
||||
|
||||
export interface GameCanvasProps {
|
||||
width: number;
|
||||
@ -7,6 +8,25 @@ export interface GameCanvasProps {
|
||||
|
||||
export const GameCanvas = ({ width, height }: GameCanvasProps) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [_game, setGame] = useState<TheAbstractionEngine>();
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
const game = new Game();
|
||||
const theAbstractionEngine = new TheAbstractionEngine(game, ctx);
|
||||
|
||||
theAbstractionEngine.init().then(() => {
|
||||
theAbstractionEngine.play();
|
||||
setGame(theAbstractionEngine);
|
||||
});
|
||||
|
||||
return () => theAbstractionEngine.stop();
|
||||
}
|
||||
}
|
||||
}, [canvasRef]);
|
||||
|
||||
return (
|
||||
<div className="centered-game">
|
||||
|
90
src/engine/Game.ts
Normal file
90
src/engine/Game.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Entity } from "./entities";
|
||||
import { System } from "./systems";
|
||||
|
||||
export class Game {
|
||||
private systemOrder: string[];
|
||||
|
||||
private running: boolean;
|
||||
private lastTimeStamp: number;
|
||||
|
||||
public entities: Map<string, Entity>;
|
||||
public systems: Map<string, System>;
|
||||
public componentEntities: Map<string, Set<string>>;
|
||||
|
||||
constructor() {
|
||||
this.lastTimeStamp = performance.now();
|
||||
this.running = false;
|
||||
this.systemOrder = [];
|
||||
this.systems = new Map();
|
||||
this.entities = new Map();
|
||||
this.componentEntities = new Map();
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.lastTimeStamp = performance.now();
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
public addEntity(entity: Entity) {
|
||||
this.entities.set(entity.id, entity);
|
||||
}
|
||||
|
||||
public getEntity(id: string): Entity | undefined {
|
||||
return this.entities.get(id);
|
||||
}
|
||||
|
||||
public removeEntity(id: string) {
|
||||
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);
|
||||
}
|
||||
this.systems.set(system.name, system);
|
||||
}
|
||||
|
||||
public getSystem<T>(name: string): T {
|
||||
return this.systems.get(name) as unknown as T;
|
||||
}
|
||||
|
||||
public doGameLoop(timeStamp: number) {
|
||||
if (!this.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = timeStamp - this.lastTimeStamp;
|
||||
this.lastTimeStamp = timeStamp;
|
||||
|
||||
// rebuild the Component -> { Entity } map
|
||||
this.componentEntities.clear();
|
||||
this.entities.forEach((entity) =>
|
||||
entity.getComponents().forEach((component) => {
|
||||
if (!this.componentEntities.has(component.name)) {
|
||||
this.componentEntities.set(
|
||||
component.name,
|
||||
new Set<string>([entity.id]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.componentEntities.get(component.name)?.add(entity.id);
|
||||
}),
|
||||
);
|
||||
|
||||
this.systemOrder.forEach((systemName) => {
|
||||
this.systems.get(systemName)?.update(dt, this);
|
||||
});
|
||||
}
|
||||
}
|
42
src/engine/TheAbstractionEngine.ts
Normal file
42
src/engine/TheAbstractionEngine.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Game } from ".";
|
||||
import { loadAssets } from "./config";
|
||||
import { Player } from "./entities";
|
||||
import { Render } from "./systems";
|
||||
|
||||
export class TheAbstractionEngine {
|
||||
private game: Game;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private animationFrameId: number | null;
|
||||
|
||||
constructor(game: Game, ctx: CanvasRenderingContext2D) {
|
||||
this.game = game;
|
||||
this.ctx = ctx;
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
await loadAssets();
|
||||
|
||||
[new Render(this.ctx)].forEach((system) => this.game.addSystem(system));
|
||||
|
||||
const player = new Player();
|
||||
this.game.addEntity(player);
|
||||
}
|
||||
|
||||
public play() {
|
||||
this.game.start();
|
||||
|
||||
const loop = (timestamp: number) => {
|
||||
this.game.doGameLoop(timestamp);
|
||||
this.animationFrameId = requestAnimationFrame(loop); // tail call recursion! /s
|
||||
};
|
||||
this.animationFrameId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.animationFrameId) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
}
|
||||
}
|
122
src/engine/components/BoundingBox.ts
Normal file
122
src/engine/components/BoundingBox.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { Component, ComponentNames } from ".";
|
||||
import type { Coord2D, Dimension2D } from "../interfaces";
|
||||
import { dotProduct, rotateVector } from "../utils";
|
||||
|
||||
export class BoundingBox extends Component {
|
||||
public center: Coord2D;
|
||||
public dimension: Dimension2D;
|
||||
public rotation: number;
|
||||
|
||||
constructor(center: Coord2D, dimension: Dimension2D, rotation?: number) {
|
||||
super(ComponentNames.BoundingBox);
|
||||
|
||||
this.center = center;
|
||||
this.dimension = dimension;
|
||||
this.rotation = rotation ?? 0;
|
||||
}
|
||||
|
||||
public isCollidingWith(box: BoundingBox): boolean {
|
||||
// optimization; when neither rotates just check if they overlap
|
||||
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++) {
|
||||
const [A, B] = [poly[i], poly[(i + 1) % poly.length]];
|
||||
const normal: Coord2D = { x: B.y - A.y, y: A.x - B.x };
|
||||
|
||||
const [[minThis, maxThis], [minBox, maxBox]] = boxes.map((box) =>
|
||||
box.reduce(
|
||||
([min, max], vertex) => {
|
||||
const projection = dotProduct(normal, vertex);
|
||||
return [Math.min(min, projection), Math.max(max, projection)];
|
||||
},
|
||||
[Infinity, -Infinity],
|
||||
),
|
||||
);
|
||||
|
||||
if (maxThis < minBox || maxBox < minThis) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public getVertices(): Coord2D[] {
|
||||
return [
|
||||
{ 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)) // rotate
|
||||
.map((vertex) => {
|
||||
// translate
|
||||
return {
|
||||
x: vertex.x + this.center.x,
|
||||
y: vertex.y + this.center.y,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public getOutscribedBoxDims(): Dimension2D {
|
||||
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)),
|
||||
};
|
||||
}
|
||||
|
||||
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)),
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
export namespace ComponentNames {
|
||||
export const Sprite = "Sprite";
|
||||
export const FacingDirection = "FacingDirection";
|
||||
export const GridPosition = "GridPosition";
|
||||
export const BoundingBox = "BoundingBox";
|
||||
}
|
||||
|
12
src/engine/components/FacingDirection.ts
Normal file
12
src/engine/components/FacingDirection.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component, ComponentNames, Sprite } from ".";
|
||||
import { type Direction } from "../interfaces";
|
||||
|
||||
export class FacingDirection extends Component {
|
||||
public readonly directionSprites: Map<Direction, Sprite>;
|
||||
|
||||
constructor() {
|
||||
super(ComponentNames.FacingDirection);
|
||||
|
||||
this.directionSprites = new Map<Direction, Sprite>();
|
||||
}
|
||||
}
|
13
src/engine/components/GridPosition.ts
Normal file
13
src/engine/components/GridPosition.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Component, ComponentNames } from ".";
|
||||
|
||||
export class GridPosition extends Component {
|
||||
public x: number;
|
||||
public y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
super(ComponentNames.GridPosition);
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
96
src/engine/components/Sprite.ts
Normal file
96
src/engine/components/Sprite.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { Component, ComponentNames } from ".";
|
||||
import type { Dimension2D, DrawArgs, Coord2D } from "../interfaces";
|
||||
|
||||
export class Sprite extends Component {
|
||||
private sheet: HTMLImageElement;
|
||||
|
||||
private spriteImgPos: Coord2D;
|
||||
private spriteImgDimensions: Dimension2D;
|
||||
|
||||
private msPerFrame: number;
|
||||
private msSinceLastFrame: number;
|
||||
private currentFrame: number;
|
||||
private numFrames: number;
|
||||
|
||||
constructor(
|
||||
sheet: HTMLImageElement,
|
||||
spriteImgPos: Coord2D,
|
||||
spriteImgDimensions: Dimension2D,
|
||||
msPerFrame: number,
|
||||
numFrames: number,
|
||||
) {
|
||||
super(ComponentNames.Sprite);
|
||||
|
||||
this.sheet = sheet;
|
||||
this.spriteImgPos = spriteImgPos;
|
||||
this.spriteImgDimensions = spriteImgDimensions;
|
||||
this.msPerFrame = msPerFrame;
|
||||
this.numFrames = numFrames;
|
||||
|
||||
this.msSinceLastFrame = 0;
|
||||
this.currentFrame = 0;
|
||||
}
|
||||
|
||||
public update(dt: number) {
|
||||
this.msSinceLastFrame += dt;
|
||||
if (this.msSinceLastFrame >= this.msPerFrame) {
|
||||
this.currentFrame = (this.currentFrame + 1) % this.numFrames;
|
||||
this.msSinceLastFrame = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public draw(ctx: CanvasRenderingContext2D, drawArgs: DrawArgs) {
|
||||
const { center, rotation, tint, opacity } = drawArgs;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(center.x, center.y);
|
||||
if (rotation != undefined && rotation != 0) {
|
||||
ctx.rotate(rotation * (Math.PI / 180));
|
||||
}
|
||||
ctx.translate(-center.x, -center.y);
|
||||
|
||||
if (opacity) {
|
||||
ctx.globalAlpha = opacity;
|
||||
}
|
||||
|
||||
ctx.drawImage(
|
||||
this.sheet,
|
||||
...this.getSpriteArgs(),
|
||||
...this.getDrawArgs(drawArgs),
|
||||
);
|
||||
|
||||
if (tint) {
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.globalCompositeOperation = "source-atop";
|
||||
ctx.fillStyle = tint;
|
||||
ctx.fillRect(...this.getDrawArgs(drawArgs));
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
private getSpriteArgs(): [sx: number, sy: number, sw: number, sh: number] {
|
||||
return [
|
||||
this.spriteImgPos.x + this.currentFrame * this.spriteImgDimensions.width,
|
||||
this.spriteImgPos.y,
|
||||
this.spriteImgDimensions.width,
|
||||
this.spriteImgDimensions.height,
|
||||
];
|
||||
}
|
||||
|
||||
private getDrawArgs({
|
||||
center,
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
public getSpriteDimensions() {
|
||||
return this.spriteImgDimensions;
|
||||
}
|
||||
}
|
@ -1,2 +1,6 @@
|
||||
export * from "./Component";
|
||||
export * from "./ComponentNames";
|
||||
export * from "./Sprite";
|
||||
export * from "./FacingDirection";
|
||||
export * from "./GridPosition";
|
||||
export * from "./BoundingBox";
|
||||
|
42
src/engine/config/assets.ts
Normal file
42
src/engine/config/assets.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { SpriteSpec } from "./sprites";
|
||||
import { SPRITE_SPECS } from "./sprites";
|
||||
|
||||
export const IMAGES = new Map<string, HTMLImageElement>();
|
||||
|
||||
export const loadSpritesIntoImageElements = (
|
||||
spriteSpecs: Partial<SpriteSpec>[],
|
||||
): Promise<void>[] => {
|
||||
const spritePromises: Promise<void>[] = [];
|
||||
|
||||
for (const spriteSpec of spriteSpecs) {
|
||||
if (spriteSpec.sheet) {
|
||||
const img = new Image();
|
||||
img.src = spriteSpec.sheet;
|
||||
IMAGES.set(spriteSpec.sheet, img);
|
||||
|
||||
spritePromises.push(
|
||||
new Promise((resolve) => {
|
||||
img.onload = () => resolve();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (spriteSpec.states) {
|
||||
spritePromises.push(
|
||||
...loadSpritesIntoImageElements(Array.from(spriteSpec.states.values())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return spritePromises;
|
||||
};
|
||||
|
||||
export const loadAssets = () =>
|
||||
Promise.all([
|
||||
...loadSpritesIntoImageElements(
|
||||
Array.from(SPRITE_SPECS.keys()).map(
|
||||
(key) => SPRITE_SPECS.get(key) as SpriteSpec,
|
||||
),
|
||||
),
|
||||
// TODO: Sound
|
||||
]);
|
7
src/engine/config/constants.ts
Normal file
7
src/engine/config/constants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export namespace Miscellaneous {
|
||||
export const WIDTH = 800;
|
||||
export const HEIGHT = 800;
|
||||
|
||||
export const DEFAULT_GRID_WIDTH = 30;
|
||||
export const DEFAULT_GRID_HEIGHT = 30;
|
||||
}
|
3
src/engine/config/index.ts
Normal file
3
src/engine/config/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./constants";
|
||||
export * from "./assets";
|
||||
export * from "./sprites";
|
39
src/engine/config/sprites.ts
Normal file
39
src/engine/config/sprites.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Direction } from "../interfaces/Direction";
|
||||
|
||||
export enum Sprites {
|
||||
PLAYER,
|
||||
}
|
||||
|
||||
export interface SpriteSpec {
|
||||
sheet: string;
|
||||
width: number;
|
||||
height: number;
|
||||
frames: number;
|
||||
msPerFrame: number;
|
||||
states?: Map<string | number, Partial<SpriteSpec>>;
|
||||
}
|
||||
|
||||
export const SPRITE_SPECS: Map<Sprites, Partial<SpriteSpec>> = new Map<
|
||||
Sprites,
|
||||
SpriteSpec
|
||||
>();
|
||||
|
||||
const playerSpriteSpec = {
|
||||
msPerFrame: 200,
|
||||
width: 64,
|
||||
height: 64,
|
||||
frames: 3,
|
||||
states: new Map<string, Partial<SpriteSpec>>(),
|
||||
};
|
||||
playerSpriteSpec.states.set(Direction.NONE, {
|
||||
sheet: "/assets/lambda/neutral.png",
|
||||
});
|
||||
[Direction.LEFT, Direction.RIGHT, Direction.UP, Direction.DOWN].forEach(
|
||||
(direction) => {
|
||||
playerSpriteSpec.states.set(direction, {
|
||||
sheet: `/assets/lambda/${direction.toLowerCase()}.png`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
SPRITE_SPECS.set(Sprites.PLAYER, playerSpriteSpec);
|
@ -1,13 +1,13 @@
|
||||
import { type Component } from "../components";
|
||||
|
||||
const randomId = () => (Math.random() * 1_000_000_000).toString();
|
||||
|
||||
export abstract class Entity {
|
||||
static Id = 0;
|
||||
|
||||
public id: string;
|
||||
public components: Map<string, Component>;
|
||||
public name: string;
|
||||
|
||||
constructor(name: string, id: string = randomId()) {
|
||||
constructor(name: string, id: string = (Entity.Id++).toString()) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.components = new Map();
|
||||
|
@ -1,5 +1,3 @@
|
||||
export namespace EntityNames {
|
||||
export const Player = "Player";
|
||||
export const Wall = "Wall";
|
||||
export const Ball = "Ball";
|
||||
}
|
||||
|
58
src/engine/entities/Player.ts
Normal file
58
src/engine/entities/Player.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Entity, EntityNames } from ".";
|
||||
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
|
||||
import {
|
||||
FacingDirection,
|
||||
Sprite,
|
||||
GridPosition,
|
||||
BoundingBox,
|
||||
} from "../components";
|
||||
import { Direction } from "../interfaces/";
|
||||
|
||||
export class Player extends Entity {
|
||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||
Sprites.PLAYER,
|
||||
) as SpriteSpec;
|
||||
|
||||
constructor() {
|
||||
super(EntityNames.Player);
|
||||
|
||||
this.addComponent(
|
||||
new BoundingBox(
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
this.addComponent(new GridPosition(0, 0));
|
||||
this.addFacingDirectionComponents();
|
||||
}
|
||||
|
||||
private addFacingDirectionComponents() {
|
||||
const facingDirectionComponent = new FacingDirection();
|
||||
[
|
||||
Direction.NONE,
|
||||
Direction.LEFT,
|
||||
Direction.RIGHT,
|
||||
Direction.UP,
|
||||
Direction.DOWN,
|
||||
].forEach((direction) => {
|
||||
const sprite = new Sprite(
|
||||
IMAGES.get(Player.spriteSpec.states!.get(direction)!.sheet!)!,
|
||||
{ x: 0, y: 0 },
|
||||
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
||||
Player.spriteSpec.msPerFrame,
|
||||
Player.spriteSpec.frames,
|
||||
);
|
||||
facingDirectionComponent.directionSprites.set(direction, sprite);
|
||||
});
|
||||
|
||||
this.addComponent(facingDirectionComponent);
|
||||
this.addComponent(
|
||||
facingDirectionComponent.directionSprites.get(Direction.NONE)!,
|
||||
); // face no direction by default
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from "./Entity";
|
||||
export * from "./EntityNames";
|
||||
export * from "./Player";
|
||||
|
@ -1,90 +1,2 @@
|
||||
import { Entity } from "./entities";
|
||||
import { System } from "./systems";
|
||||
|
||||
export class Game {
|
||||
private systemOrder: string[];
|
||||
|
||||
private running: boolean;
|
||||
private lastTimeStamp: number;
|
||||
|
||||
public entities: Map<string, Entity>;
|
||||
public systems: Map<string, System>;
|
||||
public componentEntities: Map<string, Set<string>>;
|
||||
|
||||
constructor() {
|
||||
this.lastTimeStamp = performance.now();
|
||||
this.running = false;
|
||||
this.systemOrder = [];
|
||||
this.systems = new Map();
|
||||
this.entities = new Map();
|
||||
this.componentEntities = new Map();
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.lastTimeStamp = performance.now();
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
public addEntity(entity: Entity) {
|
||||
this.entities.set(entity.id, entity);
|
||||
}
|
||||
|
||||
public getEntity(id: string): Entity | undefined {
|
||||
return this.entities.get(id);
|
||||
}
|
||||
|
||||
public removeEntity(id: string) {
|
||||
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);
|
||||
}
|
||||
this.systems.set(system.name, system);
|
||||
}
|
||||
|
||||
public getSystem<T>(name: string): T {
|
||||
return this.systems.get(name) as unknown as T;
|
||||
}
|
||||
|
||||
public doGameLoop(timeStamp: number) {
|
||||
if (!this.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = timeStamp - this.lastTimeStamp;
|
||||
this.lastTimeStamp = timeStamp;
|
||||
|
||||
// rebuild the Component -> { Entity } map
|
||||
this.componentEntities.clear();
|
||||
this.entities.forEach((entity) =>
|
||||
entity.getComponents().forEach((component) => {
|
||||
if (!this.componentEntities.has(component.name)) {
|
||||
this.componentEntities.set(
|
||||
component.name,
|
||||
new Set<string>([entity.id]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.componentEntities.get(component.name)?.add(entity.id);
|
||||
}),
|
||||
);
|
||||
|
||||
this.systemOrder.forEach((systemName) => {
|
||||
this.systems.get(systemName)?.update(dt, this);
|
||||
});
|
||||
}
|
||||
}
|
||||
export * from "./Game";
|
||||
export * from "./TheAbstractionEngine";
|
||||
|
7
src/engine/interfaces/Direction.ts
Normal file
7
src/engine/interfaces/Direction.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum Direction {
|
||||
UP = "UP",
|
||||
DOWN = "DOWN",
|
||||
LEFT = "LEFT",
|
||||
RIGHT = "RIGHT",
|
||||
NONE = "NONE",
|
||||
}
|
9
src/engine/interfaces/Draw.ts
Normal file
9
src/engine/interfaces/Draw.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { Coord2D, Dimension2D } from "./";
|
||||
|
||||
export interface DrawArgs {
|
||||
center: Coord2D;
|
||||
dimension: Dimension2D;
|
||||
tint?: string;
|
||||
opacity?: number;
|
||||
rotation?: number;
|
||||
}
|
25
src/engine/interfaces/Vec2.ts
Normal file
25
src/engine/interfaces/Vec2.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export interface Coord2D {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Dimension2D {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface Velocity2D {
|
||||
dCartesian: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
};
|
||||
dTheta: number;
|
||||
}
|
||||
|
||||
export interface Force2D {
|
||||
fCartesian: {
|
||||
fx: number;
|
||||
fy: number;
|
||||
};
|
||||
torque: number;
|
||||
}
|
3
src/engine/interfaces/index.ts
Normal file
3
src/engine/interfaces/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./Vec2";
|
||||
export * from "./Draw";
|
||||
export * from "./Direction";
|
50
src/engine/systems/Render.ts
Normal file
50
src/engine/systems/Render.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { System, SystemNames } from ".";
|
||||
import { BoundingBox, ComponentNames, Sprite } from "../components";
|
||||
import { Game } from "..";
|
||||
import { clamp } from "../utils";
|
||||
|
||||
export class Render extends System {
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(ctx: CanvasRenderingContext2D) {
|
||||
super(SystemNames.Render);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
public update(dt: number, game: Game) {
|
||||
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
||||
|
||||
game.forEachEntityWithComponent(ComponentNames.Sprite, (entity) => {
|
||||
const sprite = entity.getComponent<Sprite>(ComponentNames.Sprite);
|
||||
sprite.update(dt);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from "./SystemNames";
|
||||
export * from "./System";
|
||||
export * from "./Render";
|
||||
|
2
src/engine/utils/clamp.ts
Normal file
2
src/engine/utils/clamp.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const clamp = (num: number, min: number, max: number) =>
|
||||
Math.min(Math.max(num, min), max);
|
4
src/engine/utils/dotProduct.ts
Normal file
4
src/engine/utils/dotProduct.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import type { Coord2D } from "../interfaces";
|
||||
|
||||
export const dotProduct = (vector1: Coord2D, vector2: Coord2D): number =>
|
||||
vector1.x * vector2.x + vector1.y * vector2.y;
|
3
src/engine/utils/index.ts
Normal file
3
src/engine/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./clamp";
|
||||
export * from "./dotProduct";
|
||||
export * from "./rotateVector";
|
15
src/engine/utils/rotateVector.ts
Normal file
15
src/engine/utils/rotateVector.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { Coord2D } from "../interfaces";
|
||||
|
||||
/**
|
||||
* ([[cos(θ), -sin(θ),]) ([x,)
|
||||
* ([sin(θ), cos(θ)] ]) ( y])
|
||||
*/
|
||||
export const rotateVector = (vector: Coord2D, theta: number): Coord2D => {
|
||||
const rads = (theta * Math.PI) / 180;
|
||||
const [cos, sin] = [Math.cos(rads), Math.sin(rads)];
|
||||
|
||||
return {
|
||||
x: vector.x * cos - vector.y * sin,
|
||||
y: vector.x * sin + vector.y * cos,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user