Skip to content

Commit

Permalink
Demo for rendering opinionated Tiled maps.
Browse files Browse the repository at this point in the history
  • Loading branch information
serbanghita committed Sep 2, 2024
1 parent 7a606d6 commit b7edfe1
Show file tree
Hide file tree
Showing 20 changed files with 2,541 additions and 39 deletions.
10 changes: 7 additions & 3 deletions packages/assets/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
export function loadLocalImage(data: string) {
export async function loadLocalImage(data: string): Promise<HTMLImageElement> {
const img = new Image();
const test1 = data.match(/([a-z0-9-_]+).(png|gif|jpg)$/i);
const test2 = data.match(/^data:image\//i);
if (!test1 && !test2) {
throw new Error(`Trying to an load an invalid image ${data}.`);
}

img.src = data;
return img;
return new Promise((resolve) => {
img.src = data;
img.onload = function() {
resolve(this as HTMLImageElement);
}
});
}
6 changes: 6 additions & 0 deletions packages/demo-tiled/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

[*.{js,json,ts}]
indent_style = space
indent_size = 2
max_line_length = 140
3 changes: 3 additions & 0 deletions packages/demo-tiled/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
src/*.js
src/*.js.map
src/assets/sprites/*.png
4 changes: 4 additions & 0 deletions packages/demo-tiled/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ignore artifacts:
build
dist
coverage
3 changes: 3 additions & 0 deletions packages/demo-tiled/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 140
}
18 changes: 18 additions & 0 deletions packages/demo-tiled/dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>GLHF Demo</title>
<style>
body { background-color: dimgrey; }
#game-wrapper { border: 1px solid black; background-color: white; }
</style>
</head>
<body>

</body>
</html>
<script src="demo.js"></script>
21 changes: 21 additions & 0 deletions packages/demo-tiled/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "glhf-demo-tiled",
"version": "1.0.0",
"description": "Demo of a Tiled map loaded to Canvas",
"scripts": {
"build": "esbuild ./src/index.ts --bundle --sourcemap --loader:.png=dataurl --outfile=dist/demo.js",
"dev": "esbuild ./src/index.ts --bundle --sourcemap --watch --loader:.png=dataurl --outfile=dist/demo.js --servedir=dist",
"test": "echo \"Error: no test specified for glhf-demo yet.\""
},
"author": "Serban Ghita <[email protected]> (https://ghita.org)",
"license": "All code is MIT, the assets are copywritten.",
"devDependencies": {
"@eslint/js": "^9.4.0",
"@types/eslint__js": "^8.42.3",
"esbuild": "0.21.4",
"eslint": "^9.4.0",
"prettier": "3.3.3",
"typescript": "^5.4.5",
"typescript-eslint": "^7.11.0"
}
}
10 changes: 10 additions & 0 deletions packages/demo-tiled/src/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { loadLocalImage } from "@serbanghita-gamedev/assets";

export async function loadSprites() {
const SPRITES: { [key: string]: HTMLImageElement } = {
"./assets/sprites/terrain.png": await loadLocalImage(require("./assets/sprites/terrain.png"))
}

return SPRITES;
}
2,250 changes: 2,250 additions & 0 deletions packages/demo-tiled/src/assets/maps/E1MM2.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions packages/demo-tiled/src/component/IsTiledMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Component} from "@serbanghita-gamedev/ecs";
import {TiledMapFile} from "@serbanghita-gamedev/tiled";

export type IsTiledMapProps = {
mapFile: TiledMapFile;
}

export default class IsTiledMap extends Component {
constructor(public properties: IsTiledMapProps) {
super(properties);

this.init(properties);
}
}
31 changes: 31 additions & 0 deletions packages/demo-tiled/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 0. Create the UI and canvas.
import {createWrapperElement, createCanvas} from "@serbanghita-gamedev/renderer";
import {World} from "@serbanghita-gamedev/ecs";
import { PreRenderTiledMapSystem } from "./system/PreRenderTiledMapSystem";
import IsTiledMap from "./component/IsTiledMap";
import { loadSprites } from "./assets";

async function runGame() {
const HTML_WRAPPER = createWrapperElement("game-wrapper", 640, 480);
const CANVAS_FOREGROUND = createCanvas("foreground", 640, 480, "1");
const CANVAS_BACKGROUND = createCanvas("background", 640, 480, "2");
HTML_WRAPPER.appendChild(CANVAS_FOREGROUND);
HTML_WRAPPER.appendChild(CANVAS_BACKGROUND);
document.body.appendChild(HTML_WRAPPER);

const SPRITES = await loadSprites();

// Create the current "World" (scene).
const world = new World();

world.declarations.components.registerComponent(IsTiledMap);

const CustomMapEntity = world.createEntity("map");
CustomMapEntity.addComponent(IsTiledMap, {mapFile: require("./assets/maps/E1MM2.json")})

const TiledMapQuery = world.createQuery("TiledMapQuery", { all: [IsTiledMap] });
world.createSystem(PreRenderTiledMapSystem, TiledMapQuery, CANVAS_BACKGROUND, SPRITES).runOnlyOnce();
world.systems.forEach((system) => system.update(0));
}

runGame().then(() => console.log('Game started ...'));
60 changes: 60 additions & 0 deletions packages/demo-tiled/src/system/PreRenderTiledMapSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { System, Query, World } from "@serbanghita-gamedev/ecs";
import IsTiledMap from "../component/IsTiledMap";
import { renderTile } from "@serbanghita-gamedev/renderer";
import TiledMap from "@serbanghita-gamedev/tiled/tiled";
import { getCtx } from "@serbanghita-gamedev/renderer";

export class PreRenderTiledMapSystem extends System
{
public constructor(public world: World, public query: Query, protected CANVAS_BACKGROUND: HTMLCanvasElement, protected SPRITES: { [key: string]: HTMLImageElement }) {
super(world, query);
}

private renderToBuffer(tiledMap: TiledMap) {

if (!this.CANVAS_BACKGROUND) {
throw new Error(`Background canvas ($background) was not created or passed.`);
}

// Create all the layers.
tiledMap.getRenderLayers().forEach((layer) => {

// Create all the items existing in the layer.
for (let j = 0; j < layer.data.length; j++) {

// Don't draw empty cells.
if (layer.data[j] === 0) {
continue;
}

renderTile(
// getBufferCtx(getObjectProperty(layer.properties, "renderOnLayer")),
getCtx(this.CANVAS_BACKGROUND),
this.SPRITES["./assets/sprites/terrain.png"],
tiledMap.getTileWidth(),
tiledMap.getTileHeight(),
j,
layer.data[j],
tiledMap.getWidthInTiles(),
tiledMap.getHeightInTiles()
);
}

});

}

public update(now: number): void {
this.query.execute().forEach((entity) => {
console.log(entity);

const tiledMapComponent = entity.getComponent(IsTiledMap);
const tiledMapFile = tiledMapComponent.properties.mapFile;

const tileMap = new TiledMap(tiledMapFile);
this.renderToBuffer(tileMap);


});
}
}
30 changes: 30 additions & 0 deletions packages/demo-tiled/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"rootDirs": ["src"],
"baseUrl": "src",
"sourceMap": true,
"paths": {
"@serbanghita-gamedev/assets/*": ["../../assets/src/*"],
"@serbanghita-gamedev/bitmask/*": ["../../bitmask/src/*"],
"@serbanghita-gamedev/component/*": ["../../component/src/*"],
"@serbanghita-gamedev/ecs/*": ["../../ecs/src/*"],
"@serbanghita-gamedev/input/*": ["../../input/src/*"],
"@serbanghita-gamedev/renderer/*": ["../../renderer/src/*"],
"@serbanghita-gamedev/matrix/*": ["../../matrix/src/*"],
"@serbanghita-gamedev/tiled/*": ["../../tiled/src/*"],
}
},
"include": [
"src/index.ts",
],
"exclude": [
"node_modules",
"dist"
]
}
19 changes: 19 additions & 0 deletions packages/demo-tiled/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from 'vitest/config'
import tsconfigPaths from 'vite-tsconfig-paths'
import * as path from "node:path";

export default defineConfig({
plugins: [tsconfigPaths()],
test: {
alias: {
'@serbanghita-gamedev/assets/': path.join(__dirname, '../assets/'),
'@serbanghita-gamedev/bitmask/': path.join(__dirname, '../bitmask/'),
'@serbanghita-gamedev/component/': path.join(__dirname, '../component/'),
'@serbanghita-gamedev/ecs/': path.join(__dirname, '../ecs/'),
'@serbanghita-gamedev/input/': path.join(__dirname, '../input/'),
'@serbanghita-gamedev/renderer/': path.join(__dirname, '../renderer/'),
'@serbanghita-gamedev/matrix/': path.join(__dirname, '../matrix/'),
'@serbanghita-gamedev/tiled/': path.join(__dirname, '../tiled/')
}
}
})
43 changes: 14 additions & 29 deletions packages/demo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,39 +93,24 @@ ENTITIES_DECLARATIONS.forEach((entityDeclaration) => {
// });
});

world.createQuery("MatrixQuery", { all: [IsOnMatrix] });
const MatrixQuery = world.createQuery("MatrixQuery", { all: [IsOnMatrix] });
const KeyboardQuery = world.createQuery("KeyboardQuery", { all: [Keyboard] });
world.createQuery("IdleQuery", { all: [IsIdle] });
world.createQuery("WalkingQuery", { all: [IsWalking] });
world.createQuery("AttackingWithClubQuery", { all: [IsAttackingWithClub] });
const IdleQuery = world.createQuery("IdleQuery", { all: [IsIdle] });
const WalkingQuery = world.createQuery("WalkingQuery", { all: [IsWalking] });
const AttackingWithClubQuery = world.createQuery("AttackingWithClubQuery", { all: [IsAttackingWithClub] });
const RenderableQuery = world.createQuery("RenderableQuery", { all: [Renderable, SpriteSheet, Position] });

// world.declarations.systems.set("PreRenderSystem", PreRenderSystem); // @todo This should run only once!
// world.declarations.systems.set("PlayerKeyboardSystem", PlayerKeyboardSystem);
// world.declarations.systems.set("IdleSystem", IdleSystem);
// world.declarations.systems.set("WalkingSystem", WalkingSystem);
// world.declarations.systems.set("MatrixSystem", MatrixSystem);
// world.declarations.systems.set("AttackingWithClubSystem", AttackingWithClubSystem);
// world.declarations.systems.set("RenderSystem", RenderSystem);

world
.createSystem(PreRenderSystem, RenderableQuery)
.createSystem(PlayerKeyboardSystem, KeyboardQuery, input)
.createSystem("IdleSystem", "IdleQuery")
.createSystem("WalkingSystem", "WalkingQuery")
.createSystem("AttackingWithClubSystem", "AttackingWithClubQuery")
.createSystem("RenderSystem", "RenderableQuery", $foreground)
.createSystem("MatrixSystem", "MatrixQuery");

world.getSystem("PreRenderSystem").update(0);

const loop = (now: DOMHighResTimeStamp) => {
world.systems.forEach((system) => system.update(now));

window.requestAnimationFrame(loop);
};
world.createSystem(PreRenderSystem, RenderableQuery).runOnlyOnce();
world.createSystem(PlayerKeyboardSystem, KeyboardQuery, input);
world.createSystem(IdleSystem, IdleQuery);
world.createSystem(WalkingSystem, WalkingQuery);
world.createSystem(AttackingWithClubSystem, AttackingWithClubQuery);
world.createSystem(RenderSystem, RenderableQuery, $foreground);
world.createSystem(MatrixSystem, MatrixQuery);

world.getSystem(PreRenderSystem).update();

window.requestAnimationFrame(loop);
world.start();

// @ts-expect-error I'm too lazy to typehint window.
window["engine"] = {
Expand Down
14 changes: 13 additions & 1 deletion packages/ecs/src/System.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ import Query from "./Query";

// export type SystemConstructor = new (world: World, properties?: {}) => System;

export type SystemSettings = {
// How many times to run the system before de-registering itself from the loop.
runTimes: number;
}

export default class System {
public settings: SystemSettings = { runTimes: -1 };

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public constructor(public world: World, public query: Query, ...args: unknown[]) {
}

public runOnlyOnce() {
this.settings.runTimes = 1;
return this;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public update(now: number): void
public update(now: number = 0): void
{
throw new Error(`System update() must be implemented.`)
}
Expand Down
23 changes: 18 additions & 5 deletions packages/ecs/src/World.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Entity from "./Entity";
import System from "./System";
import System, { SystemSettings } from "./System";
import Query, {IQueryFilters} from "./Query";
import Component from "./Component";
import { hasBit } from "@serbanghita-gamedev/bitmask";
Expand Down Expand Up @@ -71,11 +71,12 @@ export default class World {
this.notifyQueriesOfEntityRemoval(entity);
}

public createSystem(system: typeof System, query: Query, ...args: unknown[]): World
public createSystem(systemDeclaration: typeof System, query: Query, ...args: unknown[]): System
{
this.systems.set(system, new System(this, query, ...args));
const systemInstance = new systemDeclaration(this, query, ...args);
this.systems.set(systemDeclaration, systemInstance);

return this;
return systemInstance;
}

public getSystem(system: typeof System)
Expand All @@ -86,7 +87,7 @@ export default class World {
throw new Error(`There is no system instance with the id ${system.name}`)
}

return system;
return systemInstance;
}

public notifyQueriesOfEntityCandidacy(entity: Entity) {
Expand Down Expand Up @@ -134,4 +135,16 @@ export default class World {
}
});
}

public start() {
const onlyOnceSystems = [...this.systems].filter(([k, system]) => system.settings.runTimes === 1);

const loop = (now: DOMHighResTimeStamp) => {
this.systems.forEach((system) => system.update(now));

window.requestAnimationFrame(loop);
};

window.requestAnimationFrame(loop);
}
}
2 changes: 2 additions & 0 deletions packages/renderer/src/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export function renderTile(
const yOnSprite = Math.floor((tileValue - 1) / tilesPerRow) * height;
// console.log(w, h, tileValue, xOnSprite, yOnSprite);

console.log(xOnSprite, yOnSprite, width, height, Math.floor(tileIndex % sW) * width, Math.floor(tileIndex / sW) * height)

ctx.drawImage(
tileSheetImg,
// Position on tileset.
Expand Down
Loading

0 comments on commit b7edfe1

Please sign in to comment.