From 32dd2ad5996d72ddd0fd3704fbbb009da1ff7b69 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Fri, 13 Sep 2024 21:45:59 -0500 Subject: [PATCH 1/6] feat(#8): modularizing --- index.js | 142 +++++++------------ modules/constants.js | 5 + modules/game-objects/camera.js | 12 ++ modules/{ => game-objects}/canvas-resizer.js | 9 +- modules/game-objects/debug.js | 40 ++++++ modules/game-objects/game-object.js | 50 +++++++ modules/game-objects/index.js | 6 + modules/game-objects/map-management.js | 47 ++++++ modules/game-objects/map.js | 46 ++++++ modules/utils.js | 4 - package.json | 4 +- 11 files changed, 271 insertions(+), 94 deletions(-) create mode 100644 modules/constants.js create mode 100644 modules/game-objects/camera.js rename modules/{ => game-objects}/canvas-resizer.js (88%) create mode 100644 modules/game-objects/debug.js create mode 100644 modules/game-objects/game-object.js create mode 100644 modules/game-objects/index.js create mode 100644 modules/game-objects/map-management.js create mode 100644 modules/game-objects/map.js diff --git a/index.js b/index.js index 1286ba9..70bb826 100644 --- a/index.js +++ b/index.js @@ -1,55 +1,61 @@ -import { CanvasResizer } from "./modules/canvas-resizer.js"; -import { getLevel } from "./modules/utils.js"; +import { GAME_HEIGHT, GAME_WIDTH } from "./modules/constants.js"; +import { + Camera, + CanvasResizer, + Debug, + GameObject, + MapManagement, +} from "./modules/game-objects/index.js"; -const TILE_SIZE = 16; -const canvasResizer = new CanvasResizer({ - canvas: document.getElementById("game"), - width: 320 / 2, - height: 240 / 2, - percentage: 0.9, -}); +const maps = [ + { + name: "overworld", + imageId: "overworld", + elementId: "level1", + selected: true, + }, + { name: "ocean", imageId: "overworld", elementId: "level2" }, +]; -const cols = canvasResizer.width / TILE_SIZE; -const rows = canvasResizer.height / TILE_SIZE; -const ctx = canvasResizer.canvas.getContext("2d"); -let debug = false; -let cameraX = 0; -let cameraY = 0; +const clicableObjects = ["debug", "level1", "level2"]; -async function drawLevel({ levelName, imageName }) { - const levelImage = document.getElementById(imageName); - const level = await getLevel(levelName); - const layer = level.layers[0]; - const { data, width, height } = layer; // Obtenemos la capa del tilemap, con su numero de columnas y filas +class Game extends GameObject { + constructor({ canvas }) { + super(); - const endCol = cameraX + cols; - const endRow = cameraY + rows; + const canvasResizer = new CanvasResizer({ + canvas: canvas, + width: GAME_WIDTH, + height: GAME_HEIGHT, + percentage: 0.9, + }); + const mapManagement = new MapManagement({ maps: maps }); + const camera = new Camera({ map: mapManagement.selected }); + const debug = new Debug({ debug: false }); + this.gameObjects = [canvasResizer, mapManagement, camera, debug]; - for (let row = cameraY; row <= endRow; row++) { - for (let col = cameraX; col <= endCol; col++) { - if (row < 0 || col < 0 || row >= height || col >= width) continue; // Omitimos tiles fuera del rango del tilemap + this.canvas = canvas; + this.ctx = this.canvas.getContext("2d"); + this.lastTime = 0; - if (debug) { - ctx.strokeRect( - (col - cameraX) * TILE_SIZE, - (row - cameraY) * TILE_SIZE, - TILE_SIZE, - TILE_SIZE - ); - } - const tile = data[row * width + col]; - ctx.drawImage( - levelImage, - ((tile - 1) * TILE_SIZE) % levelImage.width, - Math.floor(((tile - 1) * TILE_SIZE) / levelImage.width) * TILE_SIZE, - TILE_SIZE, - TILE_SIZE, - (col - cameraX) * TILE_SIZE, - (row - cameraY) * TILE_SIZE, - TILE_SIZE, - TILE_SIZE - ); - } + clicableObjects.forEach((elementId) => { + document.getElementById(elementId).addEventListener("click", () => { + this.onMouseClick(elementId); + }); + }); + } + + clear(ctx) { + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + loop(time) { + const delta = (time - this.lastTime) / 1000; + this.lastTime = time; + this.update(delta); + this.clear(this.ctx); + this.render(this.ctx); + requestAnimationFrame(this.loop.bind(this)); } } @@ -65,47 +71,9 @@ function moveCamera(dx, dy) { } async function run() { - await canvasResizer.load(); - - let selectedLevel = { levelName: "overworld", imageName: "overworld" }; - - const debugButton = document.getElementById("debug"); - debugButton.addEventListener("click", async () => { - debug = !debug; - await drawLevel(selectedLevel); - }); - - const level1Button = document.getElementById("level1"); - level1Button.addEventListener("click", async () => { - selectedLevel = { levelName: "overworld", imageName: "overworld" }; - await drawLevel(selectedLevel); - }); - - const level2Button = document.getElementById("level2"); - level2Button.addEventListener("click", async () => { - selectedLevel = { levelName: "ocean", imageName: "overworld" }; - await drawLevel(selectedLevel); - }); - - window.addEventListener("keydown", (event) => { - switch (event.key) { - case "ArrowUp": - moveCamera(0, -1); - break; - case "ArrowDown": - moveCamera(0, 1); - break; - case "ArrowLeft": - moveCamera(-1, 0); - break; - case "ArrowRight": - moveCamera(1, 0); - break; - } - drawLevel(selectedLevel); - }); - - await drawLevel(selectedLevel); + const game = new Game({ canvas: document.getElementById("game") }); + await game.load(); + game.loop(0); } run(); diff --git a/modules/constants.js b/modules/constants.js new file mode 100644 index 0000000..0538b2e --- /dev/null +++ b/modules/constants.js @@ -0,0 +1,5 @@ +export const TILE_SIZE = 16; +export const GAME_WIDTH = 320; +export const GAME_HEIGHT = 240; +export const COLS = GAME_WIDTH / TILE_SIZE; +export const ROWS = GAME_HEIGHT / TILE_SIZE; diff --git a/modules/game-objects/camera.js b/modules/game-objects/camera.js new file mode 100644 index 0000000..e360dde --- /dev/null +++ b/modules/game-objects/camera.js @@ -0,0 +1,12 @@ +import { GameObject } from "./game-object.js"; + +export class Camera extends GameObject { + constructor({ map, x = 0, y = 0, width = 320, height = 240 }) { + super({ x, y }); + this.map = map; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } +} diff --git a/modules/canvas-resizer.js b/modules/game-objects/canvas-resizer.js similarity index 88% rename from modules/canvas-resizer.js rename to modules/game-objects/canvas-resizer.js index 63658ab..fb20302 100644 --- a/modules/canvas-resizer.js +++ b/modules/game-objects/canvas-resizer.js @@ -1,4 +1,6 @@ -export class CanvasResizer { +import { GameObject } from "./game-object.js"; + +export class CanvasResizer extends GameObject { /** * Creates a new instance of `CanvasResizer` class. * @param {Object} config - The configuration options for the class. @@ -8,6 +10,7 @@ export class CanvasResizer { * @param {number} config.percentage - The percentage of the screen size to use for the canvas. */ constructor({ canvas, width, height, percentage }) { + super(); this.canvas = canvas; this.width = width; this.height = height; @@ -18,12 +21,16 @@ export class CanvasResizer { } load() { + if (this.loaded) { + return; + } return new Promise((resolve) => { ["load", "resize"].map((item) => window.addEventListener(item, () => { this._resize(); if (item === "load") { resolve(); + this.loaded = true; } }) ); diff --git a/modules/game-objects/debug.js b/modules/game-objects/debug.js new file mode 100644 index 0000000..b87ae2c --- /dev/null +++ b/modules/game-objects/debug.js @@ -0,0 +1,40 @@ +import { COLS, ROWS, TILE_SIZE } from "../constants.js"; +import { GameObject } from "./game-object.js"; + +export class Debug extends GameObject { + constructor({ debug = false }) { + super(); + this.debug = debug; + this.fps = 0; + } + + toggle() { + this.debug = !this.debug; + } + + update(delta) { + this.fps = Math.floor(1 / delta); + } + + render(ctx) { + if (!this.debug) { + return; + } + + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + ctx.strokeRect(col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE); + } + } + + ctx.fillStyle = "Red"; + ctx.font = "normal 16pt Arial"; + ctx.fillText(this.fps + " fps", 10, 26); + } + + onMouseClick(elementId) { + if (elementId === "debug") { + this.toggle(); + } + } +} diff --git a/modules/game-objects/game-object.js b/modules/game-objects/game-object.js new file mode 100644 index 0000000..b7ba42b --- /dev/null +++ b/modules/game-objects/game-object.js @@ -0,0 +1,50 @@ +export class GameObject { + /** + * Initializes a new instance of the GameObject class. + * + * @param {Object} options - An object containing the initial properties of the game object. + * @param {number} [options.x=0] - The initial x-coordinate of the game object. + * @param {number} [options.y=0] - The initial y-coordinate of the game object. + * @param {GameObject[]} [options.gameObjects=[]] - An array of child game objects. + */ + constructor(options = {}) { + const { x = 0, y = 0, gameObjects = [] } = options; + this.x = x; + this.y = y; + this.gameObjects = gameObjects; + } + + async load() { + await Promise.all( + this.gameObjects.map((item) => { + item.load(); + }) + ); + } + + update(delta) { + this.gameObjects.forEach((item) => { + item.update(delta); + }); + } + + render(ctx) { + this.gameObjects.forEach((item) => { + item.render(ctx); + }); + } + + onKeyReleased(key) { + this.gameObjects.forEach((item) => { + item.onKeyReleased(key); + }); + } + + onMouseClick(elementId) { + this.gameObjects.forEach((item) => { + item.onMouseClick(elementId); + }); + } + + onCollide() {} +} diff --git a/modules/game-objects/index.js b/modules/game-objects/index.js new file mode 100644 index 0000000..582f133 --- /dev/null +++ b/modules/game-objects/index.js @@ -0,0 +1,6 @@ +export { Camera } from "./camera.js"; +export { CanvasResizer } from "./canvas-resizer.js"; +export { Debug } from "./debug.js"; +export { GameObject } from "./game-object.js"; +export { MapManagement } from "./map-management.js"; +export { Map } from "./map.js"; diff --git a/modules/game-objects/map-management.js b/modules/game-objects/map-management.js new file mode 100644 index 0000000..7be3867 --- /dev/null +++ b/modules/game-objects/map-management.js @@ -0,0 +1,47 @@ +import { GameObject } from "./game-object.js"; +import { Map } from "./map.js"; + +export class MapManagement extends GameObject { + constructor({ maps = [] }) { + super(); + this.gameObjects = maps.map((item) => new Map(item)); + this.elementsId = this.gameObjects.map((item) => item.elementId); + } + + get selected() { + return this.gameObjects.find((item) => item.selected); + } + + set selected(name) { + this.gameObjects.forEach((item) => { + item.selected = item.name === name; + }); + } + + update(delta) { + this.gameObjects.forEach((item) => { + if (item.selected) { + item.update(delta); + } + }); + } + + render(ctx) { + this.gameObjects.forEach((item) => { + if (item.selected) { + item.render(ctx); + } + }); + } + + onMouseClick(elementId) { + if ( + !this.elementsId.includes(elementId) || + this.selected.elementId === elementId + ) { + return; + } + const map = this.gameObjects.find((item) => item.elementId === elementId); + this.selected = map.name; + } +} diff --git a/modules/game-objects/map.js b/modules/game-objects/map.js new file mode 100644 index 0000000..a681910 --- /dev/null +++ b/modules/game-objects/map.js @@ -0,0 +1,46 @@ +import { COLS, ROWS, TILE_SIZE } from "../constants.js"; +import { GameObject } from "./game-object.js"; + +export class Map extends GameObject { + constructor({ name, imageId, elementId, selected = false }) { + super(); + this.name = name; + this.imageId = imageId; + this.image = document.getElementById(imageId); + this.selected = selected; + this.elementId = elementId; + } + + async load() { + const level = await fetch("/resources/" + this.name + ".json"); + this.level = await level.json(); + return true; + } + + render(ctx) { + if (!this.level) { + return; + } + const levelImage = this.image; + const level = this.level; + const layer = level.layers[0]; + const data = layer.data; + + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const tile = data[row * COLS + col] - 1; + ctx.drawImage( + levelImage, + (tile * TILE_SIZE) % levelImage.width, + Math.floor((tile * TILE_SIZE) / levelImage.width) * TILE_SIZE, + TILE_SIZE, + TILE_SIZE, + col * TILE_SIZE, + row * TILE_SIZE, + TILE_SIZE, + TILE_SIZE + ); + } + } + } +} diff --git a/modules/utils.js b/modules/utils.js index c32ff70..e69de29 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -1,4 +0,0 @@ -export async function getLevel(name) { - const level = await fetch("/resources/" + name + ".json"); - return await level.json(); -} diff --git a/package.json b/package.json index e28be77..3199538 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "src/index.js", "scripts": { - "start": "serve ." + "start": "serve . -n" }, "repository": "git@git.jusemon.com:jusemon/evolver.git", "author": "Jusemon ", @@ -12,4 +12,4 @@ "devDependencies": { "serve": "^14.2.3" } -} +} \ No newline at end of file From 07c64eadebc79909ab82f803ef98871bfa98f3b9 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Fri, 13 Sep 2024 21:45:59 -0500 Subject: [PATCH 2/6] feat(#8): improve performance --- modules/game-objects/debug.js | 16 +++++++++--- modules/game-objects/map-management.js | 16 ------------ modules/game-objects/map.js | 35 ++++++++++++++++++-------- modules/utils.js | 7 ++++++ 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/modules/game-objects/debug.js b/modules/game-objects/debug.js index b87ae2c..0bf82a7 100644 --- a/modules/game-objects/debug.js +++ b/modules/game-objects/debug.js @@ -1,4 +1,5 @@ import { COLS, ROWS, TILE_SIZE } from "../constants.js"; +import { createCanvas } from "../utils.js"; import { GameObject } from "./game-object.js"; export class Debug extends GameObject { @@ -16,17 +17,24 @@ export class Debug extends GameObject { this.fps = Math.floor(1 / delta); } - render(ctx) { - if (!this.debug) { - return; + get grid() { + if (this._grid) { + return this._grid; } - + const { ctx, canvas } = createCanvas(COLS * TILE_SIZE, ROWS * TILE_SIZE); for (let row = 0; row < ROWS; row++) { for (let col = 0; col < COLS; col++) { ctx.strokeRect(col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } + return (this._grid = canvas); + } + render(ctx) { + if (!this.debug) { + return; + } + ctx.drawImage(this.grid, 0, 0); ctx.fillStyle = "Red"; ctx.font = "normal 16pt Arial"; ctx.fillText(this.fps + " fps", 10, 26); diff --git a/modules/game-objects/map-management.js b/modules/game-objects/map-management.js index 7be3867..99263ff 100644 --- a/modules/game-objects/map-management.js +++ b/modules/game-objects/map-management.js @@ -18,22 +18,6 @@ export class MapManagement extends GameObject { }); } - update(delta) { - this.gameObjects.forEach((item) => { - if (item.selected) { - item.update(delta); - } - }); - } - - render(ctx) { - this.gameObjects.forEach((item) => { - if (item.selected) { - item.render(ctx); - } - }); - } - onMouseClick(elementId) { if ( !this.elementsId.includes(elementId) || diff --git a/modules/game-objects/map.js b/modules/game-objects/map.js index a681910..8fd93d6 100644 --- a/modules/game-objects/map.js +++ b/modules/game-objects/map.js @@ -1,4 +1,5 @@ import { COLS, ROWS, TILE_SIZE } from "../constants.js"; +import { createCanvas } from "../utils.js"; import { GameObject } from "./game-object.js"; export class Map extends GameObject { @@ -7,22 +8,26 @@ export class Map extends GameObject { this.name = name; this.imageId = imageId; this.image = document.getElementById(imageId); + this.width = this.image.width; this.selected = selected; this.elementId = elementId; } async load() { - const level = await fetch("/resources/" + this.name + ".json"); - this.level = await level.json(); - return true; + const levelConfig = await fetch("/resources/" + this.name + ".json"); + this.levelConfig = await levelConfig.json(); } - render(ctx) { - if (!this.level) { - return; + get level() { + if (this._level) { + return this._level; } - const levelImage = this.image; - const level = this.level; + + const { ctx, canvas } = createCanvas(COLS * TILE_SIZE, ROWS * TILE_SIZE); + + const image = this.image; + const width = this.width; + const level = this.levelConfig; const layer = level.layers[0]; const data = layer.data; @@ -30,9 +35,9 @@ export class Map extends GameObject { for (let col = 0; col < COLS; col++) { const tile = data[row * COLS + col] - 1; ctx.drawImage( - levelImage, - (tile * TILE_SIZE) % levelImage.width, - Math.floor((tile * TILE_SIZE) / levelImage.width) * TILE_SIZE, + image, + (tile * TILE_SIZE) % width, + Math.floor((tile * TILE_SIZE) / width) * TILE_SIZE, TILE_SIZE, TILE_SIZE, col * TILE_SIZE, @@ -42,5 +47,13 @@ export class Map extends GameObject { ); } } + return (this._level = canvas); + } + + render(ctx) { + if (!this.levelConfig || !this.selected) { + return; + } + ctx.drawImage(this.level, 0, 0); } } diff --git a/modules/utils.js b/modules/utils.js index e69de29..077b7d8 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -0,0 +1,7 @@ +export function createCanvas(width, height) { + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + return { ctx, canvas }; +} From c1fe3acc11d38fdef809c97d81d2162536cd1a39 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Fri, 13 Sep 2024 23:30:28 -0500 Subject: [PATCH 3/6] feat(#8): fix 2d camera support --- index.js | 24 +++++------- modules/constants.js | 4 +- modules/game-objects/camera.js | 59 ++++++++++++++++++++++++++++- modules/game-objects/game-object.js | 10 ++++- modules/game-objects/map.js | 42 ++++++++++++-------- 5 files changed, 103 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index 70bb826..97d31b4 100644 --- a/index.js +++ b/index.js @@ -29,10 +29,11 @@ class Game extends GameObject { height: GAME_HEIGHT, percentage: 0.9, }); - const mapManagement = new MapManagement({ maps: maps }); - const camera = new Camera({ map: mapManagement.selected }); + const camera = new Camera({ + mapManagement: new MapManagement({ maps: maps }), + }); const debug = new Debug({ debug: false }); - this.gameObjects = [canvasResizer, mapManagement, camera, debug]; + this.gameObjects = [canvasResizer, camera, debug]; this.canvas = canvas; this.ctx = this.canvas.getContext("2d"); @@ -42,6 +43,12 @@ class Game extends GameObject { document.getElementById(elementId).addEventListener("click", () => { this.onMouseClick(elementId); }); + document.addEventListener("keydown", (event) => { + this.onKeyPressed(event.key); + }); + document.addEventListener("keyup", (event) => { + this.onKeyReleased(event.key); + }); }); } @@ -59,17 +66,6 @@ class Game extends GameObject { } } -function moveCamera(dx, dy) { - cameraX = Math.min( - Math.max(cameraX + dx, 0), - Math.floor((320 - canvasResizer.width) / TILE_SIZE) - ); - cameraY = Math.min( - Math.max(cameraY + dy, 0), - Math.floor((240 - canvasResizer.height) / TILE_SIZE) - ); -} - async function run() { const game = new Game({ canvas: document.getElementById("game") }); await game.load(); diff --git a/modules/constants.js b/modules/constants.js index 0538b2e..950ec2c 100644 --- a/modules/constants.js +++ b/modules/constants.js @@ -1,5 +1,5 @@ export const TILE_SIZE = 16; -export const GAME_WIDTH = 320; -export const GAME_HEIGHT = 240; +export const GAME_WIDTH = 160; +export const GAME_HEIGHT = 120; export const COLS = GAME_WIDTH / TILE_SIZE; export const ROWS = GAME_HEIGHT / TILE_SIZE; diff --git a/modules/game-objects/camera.js b/modules/game-objects/camera.js index e360dde..24cacb5 100644 --- a/modules/game-objects/camera.js +++ b/modules/game-objects/camera.js @@ -1,12 +1,67 @@ +import { TILE_SIZE } from "../constants.js"; import { GameObject } from "./game-object.js"; +import { MapManagement } from "./map-management.js"; export class Camera extends GameObject { - constructor({ map, x = 0, y = 0, width = 320, height = 240 }) { + constructor({ mapManagement, x = 0, y = 0, width = 160, height = 120 }) { super({ x, y }); - this.map = map; + this.mapManagement = mapManagement; + this.gameObjects = [mapManagement]; this.x = x; this.y = y; this.width = width; this.height = height; + this.keys = [ + { key: "ArrowUp", pressed: false, value: [0, -1] }, + { key: "ArrowDown", pressed: false, value: [0, 1] }, + { key: "ArrowLeft", pressed: false, value: [-1, 0] }, + { key: "ArrowRight", pressed: false, value: [1, 0] }, + ]; + this.availableKeys = this.keys.reduce( + (acc, item) => ({ ...acc, [item.key]: item }), + {} + ); + } + + update(delta) { + this.keys.forEach((item) => { + if (item.pressed) { + this.moveCamera(...item.value, delta); + } + }); + } + + onKeyPressed(key) { + if (!this.availableKeys[key]) return; + this.availableKeys[key].pressed = true; + } + + onKeyReleased(key) { + if (!this.availableKeys[key]) return; + this.availableKeys[key].pressed = false; + } + + render(ctx) { + this.gameObjects.forEach((item) => + item.render(ctx, this.x, this.y, this.width, this.height) + ); + } + + moveCamera(dx, dy, delta) { + const { levelConfig } = this.mapManagement.selected; + const layer = levelConfig.layers[0]; + const { height, width } = layer; + + const floorX = dx > 0 ? Math.floor : Math.ceil; + const floorY = dy > 0 ? Math.floor : Math.ceil; + + this.x = Math.min( + Math.max(this.x + floorX(dx * (delta * 100)), 0), + width * TILE_SIZE - this.width + ); + this.y = Math.min( + Math.max(this.y + floorY(dy * (delta * 100)), 0), + height * TILE_SIZE - this.height + ); } } diff --git a/modules/game-objects/game-object.js b/modules/game-objects/game-object.js index b7ba42b..73d1713 100644 --- a/modules/game-objects/game-object.js +++ b/modules/game-objects/game-object.js @@ -28,9 +28,15 @@ export class GameObject { }); } - render(ctx) { + render(ctx, ...args) { this.gameObjects.forEach((item) => { - item.render(ctx); + item.render(ctx, ...args); + }); + } + + onKeyPressed(key) { + this.gameObjects.forEach((item) => { + item.onKeyPressed(key); }); } diff --git a/modules/game-objects/map.js b/modules/game-objects/map.js index 8fd93d6..c12209e 100644 --- a/modules/game-objects/map.js +++ b/modules/game-objects/map.js @@ -1,4 +1,4 @@ -import { COLS, ROWS, TILE_SIZE } from "../constants.js"; +import { TILE_SIZE } from "../constants.js"; import { createCanvas } from "../utils.js"; import { GameObject } from "./game-object.js"; @@ -22,22 +22,20 @@ export class Map extends GameObject { if (this._level) { return this._level; } + const levelConfig = this.levelConfig; + const layer = levelConfig.layers[0]; + const { data, height, width } = layer; + const { ctx, canvas } = createCanvas(width * TILE_SIZE, height * TILE_SIZE); - const { ctx, canvas } = createCanvas(COLS * TILE_SIZE, ROWS * TILE_SIZE); + for (let row = 0; row < height; row++) { + for (let col = 0; col < width; col++) { + if (row < 0 || col < 0 || row >= height || col >= width) continue; - const image = this.image; - const width = this.width; - const level = this.levelConfig; - const layer = level.layers[0]; - const data = layer.data; - - for (let row = 0; row < ROWS; row++) { - for (let col = 0; col < COLS; col++) { - const tile = data[row * COLS + col] - 1; + const tile = data[row * width + col] - 1; ctx.drawImage( - image, - (tile * TILE_SIZE) % width, - Math.floor((tile * TILE_SIZE) / width) * TILE_SIZE, + this.image, + (tile * TILE_SIZE) % this.width, + Math.floor((tile * TILE_SIZE) / this.width) * TILE_SIZE, TILE_SIZE, TILE_SIZE, col * TILE_SIZE, @@ -47,13 +45,25 @@ export class Map extends GameObject { ); } } + return (this._level = canvas); } - render(ctx) { + render(ctx, sourceX, sourceY, width, height) { if (!this.levelConfig || !this.selected) { return; } - ctx.drawImage(this.level, 0, 0); + + ctx.drawImage( + this.level, + sourceX, + sourceY, + width, + height, + 0, + 0, + width, + height + ); } } From 4e2905cf961ef3ddde1739509ecc215353f9e26f Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Sat, 14 Sep 2024 17:43:53 -0500 Subject: [PATCH 4/6] feat(#8): move listeners to load --- index.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 97d31b4..8c59081 100644 --- a/index.js +++ b/index.js @@ -38,17 +38,20 @@ class Game extends GameObject { this.canvas = canvas; this.ctx = this.canvas.getContext("2d"); this.lastTime = 0; + } + async load() { + await super.load(); clicableObjects.forEach((elementId) => { document.getElementById(elementId).addEventListener("click", () => { this.onMouseClick(elementId); }); - document.addEventListener("keydown", (event) => { - this.onKeyPressed(event.key); - }); - document.addEventListener("keyup", (event) => { - this.onKeyReleased(event.key); - }); + }); + document.addEventListener("keydown", (event) => { + this.onKeyPressed(event.key); + }); + document.addEventListener("keyup", (event) => { + this.onKeyReleased(event.key); }); } From 731a32303adb4cd7536271f5275a487b2a604d12 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Sat, 14 Sep 2024 18:38:50 -0500 Subject: [PATCH 5/6] feat(#8): improve debug --- index.js | 8 ++--- modules/game-objects/camera.js | 19 ++++------ modules/game-objects/debug.js | 48 ------------------------- modules/game-objects/fps-counter.js | 21 +++++++++++ modules/game-objects/game-object.js | 23 +++++++----- modules/game-objects/index.js | 2 +- modules/game-objects/map-management.js | 1 + modules/game-objects/map.js | 50 +++++++++++++++++++------- 8 files changed, 84 insertions(+), 88 deletions(-) delete mode 100644 modules/game-objects/debug.js create mode 100644 modules/game-objects/fps-counter.js diff --git a/index.js b/index.js index 8c59081..0e0a732 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ import { GAME_HEIGHT, GAME_WIDTH } from "./modules/constants.js"; import { Camera, CanvasResizer, - Debug, + FpsCounter, GameObject, MapManagement, } from "./modules/game-objects/index.js"; @@ -30,10 +30,10 @@ class Game extends GameObject { percentage: 0.9, }); const camera = new Camera({ - mapManagement: new MapManagement({ maps: maps }), + gameObjects: [new MapManagement({ maps: maps })], }); - const debug = new Debug({ debug: false }); - this.gameObjects = [canvasResizer, camera, debug]; + const fpsCounter = new FpsCounter({ debug: false }); + this.gameObjects = [canvasResizer, camera, fpsCounter]; this.canvas = canvas; this.ctx = this.canvas.getContext("2d"); diff --git a/modules/game-objects/camera.js b/modules/game-objects/camera.js index 24cacb5..064ab0f 100644 --- a/modules/game-objects/camera.js +++ b/modules/game-objects/camera.js @@ -1,12 +1,10 @@ import { TILE_SIZE } from "../constants.js"; import { GameObject } from "./game-object.js"; -import { MapManagement } from "./map-management.js"; export class Camera extends GameObject { - constructor({ mapManagement, x = 0, y = 0, width = 160, height = 120 }) { + constructor({ gameObjects = [], x = 0, y = 0, width = 160, height = 120 }) { super({ x, y }); - this.mapManagement = mapManagement; - this.gameObjects = [mapManagement]; + this.gameObjects = gameObjects; this.x = x; this.y = y; this.width = width; @@ -48,19 +46,14 @@ export class Camera extends GameObject { } moveCamera(dx, dy, delta) { - const { levelConfig } = this.mapManagement.selected; - const layer = levelConfig.layers[0]; - const { height, width } = layer; - - const floorX = dx > 0 ? Math.floor : Math.ceil; - const floorY = dy > 0 ? Math.floor : Math.ceil; - + const [item] = this.gameObjects; + const { height, width } = item.selected ?? item; this.x = Math.min( - Math.max(this.x + floorX(dx * (delta * 100)), 0), + Math.max(this.x + dx * Math.floor(delta * 100), 0), width * TILE_SIZE - this.width ); this.y = Math.min( - Math.max(this.y + floorY(dy * (delta * 100)), 0), + Math.max(this.y + dy * Math.floor(delta * 100), 0), height * TILE_SIZE - this.height ); } diff --git a/modules/game-objects/debug.js b/modules/game-objects/debug.js deleted file mode 100644 index 0bf82a7..0000000 --- a/modules/game-objects/debug.js +++ /dev/null @@ -1,48 +0,0 @@ -import { COLS, ROWS, TILE_SIZE } from "../constants.js"; -import { createCanvas } from "../utils.js"; -import { GameObject } from "./game-object.js"; - -export class Debug extends GameObject { - constructor({ debug = false }) { - super(); - this.debug = debug; - this.fps = 0; - } - - toggle() { - this.debug = !this.debug; - } - - update(delta) { - this.fps = Math.floor(1 / delta); - } - - get grid() { - if (this._grid) { - return this._grid; - } - const { ctx, canvas } = createCanvas(COLS * TILE_SIZE, ROWS * TILE_SIZE); - for (let row = 0; row < ROWS; row++) { - for (let col = 0; col < COLS; col++) { - ctx.strokeRect(col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE); - } - } - return (this._grid = canvas); - } - - render(ctx) { - if (!this.debug) { - return; - } - ctx.drawImage(this.grid, 0, 0); - ctx.fillStyle = "Red"; - ctx.font = "normal 16pt Arial"; - ctx.fillText(this.fps + " fps", 10, 26); - } - - onMouseClick(elementId) { - if (elementId === "debug") { - this.toggle(); - } - } -} diff --git a/modules/game-objects/fps-counter.js b/modules/game-objects/fps-counter.js new file mode 100644 index 0000000..3d4b266 --- /dev/null +++ b/modules/game-objects/fps-counter.js @@ -0,0 +1,21 @@ +import { GameObject } from "./game-object.js"; + +export class FpsCounter extends GameObject { + constructor({ debug = false }) { + super({ debug }); + this.fps = 0; + } + + update(delta) { + this.fps = Math.floor(1 / delta); + } + + render(ctx) { + if (!this.debug) { + return; + } + ctx.fillStyle = "Red"; + ctx.font = "normal 12pt Arial"; + ctx.fillText(this.fps + " fps", 5, 15); + } +} diff --git a/modules/game-objects/game-object.js b/modules/game-objects/game-object.js index 73d1713..932f923 100644 --- a/modules/game-objects/game-object.js +++ b/modules/game-objects/game-object.js @@ -1,17 +1,19 @@ export class GameObject { - /** - * Initializes a new instance of the GameObject class. - * - * @param {Object} options - An object containing the initial properties of the game object. - * @param {number} [options.x=0] - The initial x-coordinate of the game object. - * @param {number} [options.y=0] - The initial y-coordinate of the game object. - * @param {GameObject[]} [options.gameObjects=[]] - An array of child game objects. - */ constructor(options = {}) { - const { x = 0, y = 0, gameObjects = [] } = options; + const { + x = 0, + y = 0, + width = 0, + height = 0, + debug = false, + gameObjects = [], + } = options; this.x = x; this.y = y; + this.width = width; + this.height = height; this.gameObjects = gameObjects; + this.debug = debug; } async load() { @@ -47,6 +49,9 @@ export class GameObject { } onMouseClick(elementId) { + if (elementId === "debug") { + this.debug = !this.debug; + } this.gameObjects.forEach((item) => { item.onMouseClick(elementId); }); diff --git a/modules/game-objects/index.js b/modules/game-objects/index.js index 582f133..fbeecb0 100644 --- a/modules/game-objects/index.js +++ b/modules/game-objects/index.js @@ -1,6 +1,6 @@ export { Camera } from "./camera.js"; export { CanvasResizer } from "./canvas-resizer.js"; -export { Debug } from "./debug.js"; +export { FpsCounter } from "./fps-counter.js"; export { GameObject } from "./game-object.js"; export { MapManagement } from "./map-management.js"; export { Map } from "./map.js"; diff --git a/modules/game-objects/map-management.js b/modules/game-objects/map-management.js index 99263ff..fd4c952 100644 --- a/modules/game-objects/map-management.js +++ b/modules/game-objects/map-management.js @@ -19,6 +19,7 @@ export class MapManagement extends GameObject { } onMouseClick(elementId) { + super.onMouseClick(elementId); if ( !this.elementsId.includes(elementId) || this.selected.elementId === elementId diff --git a/modules/game-objects/map.js b/modules/game-objects/map.js index c12209e..6bf6eff 100644 --- a/modules/game-objects/map.js +++ b/modules/game-objects/map.js @@ -3,39 +3,56 @@ import { createCanvas } from "../utils.js"; import { GameObject } from "./game-object.js"; export class Map extends GameObject { - constructor({ name, imageId, elementId, selected = false }) { - super(); + constructor({ name, imageId, elementId, selected = false, debug = false }) { + super({ debug }); this.name = name; this.imageId = imageId; this.image = document.getElementById(imageId); - this.width = this.image.width; + this.imageWidth = this.image.width; + this.imageHeight = this.image.height; this.selected = selected; this.elementId = elementId; + this.debug = debug; } async load() { const levelConfig = await fetch("/resources/" + this.name + ".json"); this.levelConfig = await levelConfig.json(); + const layer = this.levelConfig.layers[0]; + const { data, height, width } = layer; + this.width = width; + this.height = height; + this.data = data; } get level() { if (this._level) { return this._level; } - const levelConfig = this.levelConfig; - const layer = levelConfig.layers[0]; - const { data, height, width } = layer; - const { ctx, canvas } = createCanvas(width * TILE_SIZE, height * TILE_SIZE); + const { ctx, canvas } = createCanvas( + this.width * TILE_SIZE, + this.height * TILE_SIZE + ); - for (let row = 0; row < height; row++) { - for (let col = 0; col < width; col++) { - if (row < 0 || col < 0 || row >= height || col >= width) continue; + for (let row = 0; row < this.height; row++) { + for (let col = 0; col < this.width; col++) { + if (row < 0 || col < 0 || row >= this.height || col >= this.width) + continue; - const tile = data[row * width + col] - 1; + if (this.debug) { + ctx.strokeRect( + col * TILE_SIZE, + row * TILE_SIZE, + TILE_SIZE, + TILE_SIZE + ); + } + + const tile = this.data[row * this.width + col] - 1; ctx.drawImage( this.image, - (tile * TILE_SIZE) % this.width, - Math.floor((tile * TILE_SIZE) / this.width) * TILE_SIZE, + (tile * TILE_SIZE) % this.imageWidth, + Math.floor((tile * TILE_SIZE) / this.imageWidth) * TILE_SIZE, TILE_SIZE, TILE_SIZE, col * TILE_SIZE, @@ -49,6 +66,13 @@ export class Map extends GameObject { return (this._level = canvas); } + onMouseClick(elementId) { + if (elementId === "debug") { + this.debug = !this.debug; + this._level = null; + } + } + render(ctx, sourceX, sourceY, width, height) { if (!this.levelConfig || !this.selected) { return; From 580bea0848f6eab1f0c9ee0e1e44b601312a4057 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Sat, 14 Sep 2024 21:19:46 -0500 Subject: [PATCH 6/6] feat(#8): added speed control --- modules/game-objects/camera.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/game-objects/camera.js b/modules/game-objects/camera.js index 064ab0f..5e24e70 100644 --- a/modules/game-objects/camera.js +++ b/modules/game-objects/camera.js @@ -2,13 +2,21 @@ import { TILE_SIZE } from "../constants.js"; import { GameObject } from "./game-object.js"; export class Camera extends GameObject { - constructor({ gameObjects = [], x = 0, y = 0, width = 160, height = 120 }) { + constructor({ + gameObjects = [], + x = 0, + y = 0, + width = 160, + height = 120, + speed = 1, + }) { super({ x, y }); this.gameObjects = gameObjects; this.x = x; this.y = y; this.width = width; this.height = height; + this.speed = speed; this.keys = [ { key: "ArrowUp", pressed: false, value: [0, -1] }, { key: "ArrowDown", pressed: false, value: [0, 1] }, @@ -49,11 +57,11 @@ export class Camera extends GameObject { const [item] = this.gameObjects; const { height, width } = item.selected ?? item; this.x = Math.min( - Math.max(this.x + dx * Math.floor(delta * 100), 0), + Math.max(this.x + dx * Math.floor(delta * this.speed * 100), 0), width * TILE_SIZE - this.width ); this.y = Math.min( - Math.max(this.y + dy * Math.floor(delta * 100), 0), + Math.max(this.y + dy * Math.floor(delta * this.speed * 100), 0), height * TILE_SIZE - this.height ); }