diff --git a/src/DataTypes/Tilesets/Tileset.ts b/src/DataTypes/Tilesets/Tileset.ts index b2e1346..ce78410 100644 --- a/src/DataTypes/Tilesets/Tileset.ts +++ b/src/DataTypes/Tilesets/Tileset.ts @@ -65,7 +65,7 @@ export default class Tileset { return tileIndex >= this.startIndex && tileIndex <= this.endIndex; } - renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, worldSize: Vec2, origin: Vec2): void { + renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, worldSize: Vec2, origin: Vec2, scale: Vec2): void { let index = tileIndex - this.startIndex; let row = Math.floor(index / this.numCols); let col = index % this.numCols; @@ -73,8 +73,8 @@ export default class Tileset { 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); + 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); } } \ No newline at end of file diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index b73fc2b..11ec819 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -80,4 +80,8 @@ export default class Vec2 { toFixed(): string { return "(" + this.x.toFixed(1) + ", " + this.y.toFixed(1) + ")"; } + + clone(): Vec2 { + return new Vec2(this.x, this.y); + } } \ No newline at end of file diff --git a/src/GameState/Factories/PhysicsNodeFactory.ts b/src/GameState/Factories/PhysicsNodeFactory.ts index 67d913a..037f3c6 100644 --- a/src/GameState/Factories/PhysicsNodeFactory.ts +++ b/src/GameState/Factories/PhysicsNodeFactory.ts @@ -2,6 +2,7 @@ import Scene from "../Scene"; import Viewport from "../../SceneGraph/Viewport"; import PhysicsNode from "../../Physics/PhysicsNode"; import PhysicsManager from "../../Physics/PhysicsManager"; +import Tilemap from "../../Nodes/Tilemap"; export default class PhysicsNodeFactory { private scene: Scene; @@ -20,4 +21,8 @@ export default class PhysicsNodeFactory { this.physicsManager.add(instance); return instance; } + + addTilemap(tilemap: Tilemap): void { + this.physicsManager.addTilemap(tilemap); + } } \ No newline at end of file diff --git a/src/GameState/Factories/TilemapFactory.ts b/src/GameState/Factories/TilemapFactory.ts index 37dc194..e81d0f3 100644 --- a/src/GameState/Factories/TilemapFactory.ts +++ b/src/GameState/Factories/TilemapFactory.ts @@ -28,17 +28,20 @@ export default class TilemapFactory { this.scene.addTilemap(tilemap); if(tilemap.isCollidable()){ - // Create colliders - let worldSize = tilemap.getWorldSize(); - let tileSize = tilemap.getTileSize(); + // Register in physics as a tilemap + this.scene.physics.addTilemap(tilemap); - tilemap.forEachTile((tileIndex: number, i: number) => { - if(tileIndex !== 0){ - let x = (i % worldSize.x) * tileSize.x * 4; - let y = Math.floor(i / worldSize.x) * tileSize.y * 4; - this.scene.physics.add(StaticBody, new Vec2(x, y), new Vec2(tileSize.x * 4, tileSize.y * 4)); - } - }); + // Create colliders + // let worldSize = tilemap.getWorldSize(); + // let tileSize = tilemap.getTileSize(); + + // tilemap.forEachTile((tileIndex: number, i: number) => { + // if(tileIndex !== 0){ + // let x = (i % worldSize.x) * tileSize.x * 4; + // let y = Math.floor(i / worldSize.x) * tileSize.y * 4; + // this.scene.physics.add(StaticBody, new Vec2(x, y), new Vec2(tileSize.x * 4, tileSize.y * 4)); + // } + // }); } // Load images for the tilesets diff --git a/src/GameState/GameState.ts b/src/GameState/GameState.ts index 8963baf..acf121b 100644 --- a/src/GameState/GameState.ts +++ b/src/GameState/GameState.ts @@ -8,11 +8,10 @@ export default class GameState{ private worldSize: Vec2; private viewport: Viewport; - constructor(){ + constructor(viewport: Viewport){ this.sceneStack = new Stack(10); this.worldSize = new Vec2(1600, 1000); - this.viewport = new Viewport(); - this.viewport.setSize(800, 500); + this.viewport = viewport; this.viewport.setBounds(0, 0, 2560, 1280); } diff --git a/src/GameState/Scene.ts b/src/GameState/Scene.ts index 0b6a031..9c575f5 100644 --- a/src/GameState/Scene.ts +++ b/src/GameState/Scene.ts @@ -98,6 +98,7 @@ export default class Scene { this.viewport.update(deltaT); this.physicsManager.update(deltaT); this.sceneGraph.update(deltaT); + this.tilemaps.forEach((tilemap: Tilemap) => tilemap.update(deltaT)); } } diff --git a/src/Input/InputReceiver.ts b/src/Input/InputReceiver.ts index 4e060cf..c6ab8b8 100644 --- a/src/Input/InputReceiver.ts +++ b/src/Input/InputReceiver.ts @@ -2,6 +2,7 @@ import Receiver from "../Events/Receiver"; import Map from "../DataTypes/Map"; import Vec2 from "../DataTypes/Vec2"; import EventQueue from "../Events/EventQueue"; +import Viewport from "../SceneGraph/Viewport"; export default class InputReceiver{ private static instance: InputReceiver = null; @@ -14,6 +15,7 @@ export default class InputReceiver{ private mousePressPosition: Vec2; private eventQueue: EventQueue; private receiver: Receiver; + private viewport: Viewport; private constructor(){ this.mousePressed = false; @@ -108,7 +110,19 @@ export default class InputReceiver{ return this.mousePosition; } + getGlobalMousePosition(): Vec2 { + return this.mousePosition.clone().add(this.viewport.getPosition()); + } + getMousePressPosition(): Vec2 { return this.mousePressPosition; } + + getGlobalMousePressPosition(): Vec2 { + return this.mousePressPosition.clone().add(this.viewport.getPosition()); + } + + setViewport(viewport: Viewport): void { + this.viewport = viewport; + } } \ No newline at end of file diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 279d701..77d00d8 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -5,6 +5,7 @@ import Recorder from "../Playback/Recorder"; import GameState from "../GameState/GameState"; import Debug from "../Debug/Debug"; import ResourceManager from "../ResourceManager/ResourceManager"; +import Viewport from "../SceneGraph/Viewport"; export default class GameLoop{ // The amount of time to spend on a physics step @@ -29,7 +30,8 @@ export default class GameLoop{ readonly GAME_CANVAS: HTMLCanvasElement; readonly WIDTH: number; - readonly HEIGHT: number; + readonly HEIGHT: number; + private viewport: Viewport; private ctx: CanvasRenderingContext2D; private eventQueue: EventQueue; private inputHandler: InputHandler; @@ -57,12 +59,15 @@ export default class GameLoop{ this.WIDTH = 800; this.HEIGHT = 500; this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); + this.viewport = new Viewport(); + this.viewport.setSize(this.WIDTH, this.HEIGHT); this.eventQueue = EventQueue.getInstance(); this.inputHandler = new InputHandler(this.GAME_CANVAS); this.inputReceiver = InputReceiver.getInstance(); + this.inputReceiver.setViewport(this.viewport); this.recorder = new Recorder(); - this.gameState = new GameState(); + this.gameState = new GameState(this.viewport); this.debug = Debug.getInstance(); this.resourceManager = ResourceManager.getInstance(); } diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts index 4e60a3a..c6d95c1 100644 --- a/src/Nodes/Tilemap.ts +++ b/src/Nodes/Tilemap.ts @@ -8,11 +8,13 @@ import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledDat */ export default abstract class Tilemap extends GameNode { protected data: number[]; + protected collisionData: number []; protected tilesets: Tileset[]; protected worldSize: Vec2; protected tileSize: Vec2; protected visible: boolean; protected collidable: boolean; + protected scale: Vec2; constructor(tilemapData: TiledTilemapData, layerData: TiledLayerData){ super(); @@ -20,6 +22,7 @@ export default abstract class Tilemap extends GameNode { this.worldSize = new Vec2(0, 0); this.tileSize = new Vec2(0, 0); this.parseTilemapData(tilemapData, layerData); + this.scale = new Vec2(4, 4); } isCollidable(): boolean { @@ -39,9 +42,19 @@ export default abstract class Tilemap extends GameNode { } getTileSize(): Vec2 { - return this.tileSize; + return this.tileSize.clone().scale(this.scale.x, this.scale.y); } + getScale(): Vec2 { + return this.scale; + } + + setScale(scale: Vec2): void { + this.scale = scale; + } + + abstract getTileAt(worldCoords: Vec2): number; + isReady(): boolean { if(this.tilesets.length !== 0){ for(let tileset of this.tilesets){ diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index c3d4512..404e8cd 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -10,6 +10,7 @@ export default class OrthogonalTilemap extends Tilemap { this.worldSize.set(tilemapData.width, tilemapData.height); this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight); this.data = layer.data; + this.collisionData = this.data.map(tile => tile !== 0 ? 1 : 0); this.visible = layer.visible; this.collidable = false; if(layer.properties){ @@ -22,7 +23,40 @@ export default class OrthogonalTilemap extends Tilemap { tilemapData.tilesets.forEach(tilesetData => this.tilesets.push(new Tileset(tilesetData))); } - forEachTile(func: Function){ + getTileAt(worldCoords: Vec2): number { + let localCoords = this.getColRowAt(worldCoords); + if(localCoords.x < 0 || localCoords.x >= this.worldSize.x || localCoords.y < 0 || localCoords.y >= this.worldSize.y){ + // There are no tiles in negative positions or out of bounds positions + return 0; + } + + return this.data[localCoords.y * this.worldSize.x + localCoords.x]; + } + + isTileCollidable(indexOrCol: number, row?: number): boolean { + if(row){ + if(indexOrCol < 0 || indexOrCol >= this.worldSize.x || row < 0 || row >= this.worldSize.y){ + // There are no tiles in negative positions or out of bounds positions + return false; + } + return this.collisionData[row * this.worldSize.x + indexOrCol] === 1 && this.collidable; + } else { + if(indexOrCol < 0 || indexOrCol >= this.collisionData.length){ + // Tiles that don't exist aren't collidable + return false; + } + return this.collisionData[indexOrCol] === 1 && this.collidable; + } + } + + // TODO: Should this throw an error if someone tries to access an out of bounds value? + getColRowAt(worldCoords: Vec2): Vec2 { + let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x); + let row = Math.floor(worldCoords.y / this.tileSize.y / this.scale.y); + return new Vec2(col, row); + } + + forEachTile(func: Function): void { for(let i = 0; i < this.data.length; i++){ func(this.data[i], i); } @@ -37,7 +71,7 @@ export default class OrthogonalTilemap extends Tilemap { for(let tileset of this.tilesets){ if(tileset.hasTile(tileIndex)){ - tileset.renderTile(ctx, tileIndex, i, this.worldSize, origin); + tileset.renderTile(ctx, tileIndex, i, this.worldSize, origin, this.scale); } } } diff --git a/src/Physics/PhysicsManager.ts b/src/Physics/PhysicsManager.ts index 0e0aea1..c8c6338 100644 --- a/src/Physics/PhysicsManager.ts +++ b/src/Physics/PhysicsManager.ts @@ -3,14 +3,18 @@ import Vec2 from "../DataTypes/Vec2"; import StaticBody from "./StaticBody"; import Debug from "../Debug/Debug"; import MathUtils from "../Utils/MathUtils"; +import Tilemap from "../Nodes/Tilemap"; +import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; export default class PhysicsManager { physicsNodes: Array; + tilemaps: Array; movements: Array; constructor(){ this.physicsNodes = new Array(); + this.tilemaps = new Array(); this.movements = new Array(); } @@ -18,6 +22,10 @@ export default class PhysicsManager { this.physicsNodes.push(node); } + addTilemap(tilemap: Tilemap): void { + this.tilemaps.push(tilemap); + } + addMovement(node: PhysicsNode, velocity: Vec2){ this.movements.push(new MovementData(node, velocity)); } @@ -51,8 +59,14 @@ export default class PhysicsManager { } } - for(let staticNode of staticSet){ - this.handleCollision(movingNode, staticNode, velocity, (staticNode).id); + // 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 + + // Handle Collisions with the tilemaps + for(let tilemap of this.tilemaps){ + this.collideWithTilemap(movingNode, tilemap, velocity); } movingNode.finishMove(velocity); @@ -62,102 +76,90 @@ export default class PhysicsManager { this.movements = new Array(); } + collideWithTilemap(node: PhysicsNode, tilemap: Tilemap, velocity: Vec2){ + if(tilemap instanceof OrthogonalTilemap){ + this.collideWithOrthogonalTilemap(node, tilemap, velocity); + } + } + + collideWithOrthogonalTilemap(node: PhysicsNode, tilemap: OrthogonalTilemap, velocity: Vec2){ + let startPos = node.getPosition(); + let endPos = startPos.clone().add(velocity); + let size = node.getCollider().getSize(); + let min = new Vec2(Math.min(startPos.x, endPos.x), Math.min(startPos.y, endPos.y)); + let max = new Vec2(Math.max(startPos.x + size.x, endPos.x + size.x), Math.max(startPos.y + size.y, endPos.y + size.y)); + + let minIndex = tilemap.getColRowAt(min); + let maxIndex = tilemap.getColRowAt(max); + + let tilemapCollisions = new Array(); + let tileSize = tilemap.getTileSize(); + + Debug.getInstance().log("tilemapCollision", ""); + // Loop over all possible tiles + for(let col = minIndex.x; col <= maxIndex.x; col++){ + for(let row = minIndex.y; row <= maxIndex.y; row++){ + if(tilemap.isTileCollidable(col, row)){ + Debug.getInstance().log("tilemapCollision", "Colliding with Tile"); + + // Tile position + let tilePos = new Vec2(col * tileSize.x, row * tileSize.y); + + // Calculate collision area + let dx = Math.min(startPos.x, tilePos.x) - Math.max(startPos.x + size.x, tilePos.x + size.x); + let dy = Math.min(startPos.y, tilePos.y) - Math.max(startPos.y + size.y, tilePos.y + size.y); + + let overlap = 0; + if(dx * dy > 0){ + overlap = dx * dy; + } + + tilemapCollisions.push(new TileCollisionData(tilePos, overlap)); + } + } + } + + // Now that we have all collisions, sort by collision area + tilemapCollisions = tilemapCollisions.sort((a, b) => a.overlapArea - b.overlapArea); + + // Resolve the collisions + tilemapCollisions.forEach(collision => { + let [firstContact, _, collidingX, collidingY] = this.getTimeOfCollision(startPos, size, velocity, collision.position, tileSize, new Vec2(0, 0)); + + // Handle collision + if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ + if(collidingX && collidingY){ + // If we're already intersecting, freak out I guess? + } else { + // let contactTime = Math.min(firstContact.x, firstContact.y); + // 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 corner to corner collisions + if(xScale === yScale){ + xScale = 1; + } + + if(yScale !== 1){ + node.setIsGrounded(true); + } + + velocity.scale(xScale, yScale); + } + } + }) + } + handleCollision(movingNode: PhysicsNode, staticNode: PhysicsNode, velocity: Vec2, id: String){ let sizeA = movingNode.getCollider().getSize(); - let A = movingNode.getPosition(); + let posA = movingNode.getPosition(); let velA = velocity; let sizeB = staticNode.getCollider().getSize(); - let B = staticNode.getPosition(); + let posB = staticNode.getPosition(); let velB = new Vec2(0, 0); - - let firstContact = new Vec2(0, 0); - let lastContact = new Vec2(0, 0); - let collidingX = false; - let collidingY = false; - - // Sort by position - if(B.x < A.x){ - // Swap, because B is to the left of A - let temp: Vec2; - temp = sizeA; - sizeA = sizeB; - sizeB = temp; - - temp = A; - A = B; - B = temp; - - temp = velA; - velA = velB; - velB = temp; - } - - // A is left, B is right - firstContact.x = Infinity; - lastContact.x = Infinity; - - if (B.x >= A.x + sizeA.x){ - // If we aren't currently colliding - let relVel = velA.x - velB.x; - - if(relVel > 0){ - // If they are moving towards each other - firstContact.x = (B.x - (A.x + (sizeA.x)))/(relVel); - lastContact.x = ((B.x + sizeB.x) - A.x)/(relVel); - } - } else { - collidingX = true; - } - - if(B.y < A.y){ - // Swap, because B is above A - let temp: Vec2; - temp = sizeA; - sizeA = sizeB; - sizeB = temp; - - temp = A; - A = B; - B = temp; - - temp = velA; - velA = velB; - velB = temp; - } - - // A is top, B is bottom - firstContact.y = Infinity; - lastContact.y = Infinity; - - if (B.y >= A.y + sizeA.y){ - // If we aren't currently colliding - let relVel = velA.y - velB.y; - - if(relVel > 0){ - // If they are moving towards each other - firstContact.y = (B.y - (A.y + (sizeA.y)))/(relVel); - lastContact.y = ((B.y + sizeB.y) - A.y)/(relVel); - } - } else { - collidingY = true; - } - - if(B.x < A.x){ - // Swap, because B is to the left of A - let temp: Vec2; - temp = sizeA; - sizeA = sizeB; - sizeB = temp; - - temp = A; - A = B; - B = temp; - - temp = velA; - velA = velB; - velB = temp; - } + let [firstContact, _, collidingX, collidingY] = this.getTimeOfCollision(posA, sizeA, velA, posB, sizeB, velB); if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ if(collidingX && collidingY){ @@ -169,13 +171,96 @@ export default class PhysicsManager { let yScale = MathUtils.clamp(firstContact.y, 0, 1); if(yScale !== 1){ movingNode.setIsGrounded(true); - } + } velocity.scale(xScale, yScale); } } } + + /** + * Gets the collision time of two AABBs using continuous collision checking. Returns vectors representing the time + * of the start and end of the collision and booleans for whether or not the objects are currently overlapping + * (before they move). + */ + getTimeOfCollision(posA: Vec2, sizeA: Vec2, velA: Vec2, posB: Vec2, sizeB: Vec2, velB: Vec2): [Vec2, Vec2, boolean, boolean] { + let firstContact = new Vec2(0, 0); + let lastContact = new Vec2(0, 0); + + let collidingX = false; + let collidingY = false; + + // Sort by position + if(posB.x < posA.x){ + // Swap, because B is to the left of A + let temp: Vec2; + temp = sizeA; + sizeA = sizeB; + sizeB = temp; + + temp = posA; + posA = posB; + posB = temp; + + temp = velA; + velA = velB; + velB = temp; + } + + // A is left, B is right + firstContact.x = Infinity; + lastContact.x = Infinity; + + if (posB.x >= posA.x + sizeA.x){ + // If we aren't currently colliding + let relVel = velA.x - velB.x; + + if(relVel > 0){ + // If they are moving towards each other + firstContact.x = (posB.x - (posA.x + (sizeA.x)))/(relVel); + lastContact.x = ((posB.x + sizeB.x) - posA.x)/(relVel); + } + } else { + collidingX = true; + } + + if(posB.y < posA.y){ + // Swap, because B is above A + let temp: Vec2; + temp = sizeA; + sizeA = sizeB; + sizeB = temp; + + temp = posA; + posA = posB; + posB = temp; + + temp = velA; + velA = velB; + velB = temp; + } + + // A is top, B is bottom + firstContact.y = Infinity; + lastContact.y = Infinity; + + if (posB.y >= posA.y + sizeA.y){ + // If we aren't currently colliding + let relVel = velA.y - velB.y; + + if(relVel > 0){ + // If they are moving towards each other + firstContact.y = (posB.y - (posA.y + (sizeA.y)))/(relVel); + lastContact.y = ((posB.y + sizeB.y) - posA.y)/(relVel); + } + } else { + collidingY = true; + } + + return [firstContact, lastContact, collidingX, collidingY]; + } } +// Helper classes for internal data class MovementData{ node: PhysicsNode; velocity: Vec2; @@ -183,4 +268,13 @@ class MovementData{ this.node = node; this.velocity = velocity; } +} + +class TileCollisionData { + position: Vec2; + overlapArea: number; + constructor(position: Vec2, overlapArea: number){ + this.position = position; + this.overlapArea = overlapArea; + } } \ No newline at end of file diff --git a/src/Player.ts b/src/Player.ts index 196c059..b13fc7b 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -16,7 +16,7 @@ export default class Player extends PhysicsNode { super(); this.type = type; this.velocity = new Vec2(0, 0); - this.speed = 500; + this.speed = 600; this.size = new Vec2(50, 50); this.collider = new AABB(); this.collider.setSize(this.size); diff --git a/src/main.ts b/src/main.ts index d015000..fa87311 100644 --- a/src/main.ts +++ b/src/main.ts @@ -74,12 +74,12 @@ function main(){ pauseMenu.disable(); } - // backgroundScene.tilemap.add(OrthogonalTilemap, "assets/tilemaps/Background.json"); - // mainScene.tilemap.add(OrthogonalTilemap, "assets/tilemaps/Platformer.json"); - // let player = mainScene.physics.add(Player, "platformer"); + backgroundScene.tilemap.add(OrthogonalTilemap, "assets/tilemaps/Background.json"); + mainScene.tilemap.add(OrthogonalTilemap, "assets/tilemaps/Platformer.json"); + let player = mainScene.physics.add(Player, "platformer"); - mainScene.tilemap.add(OrthogonalTilemap, "assets/tilemaps/TopDown.json"); - let player = mainScene.physics.add(Player, "topdown"); + // mainScene.tilemap.add(OrthogonalTilemap, "assets/tilemaps/TopDown.json"); + // let player = mainScene.physics.add(Player, "topdown"); mainScene.getViewport().follow(player); diff --git a/tsconfig.json b/tsconfig.json index ca76d56..3c1a95a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,7 @@ "src/Loop/GameLoop.ts", - "src/Nodes/Tilemaps/OrgthogonalTilemap.ts", + "src/Nodes/Tilemaps/OrthogonalTilemap.ts", "src/Nodes/UIElements/Button.ts", "src/Nodes/UIElements/Label.ts", "src/Nodes/CanvasNode.ts",