diff --git a/src/engine/components/Component.ts b/src/engine/components/Component.ts new file mode 100644 index 0000000..7331982 --- /dev/null +++ b/src/engine/components/Component.ts @@ -0,0 +1,7 @@ +export abstract class Component { + public readonly name: string; + + constructor(name: string) { + this.name = name; + } +} diff --git a/src/engine/components/ComponentNames.ts b/src/engine/components/ComponentNames.ts new file mode 100644 index 0000000..90dfb90 --- /dev/null +++ b/src/engine/components/ComponentNames.ts @@ -0,0 +1,3 @@ +export namespace ComponentNames { + export const Sprite = "Sprite"; +} diff --git a/src/engine/components/index.ts b/src/engine/components/index.ts new file mode 100644 index 0000000..a2fd5d1 --- /dev/null +++ b/src/engine/components/index.ts @@ -0,0 +1,2 @@ +export * from "./Component"; +export * from "./ComponentNames"; diff --git a/src/engine/entities/Entity.ts b/src/engine/entities/Entity.ts new file mode 100644 index 0000000..18ee5d0 --- /dev/null +++ b/src/engine/entities/Entity.ts @@ -0,0 +1,34 @@ +import { type Component } from "../components"; + +const randomId = () => (Math.random() * 1_000_000_000).toString(); + +export abstract class Entity { + public id: string; + public components: Map; + public name: string; + + constructor(name: string, id: string = randomId()) { + this.name = name; + this.id = id; + this.components = new Map(); + } + + public addComponent(component: Component) { + this.components.set(component.name, component); + } + + public getComponent(name: string): T { + if (!this.hasComponent(name)) { + throw new Error("Entity does not have component " + name); + } + return this.components.get(name) as T; + } + + public getComponents(): Component[] { + return Array.from(this.components.values()); + } + + public hasComponent(name: string): boolean { + return this.components.has(name); + } +} diff --git a/src/engine/entities/EntityNames.ts b/src/engine/entities/EntityNames.ts new file mode 100644 index 0000000..59010fc --- /dev/null +++ b/src/engine/entities/EntityNames.ts @@ -0,0 +1,5 @@ +export namespace EntityNames { + export const Player = "Player"; + export const Wall = "Wall"; + export const Ball = "Ball"; +} diff --git a/src/engine/entities/index.ts b/src/engine/entities/index.ts new file mode 100644 index 0000000..ee26a63 --- /dev/null +++ b/src/engine/entities/index.ts @@ -0,0 +1,2 @@ +export * from "./Entity"; +export * from "./EntityNames"; diff --git a/src/engine/index.ts b/src/engine/index.ts new file mode 100644 index 0000000..2df9f17 --- /dev/null +++ b/src/engine/index.ts @@ -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; + public systems: Map; + public componentEntities: Map>; + + 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(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([entity.id]), + ); + return; + } + this.componentEntities.get(component.name)?.add(entity.id); + }), + ); + + this.systemOrder.forEach((systemName) => { + this.systems.get(systemName)?.update(dt, this); + }); + } +} diff --git a/src/engine/systems/System.ts b/src/engine/systems/System.ts new file mode 100644 index 0000000..9852276 --- /dev/null +++ b/src/engine/systems/System.ts @@ -0,0 +1,11 @@ +import { Game } from ".."; + +export abstract class System { + public readonly name: string; + + constructor(name: string) { + this.name = name; + } + + abstract update(dt: number, game: Game): void; +} diff --git a/src/engine/systems/SystemNames.ts b/src/engine/systems/SystemNames.ts new file mode 100644 index 0000000..41207a4 --- /dev/null +++ b/src/engine/systems/SystemNames.ts @@ -0,0 +1,7 @@ +export namespace SystemNames { + export const Render = "Render"; + export const Physics = "Physics"; + export const Input = "Input"; + export const Collision = "Collision"; + export const WallBounds = "WallBounds"; +} diff --git a/src/engine/systems/index.ts b/src/engine/systems/index.ts new file mode 100644 index 0000000..989dc7f --- /dev/null +++ b/src/engine/systems/index.ts @@ -0,0 +1,2 @@ +export * from "./SystemNames"; +export * from "./System";