Experimental canvas 2D engine for tile-based sidescroller/sandbox games, created strictly for educational purposes.
entity-component-system game-engine canvas-2d

♻️ Split project into engine, kit and demo

+19 -9
bun.lock
··· 3 3 "configVersion": 1, 4 4 "workspaces": { 5 5 "": {}, 6 - "packages/cosmic": { 7 - "name": "cosmic", 6 + "packages/core": { 7 + "name": "@cosmic/core", 8 8 "version": "0.1.0", 9 9 }, 10 - "packages/cosmic-demo": { 11 - "name": "cosmic-demo", 10 + "packages/demo": { 11 + "name": "@cosmic/demo", 12 12 "version": "0.1.0", 13 13 "dependencies": { 14 - "cosmic": "workspace:*", 14 + "@cosmic/core": "workspace:*", 15 + "@cosmic/kit": "workspace:*", 15 16 "vite": "^7.2.4", 16 17 }, 17 18 }, 19 + "packages/kit": { 20 + "name": "@cosmic/kit", 21 + "version": "0.1.0", 22 + "devDependencies": { 23 + "@cosmic/core": "workspace:*", 24 + }, 25 + }, 18 26 }, 19 27 "packages": { 28 + "@cosmic/core": ["@cosmic/core@workspace:packages/core"], 29 + 30 + "@cosmic/demo": ["@cosmic/demo@workspace:packages/demo"], 31 + 32 + "@cosmic/kit": ["@cosmic/kit@workspace:packages/kit"], 33 + 20 34 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 21 35 22 36 "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], ··· 114 128 "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], 115 129 116 130 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 117 - 118 - "cosmic": ["cosmic@workspace:packages/cosmic"], 119 - 120 - "cosmic-demo": ["cosmic-demo@workspace:packages/cosmic-demo"], 121 131 122 132 "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 123 133
+4 -1
package.json
··· 9 9 "email": "contact@thevoid.cafe", 10 10 "url": "https://thevoid.cafe" 11 11 } 12 - ] 12 + ], 13 + "scripts": { 14 + "dev": "cd packages/demo && bun dev" 15 + } 13 16 }
packages/cosmic-demo/index.html packages/demo/index.html
+3 -2
packages/cosmic-demo/package.json packages/demo/package.json
··· 1 1 { 2 - "name": "cosmic-demo", 2 + "name": "@cosmic/demo", 3 3 "version": "0.1.0", 4 4 "description": "Simple demo of the cosmic engine.", 5 5 "module": "src/index.ts", ··· 10 10 }, 11 11 "dependencies": { 12 12 "vite": "^7.2.4", 13 - "cosmic": "workspace:*" 13 + "@cosmic/core": "workspace:*", 14 + "@cosmic/kit": "workspace:*" 14 15 } 15 16 }
packages/cosmic-demo/src/components/Collider.ts packages/kit/src/components/Collider.ts
packages/cosmic-demo/src/components/Physics.ts packages/kit/src/components/Physics.ts
packages/cosmic-demo/src/components/PlayerControlled.ts packages/kit/src/components/PlayerControlled.ts
packages/cosmic-demo/src/components/Renderable.ts packages/kit/src/components/Renderable.ts
+1 -1
packages/cosmic-demo/src/components/Transform.ts packages/kit/src/components/Transform.ts
··· 1 - import { TILE_SIZE } from "../constants"; 1 + const TILE_SIZE = 32; 2 2 3 3 export class Transform { 4 4 constructor(
packages/cosmic-demo/src/components/WorldProperties.ts packages/kit/src/components/WorldProperties.ts
packages/cosmic-demo/src/constants.ts packages/demo/src/constants.ts
+3 -12
packages/cosmic-demo/src/index.ts packages/demo/src/index.ts
··· 1 - import { Cosmic, CosmicMode, Entity } from 'cosmic'; 1 + import { Cosmic, CosmicMode, Entity } from '@cosmic/core'; 2 + import { Transform, Collider, WorldProperties, Renderable, Physics, PlayerControlled } from '@cosmic/kit/components'; 3 + import { RenderingSystem, PhysicsSystem, CollisionSystem, PlayerControlSystem } from '@cosmic/kit/systems'; 2 4 3 5 import { TILE_SIZE } from './constants'; 4 - import { PhysicsSystem } from './systems/PhysicsSystem'; 5 - import { CollisionSystem } from './systems/CollisionSystem'; 6 - import { RenderingSystem } from './systems/RenderingSystem'; 7 - import { WorldProperties } from './components/WorldProperties'; 8 - import { PlayerControlSystem } from './systems/PlayerControlSystem'; 9 - 10 - import { Physics } from './components/Physics'; 11 - import { Collider } from './components/Collider'; 12 - import { Transform } from './components/Transform'; 13 - import { Renderable } from './components/Renderable'; 14 - import { PlayerControlled } from './components/PlayerControlled'; 15 6 16 7 const engine = Cosmic.getInstance(CosmicMode.DEVELOPMENT); 17 8
+2 -3
packages/cosmic-demo/src/systems/CollisionSystem.ts packages/kit/src/systems/CollisionSystem.ts
··· 1 - import { Entity, System } from "cosmic"; 2 - import { Collider } from "../components/Collider"; 3 - import { Transform } from "../components/Transform"; 1 + import { Entity, System } from "@cosmic/core"; 2 + import { Transform, Collider } from "@cosmic/kit/components"; 4 3 5 4 export class CollisionSystem extends System { 6 5 public readonly requiredComponents = new Set([
+2 -4
packages/cosmic-demo/src/systems/PhysicsSystem.ts packages/kit/src/systems/PhysicsSystem.ts
··· 1 - import { Entity, System, type Cosmic } from "cosmic"; 2 - import { WorldProperties } from "../components/WorldProperties"; 3 - import { Transform } from "../components/Transform"; 4 - import { Physics } from "../components/Physics"; 1 + import { Entity, System, type Cosmic } from "@cosmic/core"; 2 + import { Transform, WorldProperties, Physics } from "@cosmic/kit/components"; 5 3 6 4 export class PhysicsSystem extends System { 7 5 public requiredComponents = new Set([Transform.name, Physics.name, WorldProperties.name]);
+2 -3
packages/cosmic-demo/src/systems/PlayerControlSystem.ts packages/kit/src/systems/PlayerControlSystem.ts
··· 1 - import { Cosmic, System, Entity } from "cosmic"; 2 - import { Transform } from "../components/Transform"; 3 - import { PlayerControlled } from "../components/PlayerControlled"; 1 + import { Cosmic, System, Entity } from "@cosmic/core"; 2 + import { Transform, PlayerControlled } from "@cosmic/kit/components"; 4 3 5 4 export class PlayerControlSystem extends System { 6 5 public requiredComponents = new Set([Transform.name, PlayerControlled.name]);
+2 -3
packages/cosmic-demo/src/systems/RenderingSystem.ts packages/kit/src/systems/RenderingSystem.ts
··· 1 - import { Entity, System, type Cosmic } from "cosmic"; 2 - import { Transform } from "../components/Transform"; 3 - import { Renderable } from "../components/Renderable"; 1 + import { Entity, System, type Cosmic } from "@cosmic/core"; 2 + import { Transform, Renderable } from "@cosmic/kit/components"; 4 3 5 4 export class RenderingSystem extends System { 6 5 public requiredComponents = new Set([Transform.name, Renderable.name]);
packages/cosmic-demo/styles/base.css packages/demo/styles/base.css
packages/cosmic-demo/vite.config.ts packages/demo/vite.config.ts
+1 -1
packages/cosmic/package.json packages/core/package.json
··· 1 1 { 2 - "name": "cosmic", 2 + "name": "@cosmic/core", 3 3 "version": "0.1.0", 4 4 "description": "Experimental canvas 2D engine for tile-based sidescrolling/sandbox games, created strictly for educational purposes.", 5 5 "module": "src/index.ts",
+64 -6
packages/cosmic/src/core/Cosmic.ts packages/core/src/core/Cosmic.ts
··· 1 - import { System, Entity } from "./EntityComponentSystem"; 2 - import { InputManager } from "./InputManager"; 1 + import { System, Entity, InputManager } from "@cosmic/core"; 3 2 3 + /** 4 + * Enum representing the different modes in which the Cosmic engine can operate. 5 + */ 4 6 export enum CosmicMode { 7 + /** Enables debugging features and logs additional information. */ 5 8 DEVELOPMENT, 9 + 10 + /** Enables production mode, which optimizes performance and disables debugging features. */ 6 11 PRODUCTION, 7 12 } 8 13 ··· 29 34 // Definitions related to our ECS and input management 30 35 private systems: System[] = []; 31 36 private entities: Entity[] = []; 37 + private stores = new Map<string, Map<any, any>>(); 32 38 public readonly input: InputManager; 33 39 40 + /** 41 + * Initializes the Cosmic engine and all necessary support structures in the specified mode. 42 + * @param mode - The mode in which the Cosmic engine should operate. 43 + */ 34 44 private constructor(mode: CosmicMode) { 35 45 this.mode = mode; 36 46 ··· 62 72 window.addEventListener('resize', () => this.handleResize()); 63 73 } 64 74 75 + /** 76 + * Starts the game loop and performs any necessary state changes. 77 + */ 65 78 public start(): void { 66 79 if (!this.isRunning) { 67 80 this.isRunning = true; ··· 70 83 } 71 84 } 72 85 86 + /** 87 + * Stops the game loop and performs any necessary cleanup. 88 + */ 73 89 public stop(): void { 74 90 this.isRunning = false; 75 91 } 76 92 93 + /** 94 + * Executes any relevant engine logic, keeps track of delta time and initiates ECS updates. 95 + * @param currentTime The current time in milliseconds. 96 + */ 77 97 private gameLoop(currentTime: number): void { 78 98 if (!this.isRunning) return; 79 99 ··· 88 108 requestAnimationFrame((time) => this.gameLoop(time)); 89 109 } 90 110 111 + /** 112 + * Executes the update method for all systems registered in Cosmic's Entity Component System. 113 + */ 91 114 private update(deltaTime: number) { 92 115 // Runs through all systems registered in our ECS, 93 - // and updates each one with a list of relevant entities, 94 - // e.g. a PlayerControlled component may be required to control NPCs, 95 - // which is handled by an NpcControlSystem, and so on... 116 + // and updates each one with a list of relevant entities. 96 117 for (const system of this.systems) { 97 118 const relevantEntities = this.entities.filter(entity => 98 119 Array.from(system.requiredComponents).every(name => ··· 106 127 if (this.mode === CosmicMode.DEVELOPMENT) this.calculateFps(deltaTime); 107 128 } 108 129 130 + /** 131 + * Adds an entity to Cosmic's Entity Component System. 132 + */ 109 133 public addEntity(entity: Entity) { 110 134 this.entities.push(entity); 111 135 } 112 136 137 + /** 138 + * Adds a system to Cosmic's Entity Component System. 139 + */ 113 140 public addSystem(system: System) { 114 141 this.systems.push(system); 115 142 } 116 143 144 + /** 145 + * Calculates the FPS and updates the GUI context with the results. 146 + */ 117 147 private calculateFps(deltaTime: number) { 118 148 const fps = 1 / deltaTime; 119 149 ··· 135 165 } 136 166 } 137 167 168 + /** 169 + * Handles resize events on all relevant canvases 170 + * by applying the updated device pixel ratio to them. 171 + */ 138 172 private handleResize(): void { 139 173 const { devicePixelRatio = 1 } = window; 140 174 const width = window.innerWidth; ··· 151 185 this.guiCanvas.style.height = `${height}px`; 152 186 } 153 187 154 - public getContext() { 188 + /** 189 + * Returns the requested store. 190 + * 191 + * @param name - The name of the store to retrieve. 192 + */ 193 + public getStore<K = any, V = any>(name: string): Map<K, V> { 194 + let store = this.stores.get(name); 195 + 196 + if (!store) { 197 + store = new Map<K, V>(); 198 + this.stores.set(name, store); 199 + } 200 + 201 + return store; 202 + } 203 + 204 + /** 205 + * Returns the canvas 2D context. 206 + */ 207 + public getContext(): CanvasRenderingContext2D { 155 208 return this.context; 156 209 } 157 210 211 + /** 212 + * Returns an instance of Cosmic. 213 + * 214 + * @param mode - This defines the mode of Cosmic, if a new instance is created. 215 + */ 158 216 public static getInstance(mode: CosmicMode = CosmicMode.PRODUCTION): Cosmic { 159 217 if (!Cosmic.instance) { 160 218 Cosmic.instance = new Cosmic(mode);
packages/cosmic/src/core/EntityComponentSystem.ts packages/core/src/core/EntityComponentSystem.ts
packages/cosmic/src/core/InputManager.ts packages/core/src/core/InputManager.ts
packages/cosmic/src/index.ts packages/core/src/index.ts
+18
packages/kit/package.json
··· 1 + { 2 + "name": "@cosmic/kit", 3 + "version": "0.1.0", 4 + "description": "Collection of components, systems and helpers to aid developers of games using the Cosmic engine.", 5 + "exports": { 6 + "./components": { 7 + "import": "./src/components.ts", 8 + "require": "./src/components.cjs" 9 + }, 10 + "./systems": { 11 + "import": "./src/systems.ts", 12 + "require": "./src/systems.cjs" 13 + } 14 + }, 15 + "devDependencies": { 16 + "@cosmic/core": "workspace:*" 17 + } 18 + }
+6
packages/kit/src/components.ts
··· 1 + export * from "./components/Transform"; 2 + export * from "./components/Collider"; 3 + export * from "./components/Physics"; 4 + export * from "./components/Renderable"; 5 + export * from "./components/PlayerControlled"; 6 + export * from "./components/WorldProperties";
+4
packages/kit/src/systems.ts
··· 1 + export * from "./systems/RenderingSystem"; 2 + export * from "./systems/PhysicsSystem"; 3 + export * from "./systems/CollisionSystem"; 4 + export * from "./systems/PlayerControlSystem";