Skip to content

Commit

Permalink
Points on a grid, collision detection with solid obstacles is grid based
Browse files Browse the repository at this point in the history
  • Loading branch information
serbanghita committed Oct 21, 2024
1 parent 5693acb commit c1faf85
Show file tree
Hide file tree
Showing 17 changed files with 296 additions and 28 deletions.
4 changes: 2 additions & 2 deletions packages/demos/game-playground/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -106,4 +106,4 @@ async function runGame() {
};
}

runGame().then(() => console.log("Game started ..."));
setup().then(() => console.log("Game started ..."));
8 changes: 4 additions & 4 deletions packages/demos/game-playground/src/system/RenderSystem.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -66,7 +66,7 @@ export default class RenderSystem extends System {
);
}

renderImage(
image(
getCtx(this.CANVAS),
spriteSheetImg,
// source
Expand All @@ -81,7 +81,7 @@ export default class RenderSystem extends System {
animationFrame.height,
);

renderRectangle(
rectangle(
getCtx(this.CANVAS) as CanvasRenderingContext2D,
destPositionX,
destPositionY,
Expand All @@ -90,7 +90,7 @@ export default class RenderSystem extends System {
"#cccccc",
);

renderRectangle(
rectangle(
getCtx(this.CANVAS) as CanvasRenderingContext2D,
destPositionX + hitboxOffset.x,
destPositionY + hitboxOffset.y,
Expand Down
16 changes: 16 additions & 0 deletions packages/demos/tiled-rendering/src/IsCollisionTile.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
18 changes: 18 additions & 0 deletions packages/demos/tiled-rendering/src/IsMatrix.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions packages/demos/tiled-rendering/src/IsPlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from "@serbanghita-gamedev/ecs";

export default class IsPlayer extends Component {
constructor(public properties: Record<string, never>) {
super(properties);
}
}
7 changes: 7 additions & 0 deletions packages/demos/tiled-rendering/src/IsPreRendered.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from "@serbanghita-gamedev/ecs";

export default class IsPreRendered extends Component {
constructor(public properties: Record<string, never>) {
super(properties);
}
}
7 changes: 7 additions & 0 deletions packages/demos/tiled-rendering/src/IsRenderedInForeground.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from "@serbanghita-gamedev/ecs";

export default class IsRenderedInForeground extends Component {
constructor(public properties: Record<string, never>) {
super(properties);
}
}
41 changes: 41 additions & 0 deletions packages/demos/tiled-rendering/src/MoveSystem.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});
}
}
17 changes: 17 additions & 0 deletions packages/demos/tiled-rendering/src/PhysicsBody.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
27 changes: 27 additions & 0 deletions packages/demos/tiled-rendering/src/QuadTreeSystem.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
36 changes: 36 additions & 0 deletions packages/demos/tiled-rendering/src/RenderingSystem.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions packages/demos/tiled-rendering/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
87 changes: 69 additions & 18 deletions packages/demos/tiled-rendering/src/index.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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 ..."));
12 changes: 12 additions & 0 deletions packages/ecs/src/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Component>(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) => {
Expand Down
Loading

0 comments on commit c1faf85

Please sign in to comment.