From 254462993a818e9de9cafd534cff0368d3134c15 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Mon, 5 Oct 2020 15:01:26 -0400 Subject: [PATCH] converted all code to work with AABBs --- src/DataTypes/AABB.ts | 39 ++++++- src/DataTypes/RegionQuadTree.ts | 6 +- src/DataTypes/Shape.ts | 8 ++ src/DataTypes/Vec2.ts | 38 +++++-- src/Input/InputReceiver.ts | 4 +- src/Loop/GameLoop.ts | 1 - src/MainScene.ts | 15 ++- src/Nodes/CanvasNode.ts | 9 +- src/Nodes/GameNode.ts | 7 +- src/Nodes/Sprites/Sprite.ts | 9 +- src/Nodes/Tilemap.ts | 12 +- src/Nodes/Tilemaps/OrthogonalTilemap.ts | 12 +- src/Nodes/UIElement.ts | 17 ++- src/Physics/Colliders/AABBCollider.ts | 29 ----- src/Physics/Colliders/Collider.ts | 40 +++++-- src/Physics/Colliders/Collisions.ts | 97 ++++++++++++++++ src/Physics/PhysicsManager.ts | 145 ++++++++---------------- src/Physics/PhysicsNode.ts | 2 +- src/Physics/StaticBody.ts | 8 +- src/Player.ts | 14 ++- src/Scene/Factories/TilemapFactory.ts | 14 +-- src/Scene/Scene.ts | 3 + src/Scene/SceneManager.ts | 2 +- src/SceneGraph/SceneGraphQuadTree.ts | 9 +- src/SceneGraph/Viewport.ts | 78 ++++++++----- src/main.ts | 5 +- 26 files changed, 383 insertions(+), 240 deletions(-) create mode 100644 src/DataTypes/Shape.ts delete mode 100644 src/Physics/Colliders/AABBCollider.ts create mode 100644 src/Physics/Colliders/Collisions.ts diff --git a/src/DataTypes/AABB.ts b/src/DataTypes/AABB.ts index 736ed5b..10b71e8 100644 --- a/src/DataTypes/AABB.ts +++ b/src/DataTypes/AABB.ts @@ -1,11 +1,13 @@ +import Shape from "./Shape"; import Vec2 from "./Vec2"; -export default class AABB { +export default class AABB extends Shape { protected center: Vec2; protected halfSize: Vec2; constructor(center?: Vec2, halfSize?: Vec2){ + super(); this.center = center ? center : new Vec2(0, 0); this.halfSize = halfSize ? halfSize : new Vec2(0, 0); } @@ -26,6 +28,22 @@ export default class AABB { return this.halfSize.y; } + get top(): number { + return this.y - this.hh; + } + + get bottom(): number { + return this.y + this.hh; + } + + get left(): number { + return this.x - this.hw; + } + + get right(): number { + return this.x + this.hw; + } + getCenter(): Vec2 { return this.center; } @@ -34,6 +52,10 @@ export default class AABB { this.center = center; } + getBoundingRect(): AABB { + return this; + } + getHalfSize(): Vec2 { return this.halfSize; } @@ -100,4 +122,19 @@ export default class AABB { return true; } + + // TODO - Implement this generally and use it in the tilemap + overlapArea(other: AABB): number { + let leftx = Math.max(this.x - this.hw, other.x - other.hw); + let rightx = Math.min(this.x + this.hw, other.x + other.hw); + let dx = rightx - leftx; + + let lefty = Math.max(this.y - this.hh, other.y - other.hh); + let righty = Math.min(this.y + this.hh, other.y + other.hh); + let dy = righty - lefty; + + if(dx < 0 || dy < 0) return 0; + + return dx*dy; + } } \ No newline at end of file diff --git a/src/DataTypes/RegionQuadTree.ts b/src/DataTypes/RegionQuadTree.ts index 0afd642..8e4fbed 100644 --- a/src/DataTypes/RegionQuadTree.ts +++ b/src/DataTypes/RegionQuadTree.ts @@ -123,8 +123,9 @@ export default class QuadTree implements Collection { } /** - * Returns all items at this point. - * @param point The point to query at + * Returns all items in this region + * @param boundary The region to check + * @param inclusionCheck Allows for additional inclusion checks to further refine searches */ queryRegion(boundary: AABB): Array { // A matrix to keep track of our results @@ -212,6 +213,7 @@ export default class QuadTree implements Collection { * @param ctx */ public render_demo(ctx: CanvasRenderingContext2D): void { + return; ctx.strokeStyle = "#0000FF"; ctx.strokeRect(this.boundary.x - this.boundary.hw, this.boundary.y - this.boundary.hh, 2*this.boundary.hw, 2*this.boundary.hh); diff --git a/src/DataTypes/Shape.ts b/src/DataTypes/Shape.ts new file mode 100644 index 0000000..610a876 --- /dev/null +++ b/src/DataTypes/Shape.ts @@ -0,0 +1,8 @@ +import AABB from "./AABB"; +import Vec2 from "./Vec2"; + +export default abstract class Shape { + abstract setCenter(center: Vec2): void; + abstract getCenter(): Vec2; + abstract getBoundingRect(): AABB; +} \ No newline at end of file diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index 751b311..f3e1dca 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -4,26 +4,31 @@ export default class Vec2 { // Store x and y in an array - private vec: Float32Array; + //private vec: Float32Array; + + protected _x: number; + protected _y: number; /** * When this vector changes its value, do something */ - private onChange: Function; + private onChange: Function = () => {}; constructor(x: number = 0, y: number = 0) { - this.vec = new Float32Array(2); - this.vec[0] = x; - this.vec[1] = y; + // this.vec = new Float32Array(2); + // this.vec[0] = x; + // this.vec[1] = y; + this._x = x; + this._y = y; } // Expose x and y with getters and setters get x() { - return this.vec[0]; + return this._x; //this.vec[0]; } set x(x: number) { - this.vec[0] = x; + this._x = x;//this.vec[0] = x; if(this.onChange){ this.onChange(); @@ -31,11 +36,11 @@ export default class Vec2 { } get y() { - return this.vec[1]; + return this._y;//this.vec[1]; } set y(y: number) { - this.vec[1] = y; + this._y = y;//this.vec[1] = y; if(this.onChange){ this.onChange(); @@ -181,6 +186,17 @@ export default class Vec2 { return this; } + /** + * Divides this vector with another vector element-wise + * @param other + */ + div(other: Vec2): Vec2 { + if(other.x === 0 || other.y === 0) throw "Divide by zero error"; + this.x /= other.x; + this.y /= other.y; + return this; + } + /** * Returns the squared distance between this vector and another vector * @param other @@ -218,4 +234,8 @@ export default class Vec2 { setOnChange(f: Function): void { this.onChange = f; } + + getOnChange(): string { + return this.onChange.toString(); + } } \ No newline at end of file diff --git a/src/Input/InputReceiver.ts b/src/Input/InputReceiver.ts index 4a8b0b3..eeac2f5 100644 --- a/src/Input/InputReceiver.ts +++ b/src/Input/InputReceiver.ts @@ -120,7 +120,7 @@ export default class InputReceiver{ } getGlobalMousePosition(): Vec2 { - return this.mousePosition.clone().add(this.viewport.getPosition()); + return this.mousePosition.clone().add(this.viewport.getOrigin()); } getMousePressPosition(): Vec2 { @@ -128,7 +128,7 @@ export default class InputReceiver{ } getGlobalMousePressPosition(): Vec2 { - return this.mousePressPosition.clone().add(this.viewport.getPosition()); + return this.mousePressPosition.clone().add(this.viewport.getOrigin()); } setViewport(viewport: Viewport): void { diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index ee05000..253b6e5 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -48,7 +48,6 @@ export default class GameLoop { constructor(config?: object){ // Typecast the config object to a GameConfig object let gameConfig = config ? config : new GameConfig(); - console.log(gameConfig) this.maxFPS = 60; this.simulationTimestep = Math.floor(1000/this.maxFPS); diff --git a/src/MainScene.ts b/src/MainScene.ts index bfc0a74..d847c3e 100644 --- a/src/MainScene.ts +++ b/src/MainScene.ts @@ -9,6 +9,7 @@ import Button from "./Nodes/UIElements/Button"; import Layer from "./Scene/Layer"; import SecondScene from "./SecondScene"; import { GameEventType } from "./Events/GameEventType"; +import SceneGraphQuadTree from "./SceneGraph/SceneGraphQuadTree"; export default class MainScene extends Scene { @@ -17,7 +18,7 @@ export default class MainScene extends Scene { this.load.tilemap("background", "assets/tilemaps/Background.json"); this.load.image("player", "assets/sprites/player.png"); this.load.audio("player_jump", "assets/sounds/jump-3.wav"); - this.load.audio("level_music", "assets/sounds/level.wav"); + //this.load.audio("level_music", "assets/sounds/level.wav"); let loadingScreen = this.addLayer(); let box = this.add.graphic(Rect, loadingScreen, new Vec2(200, 300), new Vec2(400, 60)); @@ -35,17 +36,23 @@ export default class MainScene extends Scene { } startScene(){ + // Set world size + this.worldSize = new Vec2(2560, 1280) + + // Use a quadtree + this.sceneGraph = new SceneGraphQuadTree(this.viewport, this); + // Add the background tilemap - let backgroundTilemapLayer = this.add.tilemap("background")[0]; + let backgroundTilemapLayer = this.add.tilemap("background", new Vec2(4, 4))[0]; // ...and make it have parallax 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}); + //this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "level_music", loop: true, holdReference: true}); // Add the tilemap - this.add.tilemap("platformer"); + this.add.tilemap("platformer", new Vec2(4, 4)); // Create the main game layer let mainLayer = this.addLayer(); diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index 48e55c3..60993ef 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -13,6 +13,7 @@ export default abstract class CanvasNode extends GameNode implements Region { constructor(){ super(); + this.position.setOnChange(this.positionChanged); this._size = new Vec2(0, 0); this._size.setOnChange(this.sizeChanged); this._scale = new Vec2(1, 1); @@ -37,7 +38,7 @@ export default abstract class CanvasNode extends GameNode implements Region { set scale(scale: Vec2){ this._scale = scale; - this._scale.setOnChange(this.sizeChanged); + this._scale.setOnChange(this.scaleChanged); this.scaleChanged(); } @@ -68,15 +69,15 @@ export default abstract class CanvasNode extends GameNode implements Region { this.scale = scale; } - positionChanged = (): void => { + protected positionChanged = (): void => { this.updateBoundary(); } - sizeChanged = (): void => { + protected sizeChanged = (): void => { this.updateBoundary(); } - scaleChanged = (): void => { + protected scaleChanged = (): void => { this.updateBoundary(); } diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 7647d75..8b97f49 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -6,6 +6,7 @@ import Emitter from "../Events/Emitter"; import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; import { Positioned, Unique } from "../DataTypes/Interfaces/Descriptors" +import UIElement from "./UIElement"; /** * The representation of an object in the game world @@ -76,11 +77,11 @@ export default abstract class GameNode implements Positioned, Unique { /** * Called if the position vector is modified or replaced */ - protected positionChanged(){} + protected positionChanged = (): void => {}; // TODO - This doesn't seem ideal. Is there a better way to do this? - protected getViewportOriginWithParallax(): Vec2 { - return this.scene.getViewport().getPosition().clone().mult(this.layer.getParallax()); + getViewportOriginWithParallax(): Vec2 { + return this.scene.getViewport().getOrigin().mult(this.layer.getParallax()); } abstract update(deltaT: number): void; diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index 73edeb5..9474663 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -30,8 +30,15 @@ export default class Sprite extends CanvasNode { render(ctx: CanvasRenderingContext2D): void { let image = ResourceManager.getInstance().getImage(this.imageId); let origin = this.getViewportOriginWithParallax(); + ctx.drawImage(image, 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); + this.position.x - origin.x - this.size.x*this.scale.x/2, this.position.y - origin.y - this.size.y*this.scale.y/2, + this.size.x * this.scale.x, this.size.y * this.scale.y); + + ctx.lineWidth = 4; + ctx.strokeStyle = "#00FF00" + let b = this.getBoundary(); + ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2, b.hh*2); } } \ No newline at end of file diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts index 69281e4..a31f2cc 100644 --- a/src/Nodes/Tilemap.ts +++ b/src/Nodes/Tilemap.ts @@ -9,7 +9,7 @@ import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledDat export default abstract class Tilemap extends GameNode { // A tileset represents the tiles within one specific image loaded from a file protected tilesets: Array; - protected worldSize: Vec2; + protected size: Vec2; protected tileSize: Vec2; protected scale: Vec2; public data: Array; @@ -17,23 +17,23 @@ export default abstract class Tilemap extends GameNode { public visible: boolean; // TODO: Make this no longer be specific to Tiled - constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array) { + constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array, scale: Vec2) { super(); this.tilesets = tilesets; - this.worldSize = new Vec2(0, 0); + this.size = new Vec2(0, 0); this.tileSize = new Vec2(0, 0); // Defer parsing of the data to child classes - this allows for isometric vs. orthographic tilemaps and handling of Tiled data or other data this.parseTilemapData(tilemapData, layer); - this.scale = new Vec2(4, 4); + this.scale = scale.clone(); } getTilesets(): Tileset[] { return this.tilesets; } - getWorldSize(): Vec2 { - return this.worldSize; + getsize(): Vec2 { + return this.size; } getTileSize(): Vec2 { diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index 3aca9a0..41d7e61 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -14,7 +14,7 @@ export default class OrthogonalTilemap extends Tilemap { * @param layer */ protected parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void { - this.worldSize.set(tilemapData.width, tilemapData.height); + this.size.set(tilemapData.width, tilemapData.height); this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight); this.data = layer.data; this.visible = layer.visible; @@ -34,12 +34,12 @@ export default class OrthogonalTilemap extends Tilemap { */ 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){ + if(localCoords.x < 0 || localCoords.x >= this.size.x || localCoords.y < 0 || localCoords.y >= this.size.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] + return this.data[localCoords.y * this.size.x + localCoords.x] } /** @@ -50,11 +50,11 @@ export default class OrthogonalTilemap extends Tilemap { isTileCollidable(indexOrCol: number, row?: number): boolean { let index = 0; if(row){ - if(indexOrCol < 0 || indexOrCol >= this.worldSize.x || row < 0 || row >= this.worldSize.y){ + if(indexOrCol < 0 || indexOrCol >= this.size.x || row < 0 || row >= this.size.y){ // There are no tiles in negative positions or out of bounds positions return false; } - index = row * this.worldSize.x + indexOrCol; + index = row * this.size.x + indexOrCol; } else { if(indexOrCol < 0 || indexOrCol >= this.data.length){ // Tiles that don't exist aren't collidable @@ -93,7 +93,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, this.scale); + tileset.renderTile(ctx, tileIndex, i, this.size, origin, this.scale); } } } diff --git a/src/Nodes/UIElement.ts b/src/Nodes/UIElement.ts index 72face5..78ac054 100644 --- a/src/Nodes/UIElement.ts +++ b/src/Nodes/UIElement.ts @@ -5,7 +5,7 @@ import Vec2 from "../DataTypes/Vec2"; /** * The representation of a UIElement - the parent class of things like buttons */ -export default class UIElement extends CanvasNode{ +export default class UIElement extends CanvasNode { // Style attributes protected textColor: Color; protected backgroundColor: Color; @@ -179,22 +179,29 @@ export default class UIElement extends CanvasNode{ let previousAlpha = ctx.globalAlpha; ctx.globalAlpha = this.getLayer().getAlpha(); - let origin = this.scene.getViewport().getPosition().clone().mult(this.layer.getParallax()); + let origin = this.getViewportOriginWithParallax(); ctx.font = this.fontSize + "px " + this.font; let offset = this.calculateOffset(ctx); // Stroke and fill a rounded rect and give it text ctx.fillStyle = this.calculateBackgroundColor(); - ctx.fillRoundedRect(this.position.x - origin.x, this.position.y - origin.y, this.size.x, this.size.y, this.borderRadius); + ctx.fillRoundedRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, + this.size.x, this.size.y, this.borderRadius); ctx.strokeStyle = this.calculateBorderColor(); ctx.lineWidth = this.borderWidth; - ctx.strokeRoundedRect(this.position.x - origin.x, this.position.y - origin.y, this.size.x, this.size.y, this.borderRadius); + ctx.strokeRoundedRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, + this.size.x, this.size.y, this.borderRadius); ctx.fillStyle = this.calculateTextColor(); - ctx.fillText(this.text, this.position.x + offset.x - origin.x, this.position.y + offset.y - origin.y); + ctx.fillText(this.text, this.position.x + offset.x - origin.x - this.size.x/2, this.position.y + offset.y - origin.y - this.size.y/2); ctx.globalAlpha = previousAlpha; + + ctx.lineWidth = 4; + ctx.strokeStyle = "#00FF00" + let b = this.getBoundary(); + ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2, b.hh*2); } } \ No newline at end of file diff --git a/src/Physics/Colliders/AABBCollider.ts b/src/Physics/Colliders/AABBCollider.ts deleted file mode 100644 index b97829d..0000000 --- a/src/Physics/Colliders/AABBCollider.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Collider from "./Collider"; -import Vec2 from "../../DataTypes/Vec2"; - -export default class AABBCollider extends Collider { - - isCollidingWith(other: Collider): boolean { - if(other instanceof AABBCollider){ - if(other.position.x > this.position.x && other.position.x < this.position.x + this.size.x){ - return other.position.y > this.position.y && other.position.y < this.position.y + this.size.y; - } - } - return false; - } - - willCollideWith(other: Collider, thisVel: Vec2, otherVel: Vec2): boolean { - if(other instanceof AABBCollider){ - let thisPos = new Vec2(this.position.x + thisVel.x, this.position.y + thisVel.y); - let otherPos = new Vec2(other.position.x + otherVel.x, other.position.y + otherVel.y); - - if(otherPos.x > thisPos.x && otherPos.x < thisPos.x + this.size.x){ - return otherPos.y > thisPos.y && otherPos.y < thisPos.y + this.size.y; - } - } - return false; - } - - update(deltaT: number): void {} - -} \ No newline at end of file diff --git a/src/Physics/Colliders/Collider.ts b/src/Physics/Colliders/Collider.ts index 91e40e5..b2030f9 100644 --- a/src/Physics/Colliders/Collider.ts +++ b/src/Physics/Colliders/Collider.ts @@ -1,19 +1,39 @@ -import GameNode from "../../Nodes/GameNode"; +import AABB from "../../DataTypes/AABB"; +import { Positioned } from "../../DataTypes/Interfaces/Descriptors"; +import Shape from "../../DataTypes/Shape"; import Vec2 from "../../DataTypes/Vec2"; -export default abstract class Collider extends GameNode { - protected size: Vec2; +export default class Collider implements Positioned { + protected shape: Shape; - getSize(): Vec2 { - return this.size; + constructor(shape: Shape){ + this.shape = shape; } - // TODO: Make this accept vector arguments and number arguments - setSize(size: Vec2): void { - this.size = size; + setPosition(position: Vec2): void { + this.shape.setCenter(position); } - abstract isCollidingWith(other: Collider): boolean; + getPosition(): Vec2 { + return this.shape.getCenter(); + } - abstract willCollideWith(other: Collider, thisVel: Vec2, otherVel: Vec2): boolean; + getBoundingRect(): AABB { + return this.shape.getBoundingRect(); + } + + /** + * Sets the collision shape for this collider. + * @param shape + */ + setCollisionShape(shape: Shape): void { + this.shape = shape; + } + + /** + * Returns the collision shape this collider has + */ + getCollisionShape(): Shape { + return this.shape; + } } \ No newline at end of file diff --git a/src/Physics/Colliders/Collisions.ts b/src/Physics/Colliders/Collisions.ts new file mode 100644 index 0000000..e0d8221 --- /dev/null +++ b/src/Physics/Colliders/Collisions.ts @@ -0,0 +1,97 @@ +import Shape from "../../DataTypes/Shape"; +import AABB from "../../DataTypes/AABB"; +import Vec2 from "../../DataTypes/Vec2"; +import Collider from "./Collider"; +import Debug from "../../Debug/Debug"; + +export function getTimeOfCollision(A: Collider, velA: Vec2, B: Collider, velB: Vec2): [Vec2, Vec2, boolean, boolean] { + let shapeA = A.getCollisionShape(); + let shapeB = B.getCollisionShape(); + + if(shapeA instanceof AABB && shapeB instanceof AABB){ + return getTimeOfCollision_AABB_AABB(shapeA, velA, shapeB, velB); + } +} + +// TODO - Make this work with centered points to avoid this initial calculation +function getTimeOfCollision_AABB_AABB(A: AABB, velA: Vec2, B: AABB, velB: Vec2): [Vec2, Vec2, boolean, boolean] { + let posA = A.getCenter().clone(); + let posB = B.getCenter().clone(); + let sizeA = A.getHalfSize(); + let sizeB = B.getHalfSize(); + + 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 - sizeB.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 - sizeB.x) - (posA.x + sizeA.x))/(relVel); + lastContact.x = ((posB.x + sizeB.x) - (posA.x - sizeA.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 - sizeB.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 - sizeB.y) - (posA.y + sizeA.y))/(relVel); + lastContact.y = ((posB.y + sizeB.y) - (posA.y - sizeA.y))/(relVel); + } + } else { + collidingY = true; + } + + return [firstContact, lastContact, collidingX, collidingY]; +} \ No newline at end of file diff --git a/src/Physics/PhysicsManager.ts b/src/Physics/PhysicsManager.ts index bd41691..41a420b 100644 --- a/src/Physics/PhysicsManager.ts +++ b/src/Physics/PhysicsManager.ts @@ -5,12 +5,16 @@ import Debug from "../Debug/Debug"; import MathUtils from "../Utils/MathUtils"; import Tilemap from "../Nodes/Tilemap"; import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; +import AABB from "../DataTypes/AABB"; +import { getTimeOfCollision } from "./Colliders/Collisions"; +import Collider from "./Colliders/Collider"; export default class PhysicsManager { private physicsNodes: Array; private tilemaps: Array; private movements: Array; + private tcols: Array = []; constructor(){ this.physicsNodes = new Array(); @@ -63,14 +67,14 @@ export default class PhysicsManager { */ private collideWithOrthogonalTilemap(node: PhysicsNode, tilemap: OrthogonalTilemap, velocity: Vec2): void { // Get the starting position of the moving node - let startPos = node.getPosition(); + let startPos = node.getCollider().getPosition(); // Get the end position of the moving node let endPos = startPos.clone().add(velocity); - let size = node.getCollider().getSize(); + let size = node.getCollider().getBoundingRect().getHalfSize(); // Get the min and max x and y coordinates of the moving node - let min = new Vec2(Math.min(startPos.x, endPos.x), Math.min(startPos.y, endPos.y)); + let min = new Vec2(Math.min(startPos.x - size.x, endPos.x - size.x), Math.min(startPos.y - size.y, endPos.y - size.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)); // Convert the min/max x/y to the min and max row/col in the tilemap array @@ -79,6 +83,7 @@ export default class PhysicsManager { // Create an empty set of tilemap collisions (We'll handle all of them at the end) let tilemapCollisions = new Array(); + this.tcols = []; let tileSize = tilemap.getTileSize(); Debug.log("tilemapCollision", ""); @@ -88,9 +93,12 @@ export default class PhysicsManager { for(let row = minIndex.y; row <= maxIndex.y; row++){ if(tilemap.isTileCollidable(col, row)){ Debug.log("tilemapCollision", "Colliding with Tile"); - + // Get the position of this tile - let tilePos = new Vec2(col * tileSize.x, row * tileSize.y); + let tilePos = new Vec2(col * tileSize.x + tileSize.x/2, row * tileSize.y + tileSize.y/2); + + // Create a new collider for this tile + let collider = new Collider(new AABB(tilePos, tileSize.scaled(1/2))); // Calculate collision area between the node and the tile let dx = Math.min(startPos.x, tilePos.x) - Math.max(startPos.x + size.x, tilePos.x + size.x); @@ -102,17 +110,21 @@ export default class PhysicsManager { overlap = dx * dy; } - tilemapCollisions.push(new TileCollisionData(tilePos, overlap)); + this.tcols.push(new TileCollisionData(collider, overlap)) + tilemapCollisions.push(new TileCollisionData(collider, overlap)); } } } // Now that we have all collisions, sort by collision area highest to lowest tilemapCollisions = tilemapCollisions.sort((a, b) => a.overlapArea - b.overlapArea); + let areas = ""; + tilemapCollisions.forEach(col => areas += col.overlapArea + ", ") + Debug.log("cols", areas) // Resolve the collisions in order of collision area (i.e. "closest" tiles are collided with first, so we can slide along a surface of tiles) tilemapCollisions.forEach(collision => { - let [firstContact, _, collidingX, collidingY] = this.getTimeOfAABBCollision(startPos, size, velocity, collision.position, tileSize, new Vec2(0, 0)); + let [firstContact, _, collidingX, collidingY] = getTimeOfCollision(node.getCollider(), velocity, collision.collider, Vec2.ZERO); // Handle collision if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ @@ -143,13 +155,7 @@ export default class PhysicsManager { } 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 [firstContact, _, collidingX, collidingY] = this.getTimeOfAABBCollision(posA, sizeA, velA, posB, sizeB, new Vec2(0, 0)); + let [firstContact, _, collidingX, collidingY] = getTimeOfCollision(movingNode.getCollider(), velocity, staticNode.getCollider(), Vec2.ZERO); if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ if(collidingX && collidingY){ @@ -178,88 +184,6 @@ export default class PhysicsManager { } } - /** - * 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). - */ - private getTimeOfAABBCollision(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]; - } - update(deltaT: number): void { for(let node of this.physicsNodes){ if(!node.getLayer().isPaused()){ @@ -309,6 +233,28 @@ export default class PhysicsManager { // Reset movements this.movements = new Array(); } + + render(ctx: CanvasRenderingContext2D): void { + let vpo; + for(let node of this.physicsNodes){ + vpo = node.getViewportOriginWithParallax(); + let pos = node.getPosition().sub(node.getViewportOriginWithParallax()); + let size = (node.getCollider().getCollisionShape()).getHalfSize(); + + ctx.lineWidth = 2; + ctx.strokeStyle = "#FF0000"; + ctx.strokeRect(pos.x - size.x, pos.y-size.y, size.x*2, size.y*2); + } + + for(let node of this.tcols){ + let pos = node.collider.getPosition().sub(vpo); + let size = node.collider.getBoundingRect().getHalfSize(); + + ctx.lineWidth = 2; + ctx.strokeStyle = "#FF0000"; + ctx.strokeRect(pos.x - size.x, pos.y-size.y, size.x*2, size.y*2); + } + } } // Helper classes for internal data @@ -325,10 +271,11 @@ class MovementData { // Collision data objects for tilemaps class TileCollisionData { - position: Vec2; + collider: Collider; overlapArea: number; - constructor(position: Vec2, overlapArea: number){ - this.position = position; + + constructor(collider: Collider, overlapArea: number){ + this.collider = collider; this.overlapArea = overlapArea; } } \ No newline at end of file diff --git a/src/Physics/PhysicsNode.ts b/src/Physics/PhysicsNode.ts index 02cd669..deb2e37 100644 --- a/src/Physics/PhysicsNode.ts +++ b/src/Physics/PhysicsNode.ts @@ -67,7 +67,7 @@ export default abstract class PhysicsNode extends GameNode { this.position.add(velocity); this.collider.getPosition().add(velocity); for(let child of this.children){ - child.getPosition().add(velocity); + child.position.add(velocity); } } diff --git a/src/Physics/StaticBody.ts b/src/Physics/StaticBody.ts index d2b3ab9..95d20e8 100644 --- a/src/Physics/StaticBody.ts +++ b/src/Physics/StaticBody.ts @@ -1,15 +1,15 @@ import PhysicsNode from "./PhysicsNode"; import Vec2 from "../DataTypes/Vec2"; -import AABBCollider from "./Colliders/AABBCollider"; +import Collider from "./Colliders/Collider"; +import AABB from "../DataTypes/AABB"; export default class StaticBody extends PhysicsNode { constructor(position: Vec2, size: Vec2){ super(); this.setPosition(position.x, position.y); - this.collider = new AABBCollider(); - this.collider.setPosition(position.x, position.y); - this.collider.setSize(new Vec2(size.x, size.y)); + let aabb = new AABB(position.clone(), size.scaled(1/2)); + this.collider = new Collider(aabb); this.moving = false; } diff --git a/src/Player.ts b/src/Player.ts index d29001b..a25592d 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,9 +1,10 @@ import PhysicsNode from "./Physics/PhysicsNode"; import Vec2 from "./DataTypes/Vec2"; import Debug from "./Debug/Debug"; -import AABBCollider from "./Physics/Colliders/AABBCollider"; import CanvasNode from "./Nodes/CanvasNode"; import { GameEventType } from "./Events/GameEventType"; +import AABB from "./DataTypes/AABB"; +import Collider from "./Physics/Colliders/Collider"; export default class Player extends PhysicsNode { velocity: Vec2; @@ -19,18 +20,20 @@ export default class Player extends PhysicsNode { this.velocity = new Vec2(0, 0); this.speed = 600; this.size = new Vec2(50, 50); - this.collider = new AABBCollider(); - this.collider.setSize(this.size); this.position = new Vec2(0, 0); if(this.type === "topdown"){ this.position = new Vec2(100, 100); } + + this.collider = new Collider(new AABB(this.position.clone(), this.size.scaled(1/2))); } create(): void {}; + sprite: CanvasNode; setSprite(sprite: CanvasNode): void { - sprite.setPosition(this.position); + this.sprite = sprite; + sprite.position = this.position.clone(); sprite.setSize(this.size); this.children.push(sprite); } @@ -46,7 +49,8 @@ export default class Player extends PhysicsNode { this.move(new Vec2(this.velocity.x * deltaT, this.velocity.y * deltaT)); - Debug.log("player", "Player Pos: " + this.position + ", Player Vel: " + this.velocity); + Debug.log("player", "Pos: " + this.sprite.getPosition() + ", Size: " + this.sprite.getSize()); + Debug.log("playerbound", "Pos: " + this.sprite.getBoundary().getCenter() + ", Size: " + this.sprite.getBoundary().getHalfSize()); } topdown_computeDirection(): Vec2 { diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index 85d2b9c..1ed0aa3 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -29,7 +29,7 @@ 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): Array => { + add = (key: string, scale: Vec2 = new Vec2(1, 1)): Array => { // Get Tilemap Data let tilemapData = this.resourceManager.getTilemap(key); @@ -70,7 +70,7 @@ export default class TilemapFactory { if(layer.type === "tilelayer"){ // Create a new tilemap object for the layer - let tilemap = new constr(tilemapData, layer, tilesets); + let tilemap = new constr(tilemapData, layer, tilesets, scale); tilemap.setId(this.scene.generateId()); tilemap.setScene(this.scene); @@ -107,10 +107,10 @@ export default class TilemapFactory { 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.setPosition((obj.x + size.x/2)*scale.x, (obj.y - size.y/2)*scale.y); sprite.setImageOffset(offset); sprite.setSize(size); - sprite.setScale(new Vec2(4, 4)); + sprite.setScale(new Vec2(scale.x, scale.y)); } } @@ -120,8 +120,8 @@ export default class TilemapFactory { 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)); + sprite.setPosition((obj.x + tile.imagewidth/2)*scale.x, (obj.y - tile.imageheight/2)*scale.y); + sprite.setScale(new Vec2(scale.x, scale.y)); } } } @@ -129,9 +129,9 @@ export default class TilemapFactory { // Now we have sprite. Associate it with our physics object if there is one if(collidable){ let pos = sprite.getPosition().clone(); + let size = sprite.getSize().clone().mult(sprite.getScale()); 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); } diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index ce75d1b..5a5ff06 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -125,6 +125,9 @@ export default class Scene{ // Render visible set visibleSet.forEach(node => node.render(ctx)); + + // Debug render the physicsManager + this.physicsManager.render(ctx); } setRunning(running: boolean): void { diff --git a/src/Scene/SceneManager.ts b/src/Scene/SceneManager.ts index c0c4c39..81e8da6 100644 --- a/src/Scene/SceneManager.ts +++ b/src/Scene/SceneManager.ts @@ -49,7 +49,7 @@ export default class SceneManager { this.resourceManager.unloadAllResources(); - this.viewport.setPosition(0, 0); + this.viewport.setCenter(0, 0); this.addScene(constr); } diff --git a/src/SceneGraph/SceneGraphQuadTree.ts b/src/SceneGraph/SceneGraphQuadTree.ts index 2b6fcb1..74fb552 100644 --- a/src/SceneGraph/SceneGraphQuadTree.ts +++ b/src/SceneGraph/SceneGraphQuadTree.ts @@ -56,14 +56,9 @@ export default class SceneGraphQuadTree extends SceneGraph { } getVisibleSet(): Array { - let visibleSet = new Array(); + let visibleSet = this.qt.queryRegion(this.viewport.getView()); - // TODO - Currently just gets all of them - this.qt.forEach((node: CanvasNode) => { - if(!node.getLayer().isHidden() && this.viewport.includes(node)){ - visibleSet.push(node); - } - }); + visibleSet = visibleSet.filter(node => !node.getLayer().isHidden()); // Sort by depth, then by visible set by y-value visibleSet.sort((a, b) => { diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index f4ef588..6c28955 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -4,11 +4,12 @@ import GameNode from "../Nodes/GameNode"; import CanvasNode from "../Nodes/CanvasNode"; import MathUtils from "../Utils/MathUtils"; import Queue from "../DataTypes/Queue"; +import AABB from "../DataTypes/AABB"; +import Debug from "../Debug/Debug"; export default class Viewport { - private position: Vec2; - private size: Vec2; - private bounds: Vec4; + private view: AABB; + private boundary: AABB; private following: GameNode; /** @@ -22,9 +23,8 @@ export default class Viewport { private smoothingFactor: number; constructor(){ - this.position = new Vec2(0, 0); - this.size = new Vec2(0, 0); - this.bounds = new Vec4(0, 0, 0, 0); + this.view = new AABB(Vec2.ZERO, Vec2.ZERO); + this.boundary = new AABB(Vec2.ZERO, Vec2.ZERO); this.lastPositions = new Queue(); this.smoothingFactor = 10; } @@ -32,8 +32,19 @@ export default class Viewport { /** * Returns the position of the viewport as a Vec2 */ - getPosition(): Vec2 { - return this.position; + getCenter(): Vec2 { + return this.view.getCenter(); + } + + getOrigin(): Vec2 { + return this.view.getCenter().clone().sub(this.view.getHalfSize()) + } + + /** + * Returns the region visible to this viewport + */ + getView(): AABB { + return this.view; } /** @@ -41,7 +52,7 @@ export default class Viewport { * @param vecOrX * @param y */ - setPosition(vecOrX: Vec2 | number, y: number = null): void { + setCenter(vecOrX: Vec2 | number, y: number = null): void { let pos: Vec2; if(vecOrX instanceof Vec2){ pos = vecOrX; @@ -56,8 +67,8 @@ export default class Viewport { /** * Returns the size of the viewport as a Vec2 */ - getSize(): Vec2{ - return this.size; + getHalfSize(): Vec2 { + return this.view.getHalfSize(); } /** @@ -67,9 +78,17 @@ export default class Viewport { */ setSize(vecOrX: Vec2 | number, y: number = null): void { if(vecOrX instanceof Vec2){ - this.size.set(vecOrX.x, vecOrX.y); + this.view.setHalfSize(vecOrX.scaled(1/2)); } else { - this.size.set(vecOrX, y); + this.view.setHalfSize(new Vec2(vecOrX/2, y/2)); + } + } + + setHalfSize(vecOrX: Vec2 | number, y: number = null): void { + if(vecOrX instanceof Vec2){ + this.view.setHalfSize(vecOrX.clone()); + } else { + this.view.setHalfSize(new Vec2(vecOrX, y)); } } @@ -87,19 +106,12 @@ export default class Viewport { * @param node */ 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 * 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; - } - } - - return false; + let center = this.view.getCenter().clone(); + this.view.getCenter().mult(parallax); + let overlaps = this.view.overlaps(node.getBoundary()); + this.view.setCenter(center); + return overlaps; } // TODO: Put some error handling on this for trying to make the bounds too small for the viewport @@ -112,7 +124,12 @@ export default class Viewport { * @param upperY */ setBounds(lowerX: number, lowerY: number, upperX: number, upperY: number): void { - this.bounds = new Vec4(lowerX, lowerY, upperX, upperY); + let hwidth = (upperX - lowerX)/2; + let hheight = (upperY - lowerY)/2; + let x = lowerX + hwidth; + let y = lowerY + hheight; + this.boundary.setCenter(new Vec2(x, y)); + this.boundary.setHalfSize(new Vec2(hwidth, hheight)); } /** @@ -138,11 +155,10 @@ export default class Viewport { pos.scale(1/this.lastPositions.getSize()); // Set this position either to the object or to its bounds - this.position.x = pos.x - this.size.x/2; - this.position.y = pos.y - this.size.y/2; - let [min, max] = this.bounds.split(); - this.position.x = MathUtils.clamp(this.position.x, min.x, max.x - this.size.x); - this.position.y = MathUtils.clamp(this.position.y, min.y, max.y - this.size.y); + pos.x = MathUtils.clamp(pos.x, this.boundary.left + this.view.hw, this.boundary.right - this.view.hw); + pos.y = MathUtils.clamp(pos.y, this.boundary.top + this.view.hh, this.boundary.bottom - this.view.hh); + + this.view.setCenter(pos); } } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index f699249..17e434c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,14 @@ import GameLoop from "./Loop/GameLoop"; import {} from "./index"; +import MainScene from "./MainScene" import QuadTreeScene from "./QuadTreeScene"; function main(){ // Create the game object - let game = new GameLoop({viewportSize: {x: 500, y: 500}}); + let game = new GameLoop({viewportSize: {x: 800, y: 600}}); game.start(); let sm = game.getSceneManager(); - sm.addScene(QuadTreeScene); + sm.addScene(MainScene); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {