From 9cb4f55d9aff9a16d9f8417f18b0b6607f30b9d7 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Sun, 20 Sep 2020 14:05:22 -0400 Subject: [PATCH] added support for Tiled object layers --- src/DataTypes/Tilesets/TiledData.ts | 23 ++++ src/DataTypes/Tilesets/Tileset.ts | 35 ++++-- src/DataTypes/Vec2.ts | 8 ++ src/MainScene.ts | 8 +- src/Nodes/CanvasNode.ts | 17 +++ src/Nodes/Sprites/Sprite.ts | 21 ++-- src/Nodes/Tilemap.ts | 4 +- src/Nodes/Tilemaps/OrthogonalTilemap.ts | 2 - src/Physics/PhysicsManager.ts | 20 +++- src/Physics/PhysicsNode.ts | 4 + src/Physics/StaticBody.ts | 1 + src/ResourceManager/ResourceManager.ts | 15 ++- src/Scene/Factories/TilemapFactory.ts | 144 +++++++++++++++++++----- src/SceneGraph/Viewport.ts | 5 +- src/SecondScene.ts | 8 +- 15 files changed, 239 insertions(+), 76 deletions(-) diff --git a/src/DataTypes/Tilesets/TiledData.ts b/src/DataTypes/Tilesets/TiledData.ts index 6cbea50..6b4125d 100644 --- a/src/DataTypes/Tilesets/TiledData.ts +++ b/src/DataTypes/Tilesets/TiledData.ts @@ -35,6 +35,7 @@ export class TiledTilesetData { spacing: number; name: string; image: string; + tiles: Array } /** @@ -50,5 +51,27 @@ export class TiledLayerData { opacity: number; visible: boolean; properties: TiledLayerProperty[]; + type: string; + objects: Array; } +export class TiledObject { + gid: number; + height: number; + width: number; + id: number; + name: string;; + properties: Array; + rotation: number; + type: string; + visible: boolean; + x: number; + y: number; +} + +export class TiledCollectionTile { + id: number; + image: string; + imageheight: number; + imagewidth: number; +} diff --git a/src/DataTypes/Tilesets/Tileset.ts b/src/DataTypes/Tilesets/Tileset.ts index b7c7f10..4ab6dd3 100644 --- a/src/DataTypes/Tilesets/Tileset.ts +++ b/src/DataTypes/Tilesets/Tileset.ts @@ -1,3 +1,4 @@ +import ResourceManager from "../../ResourceManager/ResourceManager"; import Vec2 from "../Vec2"; import { TiledTilesetData } from "./TiledData"; @@ -6,8 +7,7 @@ import { TiledTilesetData } from "./TiledData"; * 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 imageKey: string; protected imageSize: Vec2; protected startIndex: number; protected endIndex: number; @@ -31,20 +31,31 @@ export default class Tileset { this.startIndex = tiledData.firstgid; this.endIndex = this.startIndex + tiledData.tilecount - 1; this.tileSize = new Vec2(tiledData.tilewidth, tiledData.tilewidth); - this.imageUrl = tiledData.image; + this.imageKey = tiledData.image; this.imageSize = new Vec2(tiledData.imagewidth, tiledData.imageheight); } - getImageUrl(): string { - return this.imageUrl + getImageKey(): string { + return this.imageKey; } - getImage(): HTMLImageElement { - return this.image; - } + /** + * Returns a Vec2 containing the left and top offset from the image origin for this tile. + * @param tileIndex The index of the tile from startIndex to endIndex of this tileset + */ + getImageOffsetForTile(tileIndex: number): Vec2 { + // Get the true index + 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; - setImage(image: HTMLImageElement){ - this.image = image; + // Calculate the position to start a crop in the tileset image + let left = col * width; + let top = row * height; + + return new Vec2(left, top); } getStartIndex(): number { @@ -77,6 +88,8 @@ export default class Tileset { * @param scale The scale of the tilemap */ renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, worldSize: Vec2, origin: Vec2, scale: Vec2): void { + let image = ResourceManager.getInstance().getImage(this.imageKey); + // Get the true index let index = tileIndex - this.startIndex; let row = Math.floor(index / this.numCols); @@ -91,6 +104,6 @@ export default class Tileset { // Calculate the position in the world to render the tile let x = (dataIndex % worldSize.x) * width * scale.x; let y = Math.floor(dataIndex / worldSize.x) * height * scale.y; - ctx.drawImage(this.image, left, top, width, height, x - origin.x, y - origin.y, width * scale.x, height * scale.y); + ctx.drawImage(image, left, top, width, height, x - origin.x, y - origin.y, width * scale.x, height * scale.y); } } \ No newline at end of file diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index 0b01381..60ffe9f 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -147,6 +147,14 @@ export default class Vec2 { return this; } + /** + * Returns the squared distance between this vector and another vector + * @param other + */ + distanceSqTo(other: Vec2): number { + return (this.x - other.x)*(this.x - other.x) + (this.y - other.y)*(this.y - other.y); + } + /** * Returns a string representation of this vector rounded to 1 decimal point */ diff --git a/src/MainScene.ts b/src/MainScene.ts index dd19cde..dff186f 100644 --- a/src/MainScene.ts +++ b/src/MainScene.ts @@ -36,16 +36,16 @@ export default class MainScene extends Scene { startScene(){ // Add the background tilemap - let backgroundTilemap = this.add.tilemap("background", OrthogonalTilemap)[0]; + let backgroundTilemapLayer = this.add.tilemap("background")[0]; // ...and make it have parallax - backgroundTilemap.getLayer().setParallax(0.5, 0.8); - backgroundTilemap.getLayer().setAlpha(0.5); + backgroundTilemapLayer.setParallax(0.5, 0.8); + backgroundTilemapLayer.setAlpha(0.5); // Add the music and start playing it on a loop this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "level_music", loop: true, holdReference: true}); // Add the tilemap - this.add.tilemap("platformer", OrthogonalTilemap); + this.add.tilemap("platformer"); // Create the main game layer let mainLayer = this.addLayer(); diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index 4905d13..f1caf8d 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -6,12 +6,29 @@ import Vec2 from "../DataTypes/Vec2"; */ export default abstract class CanvasNode extends GameNode{ protected size: Vec2; + protected scale: Vec2; constructor(){ super(); this.size = new Vec2(0, 0); + this.scale = new Vec2(1, 1); } + /** + * Returns the scale of the sprite + */ + getScale(): Vec2 { + return this.scale; + } + + /** + * Sets the scale of the sprite to the value provided + * @param scale + */ + setScale(scale: Vec2): void { + this.scale = scale; + } + getSize(): Vec2 { return this.size; } diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index 44b9f92..73edeb5 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -7,29 +7,22 @@ import Vec2 from "../../DataTypes/Vec2"; */ export default class Sprite extends CanvasNode { private imageId: string; - private scale: Vec2; + private imageOffset: Vec2; constructor(imageId: string){ super(); this.imageId = imageId; let image = ResourceManager.getInstance().getImage(this.imageId); this.size = new Vec2(image.width, image.height); - this.scale = new Vec2(1, 1); + this.imageOffset = Vec2.ZERO; } /** - * Returns the scale of the sprite + * Sets the offset of the sprite from (0, 0) in the image's coordinates + * @param offset */ - getScale(): Vec2 { - return this.scale; - } - - /** - * Sets the scale of the sprite to the value provided - * @param scale - */ - setScale(scale: Vec2): void { - this.scale = scale; + setImageOffset(offset: Vec2): void { + this.imageOffset = offset; } update(deltaT: number): void {} @@ -38,7 +31,7 @@ export default class Sprite extends CanvasNode { let image = ResourceManager.getInstance().getImage(this.imageId); let origin = this.getViewportOriginWithParallax(); ctx.drawImage(image, - 0, 0, this.size.x, this.size.y, + this.imageOffset.x, this.imageOffset.y, this.size.x, this.size.y, this.position.x - origin.x, this.position.y - origin.y, this.size.x * this.scale.x, this.size.y * this.scale.y); } } \ No newline at end of file diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts index 6df50d1..69281e4 100644 --- a/src/Nodes/Tilemap.ts +++ b/src/Nodes/Tilemap.ts @@ -17,9 +17,9 @@ export default abstract class Tilemap extends GameNode { public visible: boolean; // TODO: Make this no longer be specific to Tiled - constructor(tilemapData: TiledTilemapData, layer: TiledLayerData) { + constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array) { super(); - this.tilesets = new Array(); + this.tilesets = tilesets; this.worldSize = new Vec2(0, 0); this.tileSize = new Vec2(0, 0); diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index 44e07ba..3aca9a0 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -26,8 +26,6 @@ export default class OrthogonalTilemap extends Tilemap { } } } - - tilemapData.tilesets.forEach(tilesetData => this.tilesets.push(new Tileset(tilesetData))); } /** diff --git a/src/Physics/PhysicsManager.ts b/src/Physics/PhysicsManager.ts index 68a8ba0..bd41691 100644 --- a/src/Physics/PhysicsManager.ts +++ b/src/Physics/PhysicsManager.ts @@ -142,15 +142,14 @@ export default class PhysicsManager { }) } - private handleCollision(movingNode: PhysicsNode, staticNode: PhysicsNode, velocity: Vec2, id: String){ + private collideWithStaticNode(movingNode: PhysicsNode, staticNode: PhysicsNode, velocity: Vec2){ let sizeA = movingNode.getCollider().getSize(); let posA = movingNode.getPosition(); let velA = velocity; let sizeB = staticNode.getCollider().getSize(); let posB = staticNode.getPosition(); - let velB = new Vec2(0, 0); - let [firstContact, _, collidingX, collidingY] = this.getTimeOfAABBCollision(posA, sizeA, velA, posB, sizeB, velB); + let [firstContact, _, collidingX, collidingY] = this.getTimeOfAABBCollision(posA, sizeA, velA, posB, sizeB, new Vec2(0, 0)); if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ if(collidingX && collidingY){ @@ -160,9 +159,20 @@ export default class PhysicsManager { // velocity.scale(contactTime); let xScale = MathUtils.clamp(firstContact.x, 0, 1); let yScale = MathUtils.clamp(firstContact.y, 0, 1); + + // Handle special case of stickiness on perfect corner to corner collisions + if(xScale === yScale){ + xScale = 1; + } + + // If we are scaling y, we're on the ground, so tell the node it's grounded + // TODO - This is a bug, check to make sure our velocity is going downwards + // Maybe feed in a downward direction to check to be sure if(yScale !== 1){ movingNode.setGrounded(true); } + + // Scale the velocity of the node velocity.scale(xScale, yScale); } } @@ -284,7 +294,9 @@ export default class PhysicsManager { // TODO handle collisions between dynamic nodes // We probably want to sort them by their left edges - // TODO: handle collisions between dynamic nodes and static nodes + for(let staticNode of staticSet){ + this.collideWithStaticNode(movingNode, staticNode, velocity); + } // Handle Collisions with the tilemaps for(let tilemap of this.tilemaps){ diff --git a/src/Physics/PhysicsNode.ts b/src/Physics/PhysicsNode.ts index 748c2df..02cd669 100644 --- a/src/Physics/PhysicsNode.ts +++ b/src/Physics/PhysicsNode.ts @@ -30,6 +30,10 @@ export default abstract class PhysicsNode extends GameNode { this.manager = manager; } + addChild(child: GameNode): void { + this.children.push(child); + } + isCollidable(): boolean { return this.collider !== null; } diff --git a/src/Physics/StaticBody.ts b/src/Physics/StaticBody.ts index 0f25c2d..1e06487 100644 --- a/src/Physics/StaticBody.ts +++ b/src/Physics/StaticBody.ts @@ -14,6 +14,7 @@ export default class StaticBody extends PhysicsNode { this.collider.setPosition(position.x, position.y); this.collider.setSize(new Vec2(size.x, size.y)); this.id = StaticBody.numCreated.toString(); + this.moving = false; StaticBody.numCreated += 1; } diff --git a/src/ResourceManager/ResourceManager.ts b/src/ResourceManager/ResourceManager.ts index f5db6c1..def15d9 100644 --- a/src/ResourceManager/ResourceManager.ts +++ b/src/ResourceManager/ResourceManager.ts @@ -233,9 +233,18 @@ export default class ResourceManager { // Grab the tileset images we need to load and add them to the imageloading queue for(let tileset of tilemapObject.tilesets){ - let key = tileset.image; - let path = StringUtils.getPathFromFilePath(pathToTilemapJSON) + key; - this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); + if(tileset.image){ + let key = tileset.image; + let path = StringUtils.getPathFromFilePath(pathToTilemapJSON) + key; + this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); + } else if(tileset.tiles){ + for(let tile of tileset.tiles){ + let key = tile.image; + let path = StringUtils.getPathFromFilePath(pathToTilemapJSON) + key; + this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); + } + } + } // Finish loading diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index def6df2..e7d2e76 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -2,6 +2,13 @@ import Scene from "../Scene"; import Tilemap from "../../Nodes/Tilemap"; import PhysicsManager from "../../Physics/PhysicsManager"; import ResourceManager from "../../ResourceManager/ResourceManager"; +import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap"; +import Layer from "../Layer"; +import Tileset from "../../DataTypes/Tilesets/Tileset"; +import Vec2 from "../../DataTypes/Vec2"; +import { TiledCollectionTile } from "../../DataTypes/Tilesets/TiledData"; +import Sprite from "../../Nodes/Sprites/Sprite"; +import StaticBody from "../../Physics/StaticBody"; export default class TilemapFactory { private scene: Scene; @@ -22,40 +29,117 @@ export default class TilemapFactory { * @param constr The constructor of the desired tilemap * @param args Additional arguments to send to the tilemap constructor */ - add = (key: string, constr: new (...a: any) => T, ...args: any): Array => { + add = (key: string): Array => { // Get Tilemap Data let tilemapData = this.resourceManager.getTilemap(key); - // Get the return values - let tilemaps = new Array(); - - for(let layer of tilemapData.layers){ - // Create a new tilemap object for the layer - let tilemap = new constr(tilemapData, layer); - tilemap.setScene(this.scene); - - // Add tilemap to scene - this.tilemaps.push(tilemap); - - // Create a new layer in the scene - let sceneLayer = this.scene.addLayer(); - sceneLayer.addNode(tilemap); - - // Register tilemap with physics if it's collidable - if(tilemap.isCollidable()){ - this.physicsManager.addTilemap(tilemap); - } - - // Assign each tileset it's image - tilemap.getTilesets().forEach(tileset => { - let image = this.resourceManager.getImage(tileset.getImageUrl()); - tileset.setImage(image); - }); - - // Update the return value - tilemaps.push(tilemap); + // Set the constructor for this tilemap to either be orthographic or isometric + let constr: new(...args: any) => Tilemap; + if(tilemapData.orientation === "orthographic"){ + constr = OrthogonalTilemap; + } else { + // No isometric tilemap support right now, so Orthographic tilemap + constr = OrthogonalTilemap; } - return tilemaps; + // Initialize the return value array + let sceneLayers = new Array(); + + // Create all of the tilesets for this tilemap + let tilesets = new Array(); + + let collectionTiles = new Array(); + + for(let tileset of tilemapData.tilesets){ + if(tileset.image){ + // If this is a standard tileset and not a collection, create a tileset for it. + // TODO - We are ignoring collection tilesets for now. This is likely not a great idea in practice, + // as theoretically someone could want to use one for a standard tilemap. We are assuming for now + // that we only want to use them for object layers + tilesets.push(new Tileset(tileset)); + } else { + tileset.tiles.forEach(tile => tile.id += tileset.firstgid); + collectionTiles.push(...tileset.tiles); + } + } + + // Loop over the layers of the tilemap and create tiledlayers or object layers + for(let layer of tilemapData.layers){ + + let sceneLayer = this.scene.addLayer(); + + if(layer.type === "tilelayer"){ + // Create a new tilemap object for the layer + let tilemap = new constr(tilemapData, layer, tilesets); + tilemap.setScene(this.scene); + + // Add tilemap to scene + this.tilemaps.push(tilemap); + + sceneLayer.addNode(tilemap); + + // Register tilemap with physics if it's collidable + if(tilemap.isCollidable()){ + this.physicsManager.addTilemap(tilemap); + } + } else { + // Layer is an object layer, so add each object as a sprite to a new layer + for(let obj of layer.objects){ + // Check if obj is collidable + let collidable = false; + + for(let prop of obj.properties){ + if(prop.name === "Collidable"){ + collidable = prop.value; + } + } + + let sprite: Sprite; + + // Check if obj is a tile from a tileset + for(let tileset of tilesets){ + if(tileset.hasTile(obj.gid)){ + // The object is a tile from this set + let imageKey = tileset.getImageKey(); + let offset = tileset.getImageOffsetForTile(obj.gid); + sprite = this.scene.add.sprite(imageKey, sceneLayer); + let size = tileset.getTileSize().clone(); + sprite.setPosition(obj.x*4, (obj.y - size.y)*4); + sprite.setImageOffset(offset); + sprite.setSize(size); + sprite.setScale(new Vec2(4, 4)); + } + } + + // Not in a tileset, must correspond to a collection + if(!sprite){ + for(let tile of collectionTiles){ + if(obj.gid === tile.id){ + let imageKey = tile.image; + sprite = this.scene.add.sprite(imageKey, sceneLayer); + sprite.setPosition(obj.x*4, (obj.y - tile.imageheight)*4); + sprite.setScale(new Vec2(4, 4)); + } + } + } + + // Now we have sprite. Associate it with our physics object if there is one + if(collidable){ + let pos = sprite.getPosition().clone(); + pos.x = Math.floor(pos.x); + pos.y = Math.floor(pos.y); + let size = sprite.getSize().clone().mult(sprite.getScale()); + let staticBody = this.scene.add.physics(StaticBody, sceneLayer, pos, size); + staticBody.addChild(sprite); + } + + } + } + + // Update the return value + sceneLayers.push(sceneLayer); + } + + return sceneLayers; } } \ No newline at end of file diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index 483755d..f4ef588 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -89,11 +89,12 @@ export default class Viewport { includes(node: CanvasNode): boolean { let nodePos = node.getPosition(); let nodeSize = node.getSize(); + let nodeScale = node.getScale(); let parallax = node.getLayer().getParallax(); let originX = this.position.x*parallax.x; let originY = this.position.y*parallax.y; - if(nodePos.x + nodeSize.x > originX && nodePos.x < originX + this.size.x){ - if(nodePos.y + nodeSize.y > originY && nodePos.y < originY + this.size.y){ + if(nodePos.x + nodeSize.x * nodeScale.x > originX && nodePos.x < originX + this.size.x){ + if(nodePos.y + nodeSize.y * nodeScale.y > originY && nodePos.y < originY + this.size.y){ return true; } } diff --git a/src/SecondScene.ts b/src/SecondScene.ts index 234177b..b060224 100644 --- a/src/SecondScene.ts +++ b/src/SecondScene.ts @@ -35,16 +35,16 @@ export default class SecondScene extends Scene { startScene(){ // Add the background tilemap - let backgroundTilemap = this.add.tilemap("background2", OrthogonalTilemap)[0]; + let backgroundTilemapLayer = this.add.tilemap("background2")[0]; // ...and make it have parallax - backgroundTilemap.getLayer().setParallax(1, 1); - backgroundTilemap.getLayer().setAlpha(0.2); + backgroundTilemapLayer.setParallax(1, 1); + backgroundTilemapLayer.setAlpha(0.2); // Add the music and start playing it on a loop this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "level_music", loop: true, holdReference: true}); // Add the tilemap - this.add.tilemap("level2", OrthogonalTilemap); + this.add.tilemap("level2"); // Create the main game layer let mainLayer = this.addLayer();