converted all code to work with AABBs

This commit is contained in:
Joe Weaver 2020-10-05 15:01:26 -04:00
parent a32066468f
commit 254462993a
26 changed files with 383 additions and 240 deletions

View File

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

View File

@ -123,8 +123,9 @@ export default class QuadTree<T extends Region & Unique> 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<T> {
// A matrix to keep track of our results
@ -212,6 +213,7 @@ export default class QuadTree<T extends Region & Unique> 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);

8
src/DataTypes/Shape.ts Normal file
View File

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

View File

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

View File

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

View File

@ -48,7 +48,6 @@ export default class GameLoop {
constructor(config?: object){
// Typecast the config object to a GameConfig object
let gameConfig = config ? <GameConfig>config : new GameConfig();
console.log(gameConfig)
this.maxFPS = 60;
this.simulationTimestep = Math.floor(1000/this.maxFPS);

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Tileset>;
protected worldSize: Vec2;
protected size: Vec2;
protected tileSize: Vec2;
protected scale: Vec2;
public data: Array<number>;
@ -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<Tileset>) {
constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array<Tileset>, 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 {

View File

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

View File

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

View File

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

View File

@ -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;
abstract willCollideWith(other: Collider, thisVel: Vec2, otherVel: Vec2): boolean;
getPosition(): Vec2 {
return this.shape.getCenter();
}
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;
}
}

View File

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

View File

@ -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<PhysicsNode>;
private tilemaps: Array<Tilemap>;
private movements: Array<MovementData>;
private tcols: Array<TileCollisionData> = [];
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<TileCollisionData>();
this.tcols = [];
let tileSize = tilemap.getTileSize();
Debug.log("tilemapCollision", "");
@ -90,7 +95,10 @@ export default class PhysicsManager {
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 = (<AABB>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;
}
}

View File

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

View File

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

View File

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

View File

@ -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<Layer> => {
add = (key: string, scale: Vec2 = new Vec2(1, 1)): Array<Layer> => {
// 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);
}

View File

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

View File

@ -49,7 +49,7 @@ export default class SceneManager {
this.resourceManager.unloadAllResources();
this.viewport.setPosition(0, 0);
this.viewport.setCenter(0, 0);
this.addScene(constr);
}

View File

@ -56,14 +56,9 @@ export default class SceneGraphQuadTree extends SceneGraph {
}
getVisibleSet(): Array<CanvasNode> {
let visibleSet = new Array<CanvasNode>();
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) => {

View File

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

View File

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