added tilemaps and a resource loader

This commit is contained in:
Joe Weaver 2020-08-17 10:34:31 -04:00
parent 851fd050a7
commit f0bb31f61e
12 changed files with 312 additions and 23 deletions

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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<T extends Tilemap>(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);
})
});
}
});
}
}

View File

@ -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<Tilemap>;
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<Tilemap>();
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);
}
});
}
}
}

View File

@ -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 {

View File

@ -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{

View File

@ -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);
}

42
src/Nodes/Tilemap.ts Normal file
View File

@ -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<Tileset>();
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;
}

View File

@ -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);
}
}
}
}
}

View File

@ -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;
}
}

8
src/Utils/StringUtils.ts Normal file
View File

@ -0,0 +1,8 @@
export default class StringUtils {
static getPathFromFilePath(filePath: string): string {
let splitPath = filePath.split("/");
splitPath.pop();
splitPath.push("");
return splitPath.join("/");
}
}

View File

@ -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;