diff --git a/example/pong/src/collisions.ts b/example/pong/src/collisions.ts new file mode 100644 index 0000000..224b59a --- /dev/null +++ b/example/pong/src/collisions.ts @@ -0,0 +1,28 @@ +import { Hitbox, Position } from "./components"; +import { ecsLibrary } from "./index"; + +export function checkCollisions(entity: any) { + const entities = ecsLibrary.getZipper([Hitbox, Position]); + + const { x, y } = entity.Position; + const { width, height } = entity.Hitbox; + + for (const e of entities) { + if (e === undefined) continue; + const { x: ex, y: ey } = e.Position; + const { width: ew, height: eh } = e.Hitbox; + + if ( + e.Position != entity.Position && + e.Hitbox != entity.Hitbox && + x < ex + ew && + x + width > ex && + y < ey + eh && + y + height > ey + ) { + return true; + } + } + + return false; +} diff --git a/example/pong/src/components.ts b/example/pong/src/components.ts new file mode 100644 index 0000000..05f58ae --- /dev/null +++ b/example/pong/src/components.ts @@ -0,0 +1,69 @@ +import type { NfgCircle } from "@nanoforge/graphics-2d"; +import type { NfgRectangle } from "@nanoforge/graphics-2d/src/components/shape/shapes/rectangle.shape"; +import type { InputEnum } from "@nanoforge/input"; + +export class Velocity { + name = "Velocity"; + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +export class Position { + name = "Position"; + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +export class Hitbox { + name = "Hitbox"; + height: number; + width: number; + + constructor(width: number, height: number) { + this.height = height; + this.width = width; + } +} + +export class CircleComponent { + name = "CircleComponent"; + component: NfgCircle; + + constructor(component: NfgCircle) { + this.component = component; + } +} + +export class RectangleComponent { + name = "RectangleComponent"; + component: NfgRectangle; + + constructor(component: NfgRectangle) { + this.component = component; + } +} + +export class Bounce { + name = "Bounce"; +} + +export class Controller { + name = "Controller"; + up: InputEnum; + down: InputEnum; + + constructor(up: InputEnum, down: InputEnum) { + this.up = up; + this.down = down; + } +} diff --git a/example/pong/src/index.ts b/example/pong/src/index.ts index ef4b631..e853ee5 100644 --- a/example/pong/src/index.ts +++ b/example/pong/src/index.ts @@ -3,76 +3,118 @@ import { type IRunOptions } from "@nanoforge/common"; import { NanoforgeFactory } from "@nanoforge/core"; import { ECSLibrary } from "@nanoforge/ecs"; import { Graphics2DLibrary } from "@nanoforge/graphics-2d"; +import { InputLibrary } from "@nanoforge/input"; +import { InputEnum } from "@nanoforge/input"; + +import { + Bounce, + CircleComponent, + Controller, + Hitbox, + Position, + RectangleComponent, + Velocity, +} from "./components"; +import { bounce, controlPlayer, drawCircle, drawRectangle, move, moveRectangle } from "./systems"; export const ecsLibrary = new ECSLibrary(); export const app = NanoforgeFactory.createClient(); - -class Velocity { - x: number; - y: number; - - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } -} - -class Position { - x: number; - y: number; - - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } -} - -let lastFrame = 0; - -function move() { - const zip = ecsLibrary.getZipper([Position, Velocity]); - - for (let data = zip.getValue(); data != undefined; data = zip.next()) { - data["Position"].x += data["Velocity"].x; - data["Position"].y += data["Velocity"].y; - } -} - -function logger() { - const zip = ecsLibrary.getZipper([Position]); - - for (let data = zip.getValue(); data != undefined; data = zip.next()) { - console.log(data["Position"].x, data["Position"].y); - } -} - -function framerate(rate: number) { - const frameDuration = 1000 / rate; - let currentFrame = performance.now(); - let elapsedTime = currentFrame - lastFrame; - - while (elapsedTime < frameDuration) { - currentFrame = performance.now(); - elapsedTime = currentFrame - lastFrame; - } - lastFrame = performance.now(); -} +export const graphics = new Graphics2DLibrary(); +export const inputs = new InputLibrary(); export const main = async (options: IRunOptions) => { - app.useGraphics(new Graphics2DLibrary()); + app.useGraphics(graphics); app.useComponentSystem(ecsLibrary); app.useAssetManager(new AssetManagerLibrary()); + app.useInput(inputs); await app.init(options); const ball = ecsLibrary.createEntity(); + ecsLibrary.addComponent(ball, new Velocity(0.04, 0)); + ecsLibrary.addComponent(ball, new Position(0.5, 0)); + ecsLibrary.addComponent(ball, new Bounce()); + ecsLibrary.addComponent( + ball, + new CircleComponent( + await graphics.factory.createCircle({ + radius: 0.1, + color: { r: 1, g: 0, b: 0, a: 1 }, + }), + ), + ); + + const bg = ecsLibrary.createEntity(); + ecsLibrary.addComponent( + bg, + new RectangleComponent( + await graphics.factory.createRectangle({ + min: { x: -2, y: -1 }, + max: { x: 2, y: 1 }, + color: { r: 0, g: 0, b: 0, a: 0 }, + }), + ), + ); + + const topWall = ecsLibrary.createEntity(); + ecsLibrary.addComponent( + topWall, + new RectangleComponent( + await graphics.factory.createRectangle({ + color: { r: 0, g: 0, b: 0, a: 1 }, + }), + ), + ); + ecsLibrary.addComponent(topWall, new Position(-1.8, 0.91)); + ecsLibrary.addComponent(topWall, new Hitbox(3.6, 0.1)); + + const botWall = ecsLibrary.createEntity(); + ecsLibrary.addComponent( + botWall, + new RectangleComponent( + await graphics.factory.createRectangle({ + color: { r: 0, g: 0, b: 0, a: 1 }, + }), + ), + ); + ecsLibrary.addComponent(botWall, new Position(-1.8, -1)); + ecsLibrary.addComponent(botWall, new Hitbox(3.6, 0.1)); + + const player1 = ecsLibrary.createEntity(); + ecsLibrary.addComponent(player1, new Position(-1.8, -0.3)); + ecsLibrary.addComponent(player1, new Velocity(0, 0.1)); + ecsLibrary.addComponent(player1, new Hitbox(0.1, 0.5)); + ecsLibrary.addComponent(player1, new Controller(InputEnum.KeyW, InputEnum.KeyS)); + ecsLibrary.addComponent( + player1, + new RectangleComponent( + await graphics.factory.createRectangle({ + color: { r: 0, g: 0, b: 1, a: 1 }, + }), + ), + ); + + const player2 = ecsLibrary.createEntity(); + ecsLibrary.addComponent(player2, new Position(1.7, -0.3)); + ecsLibrary.addComponent(player2, new Velocity(0, 0.1)); + ecsLibrary.addComponent(player2, new Hitbox(0.1, 0.5)); + ecsLibrary.addComponent(player2, new Controller(InputEnum.ArrowUp, InputEnum.ArrowDown)); + ecsLibrary.addComponent( + player2, + new RectangleComponent( + await graphics.factory.createRectangle({ + color: { r: 0, g: 0, b: 1, a: 1 }, + }), + ), + ); - ecsLibrary.addComponent(ball, new Velocity(2, 2)); - ecsLibrary.addComponent(ball, new Position(50, 50)); ecsLibrary.addSystem(move); - ecsLibrary.addSystem(logger); - ecsLibrary.addSystem(() => framerate(30)); + ecsLibrary.addSystem(controlPlayer); + ecsLibrary.addSystem(moveRectangle); + ecsLibrary.addSystem(drawRectangle); + ecsLibrary.addSystem(drawCircle); + ecsLibrary.addSystem(bounce); app.run(); }; diff --git a/example/pong/src/systems.ts b/example/pong/src/systems.ts new file mode 100644 index 0000000..2a2f6a3 --- /dev/null +++ b/example/pong/src/systems.ts @@ -0,0 +1,81 @@ +import { checkCollisions } from "./collisions"; +import { + Bounce, + CircleComponent, + Controller, + Hitbox, + Position, + RectangleComponent, + Velocity, +} from "./components"; +import { ecsLibrary, graphics, inputs } from "./index"; + +export function move() { + const entities = ecsLibrary.getZipper([Bounce, Position, Velocity]); + + entities.forEach((entity) => { + entity.Position.x += entity.Velocity.x; + entity.Position.y += entity.Velocity.y; + }); +} + +export function bounce() { + const entities = ecsLibrary.getZipper([Bounce, Position, Velocity]); + + entities.forEach((entity) => { + if (entity.Position.x >= 1.6 || entity.Position.x <= -1.6) { + entity.Velocity.x = -entity.Velocity.x; + } + if (entity.Position.y >= 1 || entity.Position.y <= -1) { + entity.Velocity.y = -entity.Velocity.y; + } + }); +} + +export function controlPlayer() { + const entities = ecsLibrary.getZipper([Controller, Position, Hitbox, Velocity]); + + entities.forEach((entity) => { + if (inputs.isKeyPressed(entity.Controller.up) && !checkCollisions(entity)) { + entity.Position.y += entity.Velocity.y; + } else { + entity.Position.y -= entity.Velocity.y; + } + if (inputs.isKeyPressed(entity.Controller.down) && !checkCollisions(entity)) { + entity.Position.y -= entity.Velocity.y; + } else { + entity.Position.y += entity.Velocity.y; + } + }); +} + +export function drawCircle() { + const entities = ecsLibrary.getZipper([CircleComponent, Position]); + + entities.forEach((entity) => { + const pos = entity.Position; + entity.CircleComponent.component.setPosition(pos); + graphics.getWindow().draw(entity.CircleComponent.component); + }); +} + +export function moveRectangle() { + const entities = ecsLibrary.getZipper([RectangleComponent, Position, Hitbox]); + + entities.forEach((entity) => { + const pos = entity.Position; + entity.RectangleComponent.component.setMin({ x: pos.x, y: pos.y }); + entity.RectangleComponent.component.setMax({ + x: pos.x + entity.Hitbox.width, + y: pos.y + entity.Hitbox.height, + }); + }); +} + +export function drawRectangle() { + const entities = ecsLibrary.getZipper([RectangleComponent]); + + entities.forEach((entity) => { + graphics.getWindow().draw(entity.RectangleComponent.component); + }); +}