From f0bb31f61e049c225122ba0bc372dfd37acb8bab Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Mon, 17 Aug 2020 10:34:31 -0400 Subject: [PATCH] added tilemaps and a resource loader --- src/DataTypes/Tilesets/TiledData.ts | 33 ++++++++++ src/DataTypes/Tilesets/Tileset.ts | 80 +++++++++++++++++++++++ src/GameState/Factories/TilemapFactory.ts | 37 +++++++++++ src/GameState/Scene.ts | 28 ++++++-- src/Loop/GameLoop.ts | 7 +- src/Nodes/CanvasNode.ts | 1 - src/Nodes/ColoredCircle.ts | 1 - src/Nodes/Tilemap.ts | 42 ++++++++++++ src/Nodes/Tilemaps/OrthogonalTilemap.ts | 27 ++++++++ src/ResourceManager/ResourceManager.ts | 43 ++++++++++++ src/Utils/StringUtils.ts | 8 +++ src/main.ts | 28 ++++---- 12 files changed, 312 insertions(+), 23 deletions(-) create mode 100644 src/DataTypes/Tilesets/TiledData.ts create mode 100644 src/DataTypes/Tilesets/Tileset.ts create mode 100644 src/GameState/Factories/TilemapFactory.ts create mode 100644 src/Nodes/Tilemap.ts create mode 100644 src/Nodes/Tilemaps/OrthogonalTilemap.ts create mode 100644 src/ResourceManager/ResourceManager.ts create mode 100644 src/Utils/StringUtils.ts diff --git a/src/DataTypes/Tilesets/TiledData.ts b/src/DataTypes/Tilesets/TiledData.ts new file mode 100644 index 0000000..ad7818a --- /dev/null +++ b/src/DataTypes/Tilesets/TiledData.ts @@ -0,0 +1,33 @@ +export class TiledTilemapData { + height: number; + width: number; + orientation: string; + layers: TiledLayerData[]; + tilesets: TiledTilesetData[]; +} + +export class TiledTilesetData { + columns: number; + tilewidth: number; + tileheight: number; + tilecount: number; + firstgid: number; + imageheight: number; + imagewidth: number; + margin: number; + spacing: number; + name: string; + image: string; +} + +export class TiledLayerData { + data: number[]; + x: number; + y: number; + width: number; + height: number; + name: string; + opacity: number; + visible: boolean; +} + diff --git a/src/DataTypes/Tilesets/Tileset.ts b/src/DataTypes/Tilesets/Tileset.ts new file mode 100644 index 0000000..b2e1346 --- /dev/null +++ b/src/DataTypes/Tilesets/Tileset.ts @@ -0,0 +1,80 @@ +import Vec2 from "../Vec2"; +import { TiledTilesetData } from "./TiledData"; + +/** + * The data representation of a Tileset for the game engine. This represents one image, + * with a startIndex if required (as it is with Tiled using two images in one tilset). + */ +export default class Tileset { + protected imageUrl: string; + protected image: HTMLImageElement = null; + protected imageSize: Vec2; + protected startIndex: number; + protected endIndex: number; + protected tileSize: Vec2; + protected numRows: number; + protected numCols: number; + + constructor(tilesetData: TiledTilesetData){ + this.initFromTiledData(tilesetData); + } + + initFromTiledData(tiledData: TiledTilesetData){ + this.numRows = tiledData.tilecount/tiledData.columns; + this.numCols = tiledData.columns; + this.startIndex = tiledData.firstgid; + this.endIndex = this.startIndex + tiledData.tilecount - 1; + this.tileSize = new Vec2(tiledData.tilewidth, tiledData.tilewidth); + this.imageUrl = tiledData.image; + this.imageSize = new Vec2(tiledData.imagewidth, tiledData.imageheight); + } + + getImageUrl(): string { + return this.imageUrl + } + + getImage(): HTMLImageElement { + return this.image; + } + + setImage(image: HTMLImageElement){ + this.image = image; + } + + getStartIndex(): number { + return this.startIndex; + } + + getTileSize(): Vec2 { + return this.tileSize; + } + + getNumRows(): number { + return this.numRows; + } + + getNumCols(): number { + return this.numCols; + } + + isReady(): boolean { + return this.image !== null; + } + + hasTile(tileIndex: number): boolean { + return tileIndex >= this.startIndex && tileIndex <= this.endIndex; + } + + renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, worldSize: Vec2, origin: Vec2): void { + let index = tileIndex - this.startIndex; + let row = Math.floor(index / this.numCols); + let col = index % this.numCols; + let width = this.tileSize.x; + let height = this.tileSize.y; + let left = col * width; + let top = row * height; + let x = (dataIndex % worldSize.x) * width * 4; + let y = Math.floor(dataIndex / worldSize.x) * height * 4; + ctx.drawImage(this.image, left, top, width, height, x - origin.x, y - origin.y, width * 4, height * 4); + } +} \ No newline at end of file diff --git a/src/GameState/Factories/TilemapFactory.ts b/src/GameState/Factories/TilemapFactory.ts new file mode 100644 index 0000000..9843bc5 --- /dev/null +++ b/src/GameState/Factories/TilemapFactory.ts @@ -0,0 +1,37 @@ +import Scene from "../Scene"; +import Viewport from "../../SceneGraph/Viewport"; +import Tilemap from "../../Nodes/Tilemap" +import ResourceManager from "../../ResourceManager/ResourceManager"; +import { TiledTilemapData } from "../../DataTypes/Tilesets/TiledData"; +import StringUtils from "../../Utils/StringUtils"; + +export default class TilemapFactory { + private scene: Scene; + private viewport: Viewport; + private resourceManager: ResourceManager; + + constructor(scene: Scene, viewport: Viewport){ + this.scene = scene; + this.resourceManager = ResourceManager.getInstance(); + } + + add(constr: new (...a: any) => T, path: string, ...args: any): void { + this.resourceManager.loadTilemap(path, (tilemapData: TiledTilemapData) => { + // For each of the layers in the tilemap, create a tilemap + for(let layer of tilemapData.layers){ + let tilemap = new constr(tilemapData, layer); + + // Add to scene + this.scene.addTilemap(tilemap); + + // Load images for the tilesets + tilemap.getTilesets().forEach(tileset => { + let imagePath = StringUtils.getPathFromFilePath(path) + tileset.getImageUrl(); + this.resourceManager.loadImage(imagePath, (path: string, image: HTMLImageElement) => { + tileset.setImage(image); + }) + }); + } + }); + } +} \ No newline at end of file diff --git a/src/GameState/Scene.ts b/src/GameState/Scene.ts index 49db35e..1267726 100644 --- a/src/GameState/Scene.ts +++ b/src/GameState/Scene.ts @@ -3,30 +3,36 @@ import Viewport from "../SceneGraph/Viewport"; import SceneGraph from "../SceneGraph/SceneGraph"; import SceneGraphArray from "../SceneGraph/SceneGraphArray"; import CanvasNode from "../Nodes/CanvasNode"; -import CavnasNodeFactory from "./Factories/CanvasNodeFactory"; import CanvasNodeFactory from "./Factories/CanvasNodeFactory"; import GameState from "./GameState"; +import Tilemap from "../Nodes/Tilemap"; +import TilemapFactory from "./Factories/TilemapFactory"; export default class Scene { private gameState: GameState; private viewport: Viewport private parallax: Vec2; - sceneGraph: SceneGraph; + sceneGraph: SceneGraph; + private tilemaps: Array; private paused: boolean; private hidden: boolean; // Factories - public canvas: CavnasNodeFactory; + public canvasNode: CanvasNodeFactory; + public tilemap: TilemapFactory; constructor(viewport: Viewport, gameState: GameState){ this.gameState = gameState; this.viewport = viewport; this.parallax = new Vec2(1, 1); this.sceneGraph = new SceneGraphArray(this.viewport, this); + this.tilemaps = new Array(); this.paused = false; this.hidden = false; - this.canvas = new CanvasNodeFactory(this, this.viewport); + // Factories + this.canvasNode = new CanvasNodeFactory(this, this.viewport); + this.tilemap = new TilemapFactory(this, this.viewport); } setPaused(pauseValue: boolean): void { @@ -71,6 +77,10 @@ export default class Scene { this.sceneGraph.addNode(children); } + addTilemap(tilemap: Tilemap): void { + this.tilemaps.push(tilemap); + } + update(deltaT: number): void { if(!this.paused){ this.viewport.update(deltaT); @@ -83,7 +93,17 @@ export default class Scene { let visibleSet = this.sceneGraph.getVisibleSet(); let viewportOrigin = this.viewport.getPosition(); let origin = new Vec2(viewportOrigin.x*this.parallax.x, viewportOrigin.y*this.parallax.y); + let size = this.viewport.getSize(); + + // Render visible set visibleSet.forEach(node => node.render(ctx, origin)); + + // Render tilemaps + this.tilemaps.forEach(tilemap => { + if(tilemap.isReady()){ + tilemap.render(ctx, origin, size); + } + }); } } } \ No newline at end of file diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index b2ef591..279d701 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -4,6 +4,7 @@ import InputHandler from "../Input/InputHandler"; import Recorder from "../Playback/Recorder"; import GameState from "../GameState/GameState"; import Debug from "../Debug/Debug"; +import ResourceManager from "../ResourceManager/ResourceManager"; export default class GameLoop{ // The amount of time to spend on a physics step @@ -36,6 +37,7 @@ export default class GameLoop{ private recorder: Recorder; private gameState: GameState; private debug: Debug; + private resourceManager: ResourceManager; constructor(){ this.maxFPS = 60; @@ -62,12 +64,15 @@ export default class GameLoop{ this.recorder = new Recorder(); this.gameState = new GameState(); this.debug = Debug.getInstance(); + this.resourceManager = ResourceManager.getInstance(); } private initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D { canvas.width = width; canvas.height = height; - return canvas.getContext("2d"); + let ctx = canvas.getContext("2d"); + ctx.imageSmoothingEnabled = false; + return ctx; } setMaxFPS(initMax: number): void { diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index ce1a760..019c88a 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -1,6 +1,5 @@ import GameNode from "./GameNode"; import Vec2 from "../DataTypes/Vec2"; -import Viewport from "../SceneGraph/Viewport"; import Scene from "../GameState/Scene"; export default abstract class CanvasNode extends GameNode{ diff --git a/src/Nodes/ColoredCircle.ts b/src/Nodes/ColoredCircle.ts index 68b77f0..0103fda 100644 --- a/src/Nodes/ColoredCircle.ts +++ b/src/Nodes/ColoredCircle.ts @@ -10,7 +10,6 @@ export default class ColoredCircle extends CanvasNode{ super(); this.position = new Vec2(RandUtils.randInt(0, 1000), RandUtils.randInt(0, 1000)); this.color = RandUtils.randColor(); - console.log(this.color.toStringRGB()); this.size = new Vec2(50, 50); } diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts new file mode 100644 index 0000000..a150f48 --- /dev/null +++ b/src/Nodes/Tilemap.ts @@ -0,0 +1,42 @@ +import Vec2 from "../DataTypes/Vec2"; +import GameNode from "./GameNode"; +import Tileset from "../DataTypes/Tilesets/Tileset"; +import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledData" + +/** + * Represents one layer of tiles + */ +export default abstract class Tilemap extends GameNode { + protected data: number[]; + protected tilesets: Tileset[]; + protected worldSize: Vec2; + + constructor(tilemapData: TiledTilemapData, layerData: TiledLayerData){ + super(); + this.tilesets = new Array(); + this.worldSize = new Vec2(0, 0); + this.init(tilemapData, layerData); + } + + getTilesets(): Tileset[] { + return this.tilesets; + } + + isReady(): boolean { + if(this.tilesets.length !== 0){ + for(let tileset of this.tilesets){ + if(!tileset.isReady()){ + return false; + } + } + } + return true; + } + + /** + * Sets up the tileset using the data loaded from file + */ + abstract init(tilemapData: TiledTilemapData, layerData: TiledLayerData): void; + + abstract render(ctx: CanvasRenderingContext2D, origin: Vec2, viewportSize: Vec2): void; +} \ No newline at end of file diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts new file mode 100644 index 0000000..26cca19 --- /dev/null +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -0,0 +1,27 @@ +import Tilemap from "../Tilemap"; +import Vec2 from "../../DataTypes/Vec2"; +import { TiledTilemapData, TiledLayerData } from "../../DataTypes/Tilesets/TiledData"; +import Tileset from "../../DataTypes/Tilesets/Tileset"; + + +export default class OrthogonalTilemap extends Tilemap { + init(tilemapData: TiledTilemapData, layer: TiledLayerData): void { + this.worldSize.set(tilemapData.width, tilemapData.height); + this.data = layer.data; + tilemapData.tilesets.forEach(tilesetData => this.tilesets.push(new Tileset(tilesetData))); + } + + update(deltaT: number): void {} + + render(ctx: CanvasRenderingContext2D, origin: Vec2, viewportSize: Vec2) { + for(let i = 0; i < this.data.length; i++){ + let tileIndex = this.data[i]; + + for(let tileset of this.tilesets){ + if(tileset.hasTile(tileIndex)){ + tileset.renderTile(ctx, tileIndex, i, this.worldSize, origin); + } + } + } + } +} \ No newline at end of file diff --git a/src/ResourceManager/ResourceManager.ts b/src/ResourceManager/ResourceManager.ts new file mode 100644 index 0000000..9dd2e37 --- /dev/null +++ b/src/ResourceManager/ResourceManager.ts @@ -0,0 +1,43 @@ +export default class ResourceManager { + private static instance: ResourceManager; + + private constructor(){}; + + static getInstance(): ResourceManager { + if(!this.instance){ + this.instance = new ResourceManager(); + } + + return this.instance; + } + + public loadTilemap(pathToTilemapJSON: string, callback: Function): void { + this.loadTextFile(pathToTilemapJSON, (fileText: string) => { + let tilemapObject = JSON.parse(fileText); + callback(tilemapObject); + }); + } + + private loadTextFile(textFilePath: string, callback: Function): void { + let xobj: XMLHttpRequest = new XMLHttpRequest(); + xobj.overrideMimeType("application/json"); + xobj.open('GET', textFilePath, true); + xobj.onreadystatechange = function () { + if ((xobj.readyState == 4) && (xobj.status == 200)) { + callback(xobj.responseText); + } + }; + xobj.send(null); + } + + // TODO: When you switch to WebGL, make sure to make this private and make a "loadTexture" function + public loadImage(path: string, callback: Function): void { + var image = new Image(); + + image.onload = function () { + callback(path, image); + } + + image.src = path; + } +} \ No newline at end of file diff --git a/src/Utils/StringUtils.ts b/src/Utils/StringUtils.ts new file mode 100644 index 0000000..6c172b5 --- /dev/null +++ b/src/Utils/StringUtils.ts @@ -0,0 +1,8 @@ +export default class StringUtils { + static getPathFromFilePath(filePath: string): string { + let splitPath = filePath.split("/"); + splitPath.pop(); + splitPath.push(""); + return splitPath.join("/"); + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 953bf16..44a6053 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import ColoredCircle from "./Nodes/ColoredCircle"; import Color from "./Utils/Color"; import Button from "./Nodes/UIElements/Button"; import {} from "./index"; +import OrthogonalTilemap from "./Nodes/Tilemaps/OrthogonalTilemap"; function main(){ // Create the game object @@ -23,28 +24,28 @@ function main(){ pauseMenu.setParallax(0, 0); // Initialize GameObjects - let player = mainScene.canvas.add(Player); + let player = mainScene.canvasNode.add(Player); mainScene.getViewport().follow(player); - let recordButton = uiLayer.canvas.add(Button); + let recordButton = uiLayer.canvasNode.add(Button); recordButton.setSize(100, 50); recordButton.setText("Record"); recordButton.setPosition(400, 30); recordButton.onClickEventId = "record_button_press"; - let stopButton = uiLayer.canvas.add(Button); + let stopButton = uiLayer.canvasNode.add(Button); stopButton.setSize(100, 50); stopButton.setText("Stop"); stopButton.setPosition(550, 30); stopButton.onClickEventId = "stop_button_press"; - let playButton = uiLayer.canvas.add(Button); + let playButton = uiLayer.canvasNode.add(Button); playButton.setSize(100, 50); playButton.setText("Play"); playButton.setPosition(700, 30); playButton.onClickEventId = "play_button_press"; - let cycleFramerateButton = uiLayer.canvas.add(Button); + let cycleFramerateButton = uiLayer.canvasNode.add(Button); cycleFramerateButton.setSize(150, 50); cycleFramerateButton.setText("Cycle FPS"); cycleFramerateButton.setPosition(5, 400); @@ -55,7 +56,7 @@ function main(){ i = (i + 1) % 3; } - let pauseButton = uiLayer.canvas.add(Button); + let pauseButton = uiLayer.canvasNode.add(Button); pauseButton.setSize(100, 50); pauseButton.setText("Pause"); pauseButton.setPosition(700, 400); @@ -64,12 +65,12 @@ function main(){ pauseMenu.enable(); } - let modalBackground = pauseMenu.canvas.add(UIElement); + let modalBackground = pauseMenu.canvasNode.add(UIElement); modalBackground.setSize(400, 200); modalBackground.setBackgroundColor(new Color(0, 0, 0, 0.4)); modalBackground.setPosition(200, 100); - let resumeButton = pauseMenu.canvas.add(Button); + let resumeButton = pauseMenu.canvasNode.add(Button); resumeButton.setSize(100, 50); resumeButton.setText("Resume"); resumeButton.setPosition(400, 200); @@ -79,18 +80,13 @@ function main(){ } for(let i = 0; i < 10; i++){ - mainScene.canvas.add(ColoredCircle); + mainScene.canvasNode.add(ColoredCircle); } - for(let i = 0; i < 20; i++){ - let cc = backgroundScene.canvas.add(ColoredCircle); - cc.setSize(30, 30); - cc.setColor(cc.getColor().darken().darken()) - cc.getColor().a = 0.8; - } + backgroundScene.tilemap.add(OrthogonalTilemap, "assets/tilemaps/MultiLayer.json"); for(let i = 0; i < 30; i++){ - let cc = foregroundLayer.canvas.add(ColoredCircle); + let cc = foregroundLayer.canvasNode.add(ColoredCircle); cc.setSize(80, 80); cc.setColor(cc.getColor().lighten().lighten()) cc.getColor().a = 0.5;