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

✨ Implement updated Cosmic class and Loop concept

+251 -578
+10
.editorconfig
··· 1 + root = true 2 + 3 + # Unix-style newlines with a newline ending every file 4 + [*] 5 + end_of_line = lf 6 + insert_final_newline = true 7 + 8 + [*.{js,ts,html}] 9 + indent_style = space 10 + indent_size = 4
+2 -1
.oxfmtrc.jsonc
··· 1 1 { 2 - "$schema": "./node_modules/oxfmt/configuration_schema.json", 2 + "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 + "tabWidth": 4, 3 4 }
+39
README.md
··· 1 + # Cosmic 2 + The experimental pixi.js based engine for tile-based 2D games, created strictly for educational purposes. 3 + 4 + ## Requirements 5 + To run the demo project and experiment with the engine, the following tools are required. 6 + 7 + - Bun v1.3+ 8 + 9 + ## Try it out! 10 + You can try out the ever evolving demo project at any time, by running the following commands. 11 + 12 + ```sh 13 + bun install 14 + ``` 15 + 16 + ```sh 17 + bun dev 18 + ``` 19 + 20 + ## Architecture 21 + The following will explain the data-flow and general architecture of the engine, including the main class, the ECS and more! 22 + 23 + ### The `Cosmic` class 24 + 25 + ### The `Loop` class 26 + Loops are singular, scoped and enqueue-able looping directives. 27 + 28 + For example, there might be a "rendering" loop, which draws the state of various entities onto the screen, on each frame, using drawing APIs. 29 + 30 + These are enqueued separately, rather than having one hard-coded main loop, to allow varying execution speeds, like a rendering pipeline running on every frame, and a physics pipeline running at a calculated interval of 60 FPS. 31 + 32 + ### The `Layer` class 33 + 34 + ### The `EntityComponentSystem` class 35 + 36 + ### The `Store` class 37 + Stores are utilized as a means of shared state throughout the engine. 38 + 39 + They are defined as static DTO-like (data transfer object) classes that extend the `Store` class.
-10
bun.lock
··· 16 16 "version": "0.1.0", 17 17 "dependencies": { 18 18 "@cosmic/core": "workspace:*", 19 - "@cosmic/kit": "workspace:*", 20 19 "vite": "^7.2.4", 21 20 }, 22 21 }, 23 - "packages/kit": { 24 - "name": "@cosmic/kit", 25 - "version": "0.1.0", 26 - "devDependencies": { 27 - "@cosmic/core": "workspace:*", 28 - }, 29 - }, 30 22 }, 31 23 "packages": { 32 24 "@cosmic/core": ["@cosmic/core@workspace:packages/core"], 33 25 34 26 "@cosmic/demo": ["@cosmic/demo@workspace:packages/demo"], 35 - 36 - "@cosmic/kit": ["@cosmic/kit@workspace:packages/kit"], 37 27 38 28 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 39 29
+17 -17
package.json
··· 1 1 { 2 - "private": true, 3 - "workspaces": [ 4 - "packages/*" 5 - ], 6 - "contributors": [ 7 - { 8 - "name": "Johannes Reckers", 9 - "email": "contact@thevoid.cafe", 10 - "url": "https://thevoid.cafe" 2 + "private": true, 3 + "contributors": [ 4 + { 5 + "name": "Johannes Reckers", 6 + "email": "contact@thevoid.cafe", 7 + "url": "https://thevoid.cafe" 8 + } 9 + ], 10 + "workspaces": [ 11 + "packages/*" 12 + ], 13 + "scripts": { 14 + "dev": "cd packages/demo && bun dev", 15 + "format": "oxfmt" 16 + }, 17 + "devDependencies": { 18 + "oxfmt": "^0.16.0" 11 19 } 12 - ], 13 - "scripts": { 14 - "dev": "cd packages/demo && bun dev", 15 - "format": "oxfmt" 16 - }, 17 - "devDependencies": { 18 - "oxfmt": "^0.16.0" 19 - } 20 20 }
+16 -16
packages/core/package.json
··· 1 1 { 2 - "name": "@cosmic/core", 3 - "version": "0.1.0", 4 - "description": "Experimental Pixi.js based engine for tile-based 2D games, created strictly for educational purposes.", 5 - "module": "src/index.ts", 6 - "contributors": [ 7 - { 8 - "name": "Johannes Reckers", 9 - "email": "contact@thevoid.cafe", 10 - "url": "https://thevoid.cafe" 11 - } 12 - ], 13 - "exports": { 14 - ".": { 15 - "import": "./src/index.ts", 16 - "require": "./src/index.cjs" 2 + "name": "@cosmic/core", 3 + "version": "0.1.0", 4 + "description": "Experimental Pixi.js based engine for tile-based 2D games, created strictly for educational purposes.", 5 + "contributors": [ 6 + { 7 + "name": "Johannes Reckers", 8 + "email": "contact@thevoid.cafe", 9 + "url": "https://thevoid.cafe" 10 + } 11 + ], 12 + "module": "src/index.ts", 13 + "exports": { 14 + ".": { 15 + "import": "./src/index.ts", 16 + "require": "./src/index.cjs" 17 + } 17 18 } 18 - } 19 19 }
+91 -25
packages/core/src/core/Cosmic.ts
··· 1 - import { System, Entity, InputManager } from "@cosmic/core"; 1 + import { Loop } from "@cosmic/core"; 2 2 3 3 /** 4 4 * Enum representing the different modes in which the Cosmic engine can operate. 5 5 */ 6 6 export enum CosmicMode { 7 - /** Enables debugging features and logs additional information. */ 8 - DEVELOPMENT, 7 + /** Enables debugging features and logs additional information. */ 8 + DEVELOPMENT, 9 + 10 + /** Enables production mode, optimized for high performance. */ 11 + PRODUCTION, 12 + } 13 + 14 + /** 15 + * Enum representing the different states in which the Cosmic engine can operate. 16 + */ 17 + export enum CosmicState { 18 + /** Indicates the main engine pulse to be active. */ 19 + ACTIVE, 9 20 10 - /** Enables production mode, optimized for high performance. */ 11 - PRODUCTION, 21 + /** Indicates the main engine pulse is not currently active. */ 22 + INACTIVE, 12 23 } 13 24 14 25 /** 15 26 * The Cosmic engine, a powerful tool for tile-based 2D games. 27 + * Accessed through the `Cosmic.instance` property. 28 + * 29 + * @example 30 + * const engine = Cosmic.instance; 31 + * 32 + * engine.enqueue(new Loop("rendering", (dt) => { 33 + * console.log(`Delta time: ${dt}`); 34 + * })); 16 35 */ 17 36 export class Cosmic { 18 - /** 19 - * The engine's current, and only, instance. 20 - */ 21 - public _instance?: Cosmic; 37 + protected static _instance?: Cosmic; 38 + public static get instance() { 39 + if (!this._instance) { 40 + this._instance = new Cosmic(CosmicMode.PRODUCTION); 41 + } 22 42 23 - /** 24 - * Returns a reference to the current engine instance. 25 - */ 26 - get instance() { 27 - if (!this._instance) { 28 - this._instance = new Cosmic(CosmicMode.PRODUCTION); 29 - } 43 + return this._instance; 44 + } 30 45 31 - return this._instance; 32 - } 46 + public constructor( 47 + /** The mode in which the engine is operating. */ 48 + public mode: CosmicMode, 33 49 34 - /** 35 - * The mode in which the engine is operating. 36 - */ 37 - public mode: CosmicMode; 50 + /** Declares whether the engine is currently running. */ 51 + protected state: CosmicState = CosmicState.INACTIVE, 52 + ) { 53 + requestAnimationFrame(this.pulse.bind(this)); 54 + } 38 55 39 - public constructor(mode: CosmicMode) { 40 - this.mode = mode; 41 - } 56 + /** The last recorded frame timestamp. */ 57 + protected lastTimestamp: number = performance.now(); 58 + 59 + /** The current calculated deltatime in seconds */ 60 + protected deltaTime: number = 0; 61 + 62 + /** Stores the engine's enqueued external loops */ 63 + protected loops: Loop[] = []; 64 + 65 + /** 66 + * Appends a `Loop` to the engine's main pulse. 67 + * 68 + * @param {Loop} loop - The loop to enqueue. 69 + */ 70 + public enqueue(loop: Loop) { 71 + if (!loop.name) throw new Error("[Cosmic::enqueue] The provided loop is missing the `name` property"); 72 + 73 + if (!loop.pulse) throw new Error("[Cosmic::enqueue] The provided loop is missing a `pulse` method"); 74 + 75 + this.loops.push(loop); 76 + } 77 + 78 + /** Initializes the main engine pulse. */ 79 + public start() { 80 + if (this.state === CosmicState.ACTIVE) return; 81 + 82 + this.state = CosmicState.ACTIVE; 83 + } 84 + 85 + /** Stops the main engine pulse. */ 86 + public stop() { 87 + if (this.state === CosmicState.INACTIVE) return; 88 + 89 + this.state = CosmicState.INACTIVE; 90 + } 91 + 92 + /** The main engine pulse, hooked to requestAnimationFrame. Executes all enqueued loops once per frame, and calculates new deltaTime in seconds. */ 93 + protected pulse(timestamp: number) { 94 + this.deltaTime = (timestamp - this.lastTimestamp) / 1000; 95 + this.lastTimestamp = timestamp; 96 + 97 + for (const loop of this.loops) { 98 + if (this.mode === CosmicMode.DEVELOPMENT) { 99 + console.log(`Running enqueued loop: ${loop.name}`); 100 + } 101 + 102 + loop.pulse(this.deltaTime); 103 + } 104 + 105 + // Move on to the next frame 106 + requestAnimationFrame(this.pulse.bind(this)); 107 + } 42 108 }
+23
packages/core/src/core/Loop.ts
··· 1 + /** 2 + * Defines a singular scoped loop, which may be enqueued in the engine's main loop. 3 + * 4 + * @param {string} name - The unique name for this loop, e.g. "rendering" or "entity-component-system". 5 + * 6 + * @example 7 + * const renderingLoop = new Loop("rendering", (dt: number) => { 8 + * console.log(`Delta time: ${dt}`); 9 + * }); 10 + */ 11 + export class Loop { 12 + public constructor( 13 + /** The unique name for this loop, e.g. "rendering" or "entity-component-system". */ 14 + public name: string, 15 + 16 + /** 17 + * The loop's pulse, called by the engine on requestAnimationFrame. 18 + * 19 + * @param {number} dt - The delta time value passed on from the main engine loop. 20 + */ 21 + public pulse: (dt: number) => void 22 + ) { } 23 + }
+1
packages/core/src/index.ts
··· 1 1 export * from "./core/Cosmic"; 2 + export * from "./core/Loop";
+28 -12
packages/demo/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <meta http-equiv="X-UA-Compatible" content="ie=edge"> 7 - <title>Cosmic Demo</title> 8 - <link rel="stylesheet" href="styles/base.css"> 9 - <link rel="icon" href="./favicon.ico" type="image/png"> 10 - </head> 11 - <body> 12 - <script src="src/index.ts" type="module"></script> 13 - </body> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <meta http-equiv="X-UA-Compatible" content="ie=edge" /> 7 + <title>Cosmic Demo</title> 8 + <link rel="stylesheet" href="styles/base.css" /> 9 + <link rel="icon" href="./favicon.ico" type="image/png" /> 10 + </head> 11 + <body> 12 + <script src="src/index.ts" type="module"></script> 13 + </body> 14 + <style> 15 + * { 16 + margin: 0; 17 + padding: 0; 18 + box-sizing: border-box; 19 + position: relative; 20 + } 21 + 22 + body { 23 + height: 100vh; 24 + width: 100vw; 25 + 26 + color: white; 27 + background-color: black; 28 + } 29 + </style> 14 30 </html>
+13 -14
packages/demo/package.json
··· 1 1 { 2 - "name": "@cosmic/demo", 3 - "version": "0.1.0", 4 - "description": "Simple demo of the cosmic engine.", 5 - "module": "src/index.ts", 6 - "scripts": { 7 - "dev": "vite", 8 - "build": "vite build", 9 - "preview": "vite preview" 10 - }, 11 - "dependencies": { 12 - "vite": "^7.2.4", 13 - "@cosmic/core": "workspace:*", 14 - "@cosmic/kit": "workspace:*" 15 - } 2 + "name": "@cosmic/demo", 3 + "version": "0.1.0", 4 + "description": "Simple demo of the cosmic engine.", 5 + "module": "src/index.ts", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "vite build", 9 + "preview": "vite preview" 10 + }, 11 + "dependencies": { 12 + "@cosmic/core": "workspace:*", 13 + "vite": "^7.2.4" 14 + } 16 15 }
-29
packages/demo/src/LoggingSystem.ts
··· 1 - import { Cosmic, Entity, System } from "@cosmic/core"; 2 - import { Name } from "@cosmic/kit/components"; 3 - 4 - export class LoggingSystem extends System { 5 - private worldStore: Map<string, any>; 6 - 7 - constructor(engine: Cosmic) { 8 - super(); 9 - 10 - this.worldStore = engine.getStore("cosmic.world"); 11 - } 12 - 13 - public readonly requiredComponents = new Set([Name.name]); 14 - 15 - public update(entities: Entity[], _deltaTime: number) { 16 - for (const entity of entities) { 17 - console.log(` 18 - ==================== Entity ==================== 19 - ID: ${entity.id} 20 - Name: '${entity.getComponent(Name)?.name}' 21 - Description: '${entity.getComponent(Name)?.description}' 22 - 23 - =============== World Properties =============== 24 - Gravitational Strength: ${this.worldStore.get("cosmic.world.gravity.strength") ?? 0} 25 - Cell Size: ${this.worldStore.get("cosmic.world.grid.cellSize") ?? 0} 26 - `); 27 - } 28 - } 29 - }
+11 -108
packages/demo/src/index.ts
··· 1 - import { Cosmic, CosmicMode, Entity } from "@cosmic/core"; 2 - import { 3 - Transform, 4 - Collider, 5 - Renderable, 6 - Physics, 7 - PlayerControlled, 8 - Movable, 9 - Name, 10 - } from "@cosmic/kit/components"; 11 - import { 12 - RenderingSystem, 13 - PhysicsSystem, 14 - CollisionSystem, 15 - PlayerControlSystem, 16 - } from "@cosmic/kit/systems"; 17 - import { LoggingSystem } from "./LoggingSystem"; 18 - 19 - //====================================== 20 - // Initialize engine and systems 21 - //====================================== 22 - const engine = Cosmic.getInstance(CosmicMode.DEVELOPMENT); 1 + import { Cosmic, Loop } from "@cosmic/core"; 23 2 24 - engine.addSystem(new PlayerControlSystem(engine)); 25 - engine.addSystem(new PhysicsSystem(engine)); 26 - engine.addSystem(new CollisionSystem()); 27 - engine.addSystem(new RenderingSystem(engine)); 28 - engine.addSystem(new LoggingSystem(engine)); 29 - 30 - engine.start(); 3 + console.log("Hiii world :3"); 31 4 32 - //====================================== 33 - // Initialize crucial stores 34 - //====================================== 35 - const worldStore = engine.getStore("cosmic.world"); 36 - worldStore.set("cosmic.world.gravity.strength", 2); 37 - worldStore.set("cosmic.world.grid.cellSize", 32); 5 + const cosmicDeltaWrapper = document.createElement("p"); 6 + const cosmicDeltaDisplay = document.createElement("span"); 38 7 39 - //====================================== 40 - // EXPERIMENTATION AREA 41 - //====================================== 42 - const createTestEntity = (options: { 43 - initialX: number; 44 - initialY: number; 45 - isStatic: boolean; 46 - inputs: string[]; 47 - }) => { 48 - const worldGridCellSize = worldStore.get("cosmic.world.grid.cellSize"); 8 + cosmicDeltaWrapper.innerText = "The current delta time is... " 9 + cosmicDeltaWrapper.appendChild(cosmicDeltaDisplay); 10 + document.body.appendChild(cosmicDeltaWrapper); 49 11 50 - const entity = new Entity(); 51 - entity.addComponent( 52 - new Transform(options.initialX, options.initialY, worldGridCellSize * 2, worldGridCellSize * 2), 53 - ); 54 - entity.addComponent(new PlayerControlled(500, ...options.inputs)); 55 - entity.addComponent(new Renderable()); 56 - entity.addComponent(new Movable()); 57 - entity.addComponent(new Physics()); 58 - entity.addComponent(new Name("player", "This is a player controlled entity.")); 59 - entity.addComponent(new Collider(options.isStatic)); 60 - 61 - return entity; 62 - }; 63 - 64 - engine.addEntity( 65 - createTestEntity({ 66 - initialX: 0, 67 - initialY: 0, 68 - isStatic: false, 69 - inputs: ["w", "s", "a", "d"], 70 - }), 71 - ); 72 - engine.addEntity( 73 - createTestEntity({ 74 - initialX: 30, 75 - initialY: 200, 76 - isStatic: false, 77 - inputs: ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"], 78 - }), 79 - ); 80 - 81 - // Create a static object entity, e.g. a world tile. 82 - const createObjectEntity = (options: { initialX: number; initialY: number }) => { 83 - const worldGridCellSize = worldStore.get("cosmic.world.grid.cellSize"); 84 - 85 - const entity = new Entity(); 86 - entity.addComponent( 87 - new Transform(options.initialX, options.initialY, worldGridCellSize, worldGridCellSize), 88 - ); 89 - entity.addComponent(new Renderable()); 90 - entity.addComponent(new Movable()); 91 - entity.addComponent(new Physics(0)); 92 - entity.addComponent(new Name("object", "This is a static object entity.")); 93 - entity.addComponent(new Collider(true)); 94 - 95 - return entity; 96 - }; 97 - 98 - engine.addEntity( 99 - createObjectEntity({ 100 - initialX: 0, 101 - initialY: 900, 102 - }), 103 - ); 104 - 105 - for (let i = 0; i < 70; i++) { 106 - engine.addEntity( 107 - createObjectEntity({ 108 - initialX: i * worldStore.get("cosmic.world.grid.cellSize"), 109 - initialY: 900, 110 - }), 111 - ); 112 - } 12 + const engine = Cosmic.instance; 13 + engine.enqueue(new Loop("rendering", (dt) => { 14 + cosmicDeltaDisplay.innerText = dt.toString(); 15 + }));
-74
packages/demo/styles/base.css
··· 1 - * { 2 - margin: 0; 3 - padding: 0; 4 - position: relative; 5 - box-sizing: border-box; 6 - } 7 - 8 - :root { 9 - --color-bg0: #10131B; 10 - --color-bg1: #13161e; 11 - --color-bg2: #242e34; 12 - --color-bg-gradient: linear-gradient(to bottom, var(--color-bg1), var(--color-primary)); 13 - 14 - --color-text: #DFE5E8; 15 - 16 - --color-primary: #70b9ae; 17 - --color-secondary: #bf1d70; 18 - --color-success: var(--color-primary); 19 - --color-danger: var(--color-secondary); 20 - 21 - --color-border: rgba(200, 220, 229, 0.15); 22 - 23 - --color-shadow: rgba(0, 0, 0, 0.3); 24 - 25 - --color-overlay-glow: var(--color-primary); 26 - } 27 - 28 - * { 29 - padding: 0; 30 - margin: 0; 31 - box-sizing: border-box; 32 - position: relative; 33 - } 34 - 35 - body { 36 - width: 100vw; 37 - height: 100vh; 38 - 39 - overflow: hidden; 40 - 41 - background: var(--color-bg0); 42 - 43 - &::after { 44 - content: ""; 45 - 46 - position: absolute; 47 - top: 0; 48 - left: 0; 49 - right: 0; 50 - bottom: 0; 51 - 52 - opacity: 0.05; 53 - background-image: linear-gradient(rgba(0, 255, 65, 0.5) 1px, 54 - transparent 1px), 55 - linear-gradient(90deg, rgba(0, 255, 65, 0.5) 1px, transparent 1px); 56 - background-size: 32px 32px; 57 - 58 - pointer-events: none; 59 - z-index: 1; 60 - } 61 - } 62 - 63 - .cosmic-layer { 64 - position: absolute; 65 - top: 0; 66 - left: 0; 67 - 68 - width: 100%; 69 - height: 100%; 70 - } 71 - 72 - .cosmic-gui { 73 - z-index: 1; 74 - }
-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 - }
-7
packages/kit/src/components.ts
··· 1 - export * from "./components/PlayerControlled"; 2 - export * from "./components/Renderable"; 3 - export * from "./components/Transform"; 4 - export * from "./components/Collider"; 5 - export * from "./components/Physics"; 6 - export * from "./components/Movable"; 7 - export * from "./components/Name";
-3
packages/kit/src/components/Collider.ts
··· 1 - export class Collider { 2 - constructor(public isStatic: boolean = false) {} 3 - }
-3
packages/kit/src/components/Movable.ts
··· 1 - export class Movable { 2 - constructor(public speed: number = 500) {} 3 - }
-6
packages/kit/src/components/Name.ts
··· 1 - export class Name { 2 - constructor( 3 - public name: string, 4 - public description: string, 5 - ) {} 6 - }
-3
packages/kit/src/components/Physics.ts
··· 1 - export class Physics { 2 - constructor(public weight: number = 120) {} 3 - }
-9
packages/kit/src/components/PlayerControlled.ts
··· 1 - export class PlayerControlled { 2 - constructor( 3 - public speed = 500, 4 - public inputUp = "w", 5 - public inputDown = "s", 6 - public inputLeft = "a", 7 - public inputRight = "d", 8 - ) {} 9 - }
-1
packages/kit/src/components/Renderable.ts
··· 1 - export class Renderable {}
-12
packages/kit/src/components/Transform.ts
··· 1 - export class Transform { 2 - constructor( 3 - public x: number = 0, 4 - public y: number = 0, 5 - public width: number = 32, 6 - public height: number = 32, 7 - 8 - // Todo: Consider moving this to Collider.ts 9 - public isCollidingWithEntity: boolean = false, 10 - public isCollidingWithCanvasBoundaries: boolean = false, 11 - ) {} 12 - }
-4
packages/kit/src/systems.ts
··· 1 - export * from "./systems/PlayerControlSystem"; 2 - export * from "./systems/CollisionSystem"; 3 - export * from "./systems/RenderingSystem"; 4 - export * from "./systems/PhysicsSystem";
-129
packages/kit/src/systems/CollisionSystem.ts
··· 1 - import { Entity, System } from "@cosmic/core"; 2 - import { Transform, Collider } from "@cosmic/kit/components"; 3 - 4 - export class CollisionSystem extends System { 5 - public readonly requiredComponents = new Set([Transform.name, Collider.name]); 6 - 7 - public update(entities: Entity[], _deltaTime: number) { 8 - // Reset collision flag for all first 9 - for (const entity of entities) { 10 - const transform = entity.getComponent(Transform)!; 11 - transform.isCollidingWithEntity = false; 12 - } 13 - 14 - // Check each pair once 15 - for (let i = 0; i < entities.length; i++) { 16 - const entityA = entities[i]; 17 - const aTransform = entityA.getComponent(Transform)!; 18 - const aCollider = entityA.getComponent(Collider)!; 19 - 20 - for (let j = i + 1; j < entities.length; j++) { 21 - const entityB = entities[j]; 22 - const bTransform = entityB.getComponent(Transform)!; 23 - const bCollider = entityB.getComponent(Collider)!; 24 - 25 - if (this.areColliding(aTransform, bTransform)) { 26 - aTransform.isCollidingWithEntity = true; 27 - bTransform.isCollidingWithEntity = true; 28 - 29 - this.resolveOverlap(aTransform, bTransform, aCollider, bCollider); 30 - } 31 - } 32 - } 33 - } 34 - 35 - protected areColliding(a: Transform, b: Transform): boolean { 36 - // AABB vs AABB 37 - return ( 38 - a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y 39 - ); 40 - } 41 - 42 - protected resolveOverlap(a: Transform, b: Transform, colA: Collider, colB: Collider) { 43 - // Centers 44 - const centerAx = a.x + a.width / 2; 45 - const centerAy = a.y + a.height / 2; 46 - const centerBx = b.x + b.width / 2; 47 - const centerBy = b.y + b.height / 2; 48 - 49 - const dx = centerAx - centerBx; 50 - const dy = centerAy - centerBy; 51 - 52 - const combinedHalfWidths = (a.width + b.width) / 2; 53 - const combinedHalfHeights = (a.height + b.height) / 2; 54 - 55 - const overlapX = combinedHalfWidths - Math.abs(dx); 56 - const overlapY = combinedHalfHeights - Math.abs(dy); 57 - 58 - if (overlapX <= 0 || overlapY <= 0) return; // just in case 59 - 60 - const aStatic = colA.isStatic; 61 - const bStatic = colB.isStatic; 62 - 63 - // Decide which axis to resolve along (least penetration) 64 - if (overlapX < overlapY) { 65 - // Horizontal 66 - if (dx > 0) { 67 - // A is to the right of B 68 - this.pushApartX(a, b, overlapX, +1, aStatic, bStatic); 69 - } else { 70 - // A is to the left of B 71 - this.pushApartX(a, b, overlapX, -1, aStatic, bStatic); 72 - } 73 - } else { 74 - // Vertical 75 - if (dy > 0) { 76 - // A is below B 77 - this.pushApartY(a, b, overlapY, +1, aStatic, bStatic); 78 - } else { 79 - // A is above B 80 - this.pushApartY(a, b, overlapY, -1, aStatic, bStatic); 81 - } 82 - } 83 - } 84 - 85 - private pushApartX( 86 - a: Transform, 87 - b: Transform, 88 - overlap: number, 89 - direction: 1 | -1, // +1: A right of B, -1: A left of B 90 - aStatic: boolean, 91 - bStatic: boolean, 92 - ) { 93 - if (aStatic && bStatic) return; 94 - 95 - if (aStatic && !bStatic) { 96 - // Only move B 97 - b.x -= direction * overlap; 98 - } else if (!aStatic && bStatic) { 99 - // Only move A 100 - a.x += direction * overlap; 101 - } else { 102 - // Both dynamic → split 103 - const half = overlap / 2; 104 - a.x += direction * half; 105 - b.x -= direction * half; 106 - } 107 - } 108 - 109 - private pushApartY( 110 - a: Transform, 111 - b: Transform, 112 - overlap: number, 113 - direction: 1 | -1, // +1: A below B, -1: A above B 114 - aStatic: boolean, 115 - bStatic: boolean, 116 - ) { 117 - if (aStatic && bStatic) return; 118 - 119 - if (aStatic && !bStatic) { 120 - b.y -= direction * overlap; 121 - } else if (!aStatic && bStatic) { 122 - a.y += direction * overlap; 123 - } else { 124 - const half = overlap / 2; 125 - a.y += direction * half; 126 - b.y -= direction * half; 127 - } 128 - } 129 - }
-23
packages/kit/src/systems/PhysicsSystem.ts
··· 1 - import { Entity, System, type Cosmic } from "@cosmic/core"; 2 - import { Transform, Physics } from "@cosmic/kit/components"; 3 - 4 - export class PhysicsSystem extends System { 5 - public requiredComponents = new Set([Transform.name, Physics.name]); 6 - private worldStore: Map<string, any>; 7 - 8 - constructor(engine: Cosmic) { 9 - super(); 10 - this.worldStore = engine.getStore("cosmic.world"); 11 - } 12 - 13 - update(entities: Entity[], deltaTime: number) { 14 - const worldGravityStrength = this.worldStore.get("cosmic.world.gravity.strength") ?? 0; 15 - 16 - entities.forEach((entity) => { 17 - const transform = entity.getComponent(Transform)!; 18 - const physics = entity.getComponent(Physics)!; 19 - 20 - transform.y += worldGravityStrength * physics.weight * deltaTime; 21 - }); 22 - } 23 - }
-28
packages/kit/src/systems/PlayerControlSystem.ts
··· 1 - import { Cosmic, System, Entity } from "@cosmic/core"; 2 - import { Transform, PlayerControlled } from "@cosmic/kit/components"; 3 - 4 - export class PlayerControlSystem extends System { 5 - public requiredComponents = new Set([Transform.name, PlayerControlled.name]); 6 - 7 - constructor(private engine: Cosmic) { 8 - super(); 9 - } 10 - 11 - update(entities: Entity[], deltaTime: number): void { 12 - const input = this.engine.input; 13 - 14 - entities.forEach((entity) => { 15 - const transform = entity.getComponent(Transform)!; 16 - const playerControlled = entity.getComponent(PlayerControlled)!; 17 - 18 - if (input.isKeyDown(playerControlled.inputUp)) 19 - transform.y -= playerControlled.speed * deltaTime; 20 - if (input.isKeyDown(playerControlled.inputDown)) 21 - transform.y += playerControlled.speed * deltaTime; 22 - if (input.isKeyDown(playerControlled.inputLeft)) 23 - transform.x -= playerControlled.speed * deltaTime; 24 - if (input.isKeyDown(playerControlled.inputRight)) 25 - transform.x += playerControlled.speed * deltaTime; 26 - }); 27 - } 28 - }
-26
packages/kit/src/systems/RenderingSystem.ts
··· 1 - import { Entity, System, type Cosmic } from "@cosmic/core"; 2 - import { Transform, Renderable } from "@cosmic/kit/components"; 3 - 4 - export class RenderingSystem extends System { 5 - public requiredComponents = new Set([Transform.name, Renderable.name]); 6 - private context: CanvasRenderingContext2D; 7 - 8 - constructor(engine: Cosmic) { 9 - super(); 10 - this.context = engine.getContext(); 11 - } 12 - 13 - public update(entities: Entity[]): void { 14 - this.context.clearRect(0, 0, 9999, 9999); 15 - 16 - entities.forEach((entity) => { 17 - const transform = entity.getComponent(Transform)!; 18 - 19 - this.context.strokeStyle = "#bf1d70"; 20 - if (transform.isCollidingWithEntity) this.context.strokeStyle = "#d6a5b0"; 21 - if (transform.isCollidingWithCanvasBoundaries) this.context.strokeStyle = "purple"; 22 - 23 - this.context.strokeRect(transform.x, transform.y, transform.width, transform.height); 24 - }); 25 - } 26 - }