fixed tilemap physics

This commit is contained in:
Joe Weaver 2020-08-20 20:45:48 -04:00
parent 66ced08987
commit 2dd60b5197
14 changed files with 294 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -30,6 +31,7 @@ export default class GameLoop{
readonly GAME_CANVAS: HTMLCanvasElement;
readonly WIDTH: 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();
}

View File

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

View File

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

View File

@ -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<PhysicsNode>;
tilemaps: Array<Tilemap>;
movements: Array<MovementData>;
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, (<StaticBody>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<TileCollisionData>();
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){
@ -174,8 +176,91 @@ 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).
*/
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;
@ -184,3 +269,12 @@ class MovementData{
this.velocity = velocity;
}
}
class TileCollisionData {
position: Vec2;
overlapArea: number;
constructor(position: Vec2, overlapArea: number){
this.position = position;
this.overlapArea = overlapArea;
}
}

View File

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

View File

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

View File

@ -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",