diff --git a/packages/demos/game-playground/src/index.ts b/packages/demos/game-playground/src/index.ts index 33cd9f4..d7853a1 100644 --- a/packages/demos/game-playground/src/index.ts +++ b/packages/demos/game-playground/src/index.ts @@ -25,7 +25,7 @@ import AttackingWithClubSystem from "./system/AttackingWithClubSystem"; import MatrixSystem from "./system/MatrixSystem"; import { createHtmlUiElements, PreRenderTiledMapSystem } from "@serbanghita-gamedev/renderer"; -async function runGame() { +async function setup() { // 0. Create the UI and canvas. const [, CANVAS_BACKGROUND, CANVAS_FOREGROUND] = createHtmlUiElements(); @@ -106,4 +106,4 @@ async function runGame() { }; } -runGame().then(() => console.log("Game started ...")); +setup().then(() => console.log("Game started ...")); diff --git a/packages/demos/game-playground/src/system/RenderSystem.ts b/packages/demos/game-playground/src/system/RenderSystem.ts index d942061..e252a19 100644 --- a/packages/demos/game-playground/src/system/RenderSystem.ts +++ b/packages/demos/game-playground/src/system/RenderSystem.ts @@ -1,5 +1,5 @@ import { System, Query, World } from "@serbanghita-gamedev/ecs"; -import { clearCtx, getCtx, renderImage, renderRectangle } from "@serbanghita-gamedev/renderer"; +import { clearCtx, getCtx, image, rectangle } from "@serbanghita-gamedev/renderer"; import { Position, SpriteSheet } from "@serbanghita-gamedev/component"; import IsWalking from "../component/IsWalking"; import IsIdle from "../component/IsIdle"; @@ -66,7 +66,7 @@ export default class RenderSystem extends System { ); } - renderImage( + image( getCtx(this.CANVAS), spriteSheetImg, // source @@ -81,7 +81,7 @@ export default class RenderSystem extends System { animationFrame.height, ); - renderRectangle( + rectangle( getCtx(this.CANVAS) as CanvasRenderingContext2D, destPositionX, destPositionY, @@ -90,7 +90,7 @@ export default class RenderSystem extends System { "#cccccc", ); - renderRectangle( + rectangle( getCtx(this.CANVAS) as CanvasRenderingContext2D, destPositionX + hitboxOffset.x, destPositionY + hitboxOffset.y, diff --git a/packages/demos/tiled-rendering/src/IsCollisionTile.ts b/packages/demos/tiled-rendering/src/IsCollisionTile.ts new file mode 100644 index 0000000..ce007e4 --- /dev/null +++ b/packages/demos/tiled-rendering/src/IsCollisionTile.ts @@ -0,0 +1,16 @@ +import { Component } from "@serbanghita-gamedev/ecs"; +import { Point } from "@serbanghita-gamedev/geometry"; + +export interface IsCollisionTileProps { + x: number; + y: number; + point: Point; + // Tile index. + tile: number; +} + +export default class IsCollisionTile extends Component { + constructor(public properties: IsCollisionTileProps) { + super(properties); + } +} diff --git a/packages/demos/tiled-rendering/src/IsMatrix.ts b/packages/demos/tiled-rendering/src/IsMatrix.ts new file mode 100644 index 0000000..22ae762 --- /dev/null +++ b/packages/demos/tiled-rendering/src/IsMatrix.ts @@ -0,0 +1,18 @@ +import { Component } from "@serbanghita-gamedev/ecs"; + +export interface IsMatrixProps { + // Flat array matrix with all the possible entities, obstacles, etc. + matrix: number[]; + // Width in tiles. + width: number; + // Height in tiles. + height: number; + // Size of the tile in pixels. + tileSize: number; +} + +export default class IsMatrix extends Component { + constructor(public properties: IsMatrixProps) { + super(properties); + } +} diff --git a/packages/demos/tiled-rendering/src/IsPlayer.ts b/packages/demos/tiled-rendering/src/IsPlayer.ts new file mode 100644 index 0000000..20c27e6 --- /dev/null +++ b/packages/demos/tiled-rendering/src/IsPlayer.ts @@ -0,0 +1,7 @@ +import { Component } from "@serbanghita-gamedev/ecs"; + +export default class IsPlayer extends Component { + constructor(public properties: Record) { + super(properties); + } +} diff --git a/packages/demos/tiled-rendering/src/IsPreRendered.ts b/packages/demos/tiled-rendering/src/IsPreRendered.ts new file mode 100644 index 0000000..8d9f924 --- /dev/null +++ b/packages/demos/tiled-rendering/src/IsPreRendered.ts @@ -0,0 +1,7 @@ +import { Component } from "@serbanghita-gamedev/ecs"; + +export default class IsPreRendered extends Component { + constructor(public properties: Record) { + super(properties); + } +} diff --git a/packages/demos/tiled-rendering/src/IsRenderedInForeground.ts b/packages/demos/tiled-rendering/src/IsRenderedInForeground.ts new file mode 100644 index 0000000..39d1e28 --- /dev/null +++ b/packages/demos/tiled-rendering/src/IsRenderedInForeground.ts @@ -0,0 +1,7 @@ +import { Component } from "@serbanghita-gamedev/ecs"; + +export default class IsRenderedInForeground extends Component { + constructor(public properties: Record) { + super(properties); + } +} diff --git a/packages/demos/tiled-rendering/src/MoveSystem.ts b/packages/demos/tiled-rendering/src/MoveSystem.ts new file mode 100644 index 0000000..baaa483 --- /dev/null +++ b/packages/demos/tiled-rendering/src/MoveSystem.ts @@ -0,0 +1,41 @@ +import { System, Query, World, Entity } from "@serbanghita-gamedev/ecs"; +import { QuadTree } from "@serbanghita-gamedev/quadtree"; +import PhysicsBody from "./PhysicsBody"; +import { randomInt } from "./helpers"; +import IsCollisionTile from "./IsCollisionTile"; +import IsMatrix from "./IsMatrix"; +import { getTileFromCoordinates } from "@serbanghita-gamedev/matrix"; + +export default class MoveSystem extends System { + public constructor( + public world: World, + public query: Query, + ) { + super(world, query); + } + + public update(now) { + const map = this.world.getEntity("map"); + if (!map) { + throw new Error(`Map entity is not defined.`); + } + const matrixComponent = map.getComponent(IsMatrix); + const matrix = matrixComponent.properties.matrix; + + this.query.execute().forEach((entity) => { + const tile = entity.getComponent(IsCollisionTile); + + let futureX = tile.properties.x + randomInt(-1, 1); + let futureY = tile.properties.y + randomInt(-1, 1); + + const futureTile = getTileFromCoordinates(futureX, futureY, matrixComponent.properties); + if (matrix[futureTile] === 0) { + tile.properties.x = futureX; + tile.properties.y = futureY; + tile.properties.point.x = futureX; + tile.properties.point.y = futureY; + tile.properties.tile = futureTile; + } + }); + } +} diff --git a/packages/demos/tiled-rendering/src/PhysicsBody.ts b/packages/demos/tiled-rendering/src/PhysicsBody.ts new file mode 100644 index 0000000..e498e7d --- /dev/null +++ b/packages/demos/tiled-rendering/src/PhysicsBody.ts @@ -0,0 +1,17 @@ +import { Component } from "@serbanghita-gamedev/ecs"; +import { Rectangle, Point } from "@serbanghita-gamedev/geometry"; + +export interface PhysicsBodyProps { + width: number; + height: number; + x: number; + y: number; + rectangle: Rectangle; + point: Point; +} + +export default class PhysicsBody extends Component { + constructor(public properties: PhysicsBodyProps) { + super(properties); + } +} diff --git a/packages/demos/tiled-rendering/src/PreRenderCollisionTilesSystem.ts b/packages/demos/tiled-rendering/src/PreRenderCollisionTilesSystem.ts new file mode 100644 index 0000000..e0b1fe4 --- /dev/null +++ b/packages/demos/tiled-rendering/src/PreRenderCollisionTilesSystem.ts @@ -0,0 +1,21 @@ +import { System, Query, World, Entity } from "@serbanghita-gamedev/ecs"; +import IsCollisionTile from "./IsCollisionTile"; +import { dot } from "@serbanghita-gamedev/renderer"; + +export default class PreRenderCollisionTilesSystem extends System { + public constructor( + public world: World, + public query: Query, + public ctx: CanvasRenderingContext2D, + ) { + super(world, query); + } + + public update(now: number): void { + this.query.execute().forEach((entity) => { + const tile = entity.getComponent(IsCollisionTile); + const point = tile.properties.point; + dot(this.ctx, point.x, point.y, "rgb(255,0,0)", 2); + }); + } +} diff --git a/packages/demos/tiled-rendering/src/QuadTreeSystem.ts b/packages/demos/tiled-rendering/src/QuadTreeSystem.ts new file mode 100644 index 0000000..2b52669 --- /dev/null +++ b/packages/demos/tiled-rendering/src/QuadTreeSystem.ts @@ -0,0 +1,27 @@ +import { System, Query, World } from "@serbanghita-gamedev/ecs"; +import { QuadTree } from "@serbanghita-gamedev/quadtree"; +import IsCollisionTile from "./IsCollisionTile"; + +export default class QuadTreeSystem extends System { + public constructor( + public world: World, + public query: Query, + public quadtree: QuadTree, + ) { + super(world, query); + } + public update(now: number): void { + this.preUpdate(); + + if (this.isPaused) { + return; + } + + this.quadtree.clear(); + + this.query.execute().forEach((entity) => { + const tile = entity.getComponent(IsCollisionTile); + this.quadtree.addPoint(tile.properties.point); + }); + } +} diff --git a/packages/demos/tiled-rendering/src/RenderingSystem.ts b/packages/demos/tiled-rendering/src/RenderingSystem.ts new file mode 100644 index 0000000..29526d8 --- /dev/null +++ b/packages/demos/tiled-rendering/src/RenderingSystem.ts @@ -0,0 +1,36 @@ +import { System, Query, World, Entity } from "@serbanghita-gamedev/ecs"; +import IsCollisionTile from "./IsCollisionTile"; +import { dot, rectangle, text } from "@serbanghita-gamedev/renderer"; +import { QuadTree } from "@serbanghita-gamedev/quadtree"; + +export default class RenderingSystem extends System { + public constructor( + public world: World, + public query: Query, + public ctx: CanvasRenderingContext2D, + public quadtree: QuadTree, + ) { + super(world, query); + } + + private renderQuadTree(quadtree: QuadTree) { + rectangle(this.ctx, quadtree.area.topLeftX, quadtree.area.topLeftY, quadtree.area.width, quadtree.area.height, "rgba(0,0,0,0.5)"); + + Object.values(quadtree.quadrants).forEach((subQuadtree) => { + this.renderQuadTree(subQuadtree); + }); + } + + public update(now: number): void { + this.ctx.clearRect(0, 0, 640, 480); + + this.query.execute().forEach((entity) => { + const tile = entity.getComponent(IsCollisionTile); + const point = tile.properties.point; + dot(this.ctx, point.x, point.y, "rgb(0,255,0)", 6); + text(this.ctx, `${tile.properties.tile}`, point.x, point.y, "10", "arial", "", "black"); + }); + + this.renderQuadTree(this.quadtree); + } +} diff --git a/packages/demos/tiled-rendering/src/helpers.ts b/packages/demos/tiled-rendering/src/helpers.ts new file mode 100644 index 0000000..e42869f --- /dev/null +++ b/packages/demos/tiled-rendering/src/helpers.ts @@ -0,0 +1,3 @@ +export function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} \ No newline at end of file diff --git a/packages/demos/tiled-rendering/src/index.ts b/packages/demos/tiled-rendering/src/index.ts index 39f734d..30b5db9 100644 --- a/packages/demos/tiled-rendering/src/index.ts +++ b/packages/demos/tiled-rendering/src/index.ts @@ -1,20 +1,30 @@ // 0. Create the UI and canvas. -import { createWrapperElement, createCanvas, run, dot } from "@serbanghita-gamedev/renderer"; +import { createWrapperElement, createCanvas, run, dot, rectangle } from "@serbanghita-gamedev/renderer"; import { World } from "@serbanghita-gamedev/ecs"; import { PreRenderTiledMapSystem } from "@serbanghita-gamedev/renderer"; -import { IsTiledMap } from "@serbanghita-gamedev/component"; +import { IsTiledMap, Renderable } from "@serbanghita-gamedev/component"; import { loadSprites } from "./assets"; -import { Point } from "@serbanghita-gamedev/geometry"; - -export function randomInt(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min; -} +import { Point, Rectangle } from "@serbanghita-gamedev/geometry"; +import { randomInt } from "./helpers"; +import IsMatrix from "./IsMatrix"; +import { TiledMap } from "@serbanghita-gamedev/tiled"; +import { getTileCoordinates } from "@serbanghita-gamedev/matrix"; +import { QuadTree } from "@serbanghita-gamedev/quadtree"; +import QuadTreeSystem from "./QuadTreeSystem"; +import IsCollisionTile from "./IsCollisionTile"; +import IsPreRendered from "./IsPreRendered"; +import PreRenderCollisionTilesSystem from "./PreRenderCollisionTilesSystem"; +import IsRenderedInForeground from "./IsRenderedInForeground"; +import RenderingSystem from "./RenderingSystem"; +import IsPlayer from "./IsPlayer"; +import MoveSystem from "./MoveSystem"; async function setup() { const HTML_WRAPPER = createWrapperElement("game-wrapper", 640, 480); const CANVAS_FOREGROUND = createCanvas("foreground", 640, 480, "2"); const CTX_FOREGROUND = CANVAS_FOREGROUND.getContext("2d") as CanvasRenderingContext2D; const CANVAS_BACKGROUND = createCanvas("background", 640, 480, "1"); + const CTX_BACKGROUND = CANVAS_BACKGROUND.getContext("2d") as CanvasRenderingContext2D; HTML_WRAPPER.appendChild(CANVAS_FOREGROUND); HTML_WRAPPER.appendChild(CANVAS_BACKGROUND); document.body.appendChild(HTML_WRAPPER); @@ -24,25 +34,66 @@ async function setup() { // Create the current "World" (scene). const world = new World(); - world.declarations.components.registerComponent(IsTiledMap); + world.registerComponent(IsTiledMap); + world.registerComponent(IsMatrix); + world.registerComponent(IsCollisionTile); + world.registerComponent(IsPreRendered); + world.registerComponent(IsRenderedInForeground); + world.registerComponent(IsPlayer); - const CustomMapEntity = world.createEntity("map"); - CustomMapEntity.addComponent(IsTiledMap, { mapFile: require("./assets/maps/E1MM2.json"), mapFilePath: "./assets/maps/E1MM2.json" }); + // Load the map from Tiled json file declaration. + // Create a dedicated map entity. + const map = world.createEntity("map"); + map.addComponent(IsTiledMap, { mapFile: require("./assets/maps/E1MM2.json"), mapFilePath: "./assets/maps/E1MM2.json" }); + // Load the "TiledMap" class wrapper over the json file declaration. + const tiledMap = new TiledMap(map.getComponent(IsTiledMap).properties.mapFile); + // Add the "collision" layer data to the map. + const collisionLayer = tiledMap.getCollisionLayers()[0]; + map.addComponent(IsMatrix, { matrix: collisionLayer.data, width: collisionLayer.width, height: collisionLayer.height, tileSize: 16 }); + + // Transform all collision tiles as entities. + collisionLayer.data.forEach((tileValue, tileIndex) => { + if (tileValue > 0) { + const entityId = `collision-tile-${tileIndex}`; + const collisionTileEntity = world.createEntity(entityId); + let { x, y } = getTileCoordinates(tileIndex, map.getComponent(IsMatrix).properties); + x = x + 16 / 2; + y = y + 16 / 2; + collisionTileEntity.addComponent(IsCollisionTile, { x, y, point: new Point(x, y, entityId) }); + collisionTileEntity.addComponent(IsPreRendered); + } + }); const TiledMapQuery = world.createQuery("TiledMapQuery", { all: [IsTiledMap] }); world.createSystem(PreRenderTiledMapSystem, TiledMapQuery, CANVAS_BACKGROUND, SPRITES["./assets/sprites/terrain.png"]).runOnlyOnce(); - const point = new Point(640 / 2, 480 / 2); + // Quadtree + const area = new Rectangle(640, 480, new Point(640 / 2, 480 / 2)); + const quadtree = new QuadTree(area, 5, 5); - world.start(() => { - CTX_FOREGROUND.clearRect(0, 0, 640, 480); - // No need to re-render the "background" layer if it's static. - // image(CTX_FOREGROUND, CANVAS_BACKGROUND, 0, 0, 640, 480, 0, 0, 640, 480); + const CollisionTilesQuery = world.createQuery("CollisionTilesQuery", { all: [IsCollisionTile] }); + world.createSystem(PreRenderCollisionTilesSystem, CollisionTilesQuery, CTX_BACKGROUND).runOnlyOnce(); - point.x += randomInt(-2, 2); - point.y += randomInt(-2, 2); - dot(CTX_FOREGROUND, point.x, point.y, "rgb(255,0,0)", 10); + const RenderingQuery = world.createQuery("RenderingQuery", { all: [IsRenderedInForeground] }); + world.createSystem(RenderingSystem, RenderingQuery, CTX_FOREGROUND, quadtree); + const IsPlayerQuery = world.createQuery("IsPlayerQuery", { all: [IsPlayer] }); + world.createSystem(MoveSystem, IsPlayerQuery); + world.createSystem(QuadTreeSystem, IsPlayerQuery, quadtree); + + CANVAS_FOREGROUND.addEventListener("dblclick", (event) => { + const x = event.clientX | 0; + const y = event.clientY | 0; + + const playerId = `player-${x}-${y}`; + const player = world.createEntity(playerId); + const point = new Point(x, y, playerId); + player.addComponent(IsCollisionTile, { x, y, point }); + player.addComponent(IsRenderedInForeground); + player.addComponent(IsPlayer); + quadtree.addPoint(point); }); + + world.start(0, () => {}); } setup().then(() => console.log("Game started ...")); diff --git a/packages/ecs/src/Query.ts b/packages/ecs/src/Query.ts index bb0389d..e2b6ea6 100644 --- a/packages/ecs/src/Query.ts +++ b/packages/ecs/src/Query.ts @@ -32,9 +32,21 @@ export default class Query { * @param filters */ constructor(public world: World, public id: string, public filters: IQueryFilters) { + this.checkIfComponentsAreRegistered(); this.processFiltersAsBitMasks(); } + private checkIfComponentsAreRegistered() { + // Get a list of unique "Components" that are being used in the filters. + [...new Set(Object.values(this.filters).reduce((acc, value) => { + return acc.concat(value); + }, []))].forEach((component) => { + if (typeof component.prototype.bitmask === "undefined") { + throw new Error(`Please register the component ${component.name} in the ComponentRegistry.`); + } + }); + } + private processFiltersAsBitMasks(): void { if (this.filters.all) { this.filters.all.forEach((component) => { diff --git a/packages/ecs/src/World.ts b/packages/ecs/src/World.ts index 0deb638..7efdd24 100644 --- a/packages/ecs/src/World.ts +++ b/packages/ecs/src/World.ts @@ -15,6 +15,11 @@ export default class World { public systems = new Map(); public fps: number = 0; + public registerComponent(declaration: typeof Component) + { + this.declarations.components.registerComponent(declaration); + } + public createQuery(id: string, filters: IQueryFilters): Query { const query = new Query(this, id, filters); diff --git a/packages/renderer/src/canvas.ts b/packages/renderer/src/canvas.ts index 2351d27..939fd9d 100644 --- a/packages/renderer/src/canvas.ts +++ b/packages/renderer/src/canvas.ts @@ -35,8 +35,8 @@ export function text( y: number, fontSize: string = "10", fontFamily: string = "arial", - strokeColor?: string, - fillColor?: string, + strokeColor: string = 'black', + fillColor: string = 'black', ) { ctx.save(); ctx.font = `${fontSize}px ${fontFamily}`; @@ -121,10 +121,10 @@ export function renderTile( } export function dot(ctx: CanvasRenderingContext2D, x: number, y: number, fillColor: string = "rgb(0,0,0)", size: number = 1) { - // ctx.save(); + ctx.save(); ctx.fillStyle = fillColor; ctx.fillRect(x, y, size, size); - // ctx.restore(); + ctx.restore(); } export function rectangle(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, strokeColor: string = "black", fillColor?: string): void {