added a working physics system
This commit is contained in:
parent
4b8ebf360d
commit
ea33e71619
|
@ -55,6 +55,9 @@ export interface Physical {
|
||||||
/** The shape of the collider for this physics object. */
|
/** The shape of the collider for this physics object. */
|
||||||
collisionShape: Shape;
|
collisionShape: Shape;
|
||||||
|
|
||||||
|
/** The offset of the collision shape from the center of the node */
|
||||||
|
colliderOffset: Vec2;
|
||||||
|
|
||||||
/** Represents whether this object can move or not. */
|
/** Represents whether this object can move or not. */
|
||||||
isStatic: boolean;
|
isStatic: boolean;
|
||||||
|
|
||||||
|
@ -84,6 +87,8 @@ export interface Physical {
|
||||||
|
|
||||||
isPlayer: boolean;
|
isPlayer: boolean;
|
||||||
|
|
||||||
|
isColliding: boolean;
|
||||||
|
|
||||||
/*---------- FUNCTIONS ----------*/
|
/*---------- FUNCTIONS ----------*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,7 +109,7 @@ export interface Physical {
|
||||||
* @param isCollidable Whether this object will be able to collide with other objects
|
* @param isCollidable Whether this object will be able to collide with other objects
|
||||||
* @param isStatic Whether this object will be static or not
|
* @param isStatic Whether this object will be static or not
|
||||||
*/
|
*/
|
||||||
addPhysics: (collisionShape?: Shape, isCollidable?: boolean, isStatic?: boolean) => void;
|
addPhysics: (collisionShape?: Shape, colliderOffset?: Vec2, isCollidable?: boolean, isStatic?: boolean) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a trigger to this object for a specific group
|
* Adds a trigger to this object for a specific group
|
||||||
|
@ -118,6 +123,11 @@ export interface Physical {
|
||||||
* @param layer The name of the layer
|
* @param layer The name of the layer
|
||||||
*/
|
*/
|
||||||
setPhysicsLayer: (layer: String) => void;
|
setPhysicsLayer: (layer: String) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If used before "move()", it will tell you the velocity of the node after its last movement
|
||||||
|
*/
|
||||||
|
getLastVelocity(): Vec2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Shape from "./Shape";
|
||||||
import Vec2 from "../Vec2";
|
import Vec2 from "../Vec2";
|
||||||
import MathUtils from "../../Utils/MathUtils";
|
import MathUtils from "../../Utils/MathUtils";
|
||||||
import Circle from "./Circle";
|
import Circle from "./Circle";
|
||||||
|
import Debug from "../../Debug/Debug";
|
||||||
|
|
||||||
export default class AABB extends Shape {
|
export default class AABB extends Shape {
|
||||||
|
|
||||||
|
@ -109,25 +110,20 @@ export default class AABB extends Shape {
|
||||||
* @param paddingX Pads the AABB in the x axis
|
* @param paddingX Pads the AABB in the x axis
|
||||||
* @param paddingY Pads the AABB in the y axis
|
* @param paddingY Pads the AABB in the y axis
|
||||||
*/
|
*/
|
||||||
intersectSegment(point: Vec2, direction: Vec2, distance?: number, paddingX?: number, paddingY?: number): Hit {
|
intersectSegment(point: Vec2, delta: Vec2, padding?: Vec2): Hit {
|
||||||
// Scale by the distance if it has been provided
|
let paddingX = padding ? padding.x : 0;
|
||||||
if(distance){
|
let paddingY = padding ? padding.y : 0;
|
||||||
direction = direction.scaled(distance);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _paddingX = paddingX ? paddingX : 0;
|
let scaleX = 1/delta.x;
|
||||||
let _paddingY = paddingY ? paddingY : 0;
|
let scaleY = 1/delta.y;
|
||||||
|
|
||||||
let scaleX = 1/direction.x;
|
|
||||||
let scaleY = 1/direction.y;
|
|
||||||
|
|
||||||
let signX = MathUtils.sign(scaleX);
|
let signX = MathUtils.sign(scaleX);
|
||||||
let signY = MathUtils.sign(scaleY);
|
let signY = MathUtils.sign(scaleY);
|
||||||
|
|
||||||
let tnearx = scaleX*(this.x - signX*(this.hw + _paddingX) - point.x);
|
let tnearx = scaleX*(this.x - signX*(this.hw + paddingX) - point.x);
|
||||||
let tneary = scaleX*(this.y - signY*(this.hh + _paddingY) - point.y);
|
let tneary = scaleY*(this.y - signY*(this.hh + paddingY) - point.y);
|
||||||
let tfarx = scaleY*(this.x + signX*(this.hw + _paddingX) - point.x);
|
let tfarx = scaleX*(this.x + signX*(this.hw + paddingX) - point.x);
|
||||||
let tfary = scaleY*(this.y + signY*(this.hh + _paddingY) - point.y);
|
let tfary = scaleY*(this.y + signY*(this.hh + paddingY) - point.y);
|
||||||
|
|
||||||
if(tnearx > tfary || tneary > tfarx){
|
if(tnearx > tfary || tneary > tfarx){
|
||||||
// We aren't colliding - we clear one axis before intersecting another
|
// We aren't colliding - we clear one axis before intersecting another
|
||||||
|
@ -135,15 +131,29 @@ export default class AABB extends Shape {
|
||||||
}
|
}
|
||||||
|
|
||||||
let tnear = Math.max(tnearx, tneary);
|
let tnear = Math.max(tnearx, tneary);
|
||||||
|
|
||||||
|
// Double check for NaNs
|
||||||
|
if(tnearx !== tnearx){
|
||||||
|
tnear = tneary;
|
||||||
|
} else if (tneary !== tneary){
|
||||||
|
tnear = tnearx;
|
||||||
|
}
|
||||||
|
|
||||||
let tfar = Math.min(tfarx, tfary);
|
let tfar = Math.min(tfarx, tfary);
|
||||||
|
|
||||||
|
if(tnear === -Infinity){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if(tnear >= 1 || tfar <= 0){
|
if(tnear >= 1 || tfar <= 0){
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are colliding
|
// We are colliding
|
||||||
let hit = new Hit();
|
let hit = new Hit();
|
||||||
hit.t = MathUtils.clamp01(tnear);
|
hit.time = MathUtils.clamp01(tnear);
|
||||||
|
hit.nearTimes.x = tnearx;
|
||||||
|
hit.nearTimes.y = tneary;
|
||||||
|
|
||||||
if(tnearx > tneary){
|
if(tnearx > tneary){
|
||||||
// We hit on the left or right size
|
// We hit on the left or right size
|
||||||
|
@ -154,10 +164,10 @@ export default class AABB extends Shape {
|
||||||
hit.normal.y = -signY;
|
hit.normal.y = -signY;
|
||||||
}
|
}
|
||||||
|
|
||||||
hit.delta.x = (1.0 - hit.t) * -direction.x;
|
hit.delta.x = (1.0 - hit.time) * -delta.x;
|
||||||
hit.delta.y = (1.0 - hit.t) * -direction.y;
|
hit.delta.y = (1.0 - hit.time) * -delta.y;
|
||||||
hit.pos.x = point.x + direction.x * hit.t;
|
hit.pos.x = point.x + delta.x * hit.time;
|
||||||
hit.pos.y = point.y + direction.y * hit.t;
|
hit.pos.y = point.y + delta.y * hit.time;
|
||||||
|
|
||||||
return hit;
|
return hit;
|
||||||
}
|
}
|
||||||
|
@ -241,7 +251,8 @@ export default class AABB extends Shape {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Hit {
|
export class Hit {
|
||||||
t: number;
|
time: number;
|
||||||
|
nearTimes: Vec2 = Vec2.ZERO;
|
||||||
pos: Vec2 = Vec2.ZERO;
|
pos: Vec2 = Vec2.ZERO;
|
||||||
delta: Vec2 = Vec2.ZERO;
|
delta: Vec2 = Vec2.ZERO;
|
||||||
normal: Vec2 = Vec2.ZERO;
|
normal: Vec2 = Vec2.ZERO;
|
||||||
|
|
|
@ -94,6 +94,7 @@ export default abstract class CanvasNode extends GameNode implements Region {
|
||||||
|
|
||||||
debugRender(): void {
|
debugRender(): void {
|
||||||
super.debugRender();
|
super.debugRender();
|
||||||
Debug.drawBox(this.relativePosition, this.sizeWithZoom, false, Color.GREEN);
|
let color = this.isColliding ? Color.RED : Color.GREEN;
|
||||||
|
Debug.drawBox(this.relativePosition, this.sizeWithZoom, false, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -31,6 +31,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
||||||
onCeiling: boolean;
|
onCeiling: boolean;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
collisionShape: Shape;
|
collisionShape: Shape;
|
||||||
|
colliderOffset: Vec2;
|
||||||
isStatic: boolean;
|
isStatic: boolean;
|
||||||
isCollidable: boolean;
|
isCollidable: boolean;
|
||||||
isTrigger: boolean;
|
isTrigger: boolean;
|
||||||
|
@ -41,6 +42,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
||||||
collidedWithTilemap: boolean;
|
collidedWithTilemap: boolean;
|
||||||
physicsLayer: number;
|
physicsLayer: number;
|
||||||
isPlayer: boolean;
|
isPlayer: boolean;
|
||||||
|
isColliding: boolean = false;
|
||||||
|
|
||||||
/*---------- ACTOR ----------*/
|
/*---------- ACTOR ----------*/
|
||||||
_ai: AI;
|
_ai: AI;
|
||||||
|
@ -128,7 +130,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
||||||
* @param isCollidable Whether this is collidable or not. True by default.
|
* @param isCollidable Whether this is collidable or not. True by default.
|
||||||
* @param isStatic Whether this is static or not. False by default
|
* @param isStatic Whether this is static or not. False by default
|
||||||
*/
|
*/
|
||||||
addPhysics = (collisionShape?: Shape, isCollidable: boolean = true, isStatic: boolean = false): void => {
|
addPhysics = (collisionShape?: Shape, colliderOffset?: Vec2, isCollidable: boolean = true, isStatic: boolean = false): void => {
|
||||||
this.hasPhysics = true;
|
this.hasPhysics = true;
|
||||||
this.moving = false;
|
this.moving = false;
|
||||||
this.onGround = false;
|
this.onGround = false;
|
||||||
|
@ -154,6 +156,12 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
||||||
throw "No collision shape specified for physics object."
|
throw "No collision shape specified for physics object."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(colliderOffset){
|
||||||
|
this.colliderOffset = colliderOffset;
|
||||||
|
} else {
|
||||||
|
this.colliderOffset = Vec2.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
this.sweptRect = this.collisionShape.getBoundingRect();
|
this.sweptRect = this.collisionShape.getBoundingRect();
|
||||||
this.scene.getPhysicsManager().registerObject(this);
|
this.scene.getPhysicsManager().registerObject(this);
|
||||||
}
|
}
|
||||||
|
@ -171,6 +179,10 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
||||||
this.scene.getPhysicsManager().setLayer(this, layer);
|
this.scene.getPhysicsManager().setLayer(this, layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLastVelocity(): Vec2 {
|
||||||
|
return this._velocity;
|
||||||
|
}
|
||||||
|
|
||||||
/*---------- ACTOR ----------*/
|
/*---------- ACTOR ----------*/
|
||||||
get ai(): AI {
|
get ai(): AI {
|
||||||
return this._ai;
|
return this._ai;
|
||||||
|
@ -251,7 +263,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
||||||
*/
|
*/
|
||||||
protected positionChanged(): void {
|
protected positionChanged(): void {
|
||||||
if(this.hasPhysics){
|
if(this.hasPhysics){
|
||||||
this.collisionShape.center = this.position;
|
this.collisionShape.center = this.position.clone().add(this.colliderOffset);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -260,11 +272,17 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
||||||
}
|
}
|
||||||
|
|
||||||
debugRender(): void {
|
debugRender(): void {
|
||||||
Debug.drawPoint(this.relativePosition, Color.GREEN);
|
let color = this.isColliding ? Color.RED : Color.GREEN;
|
||||||
|
Debug.drawPoint(this.relativePosition, color);
|
||||||
|
|
||||||
// If velocity is not zero, draw a vector for it
|
// If velocity is not zero, draw a vector for it
|
||||||
if(this._velocity && !this._velocity.isZero()){
|
if(this._velocity && !this._velocity.isZero()){
|
||||||
Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), Color.GREEN);
|
Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this has a collider, draw it
|
||||||
|
if(this.isCollidable && this.collisionShape){
|
||||||
|
Debug.drawBox(this.collisionShape.center, this.collisionShape.halfSize, false, Color.RED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class OrthogonalTilemap extends Tilemap {
|
||||||
|
|
||||||
// The size of the tilemap on the canvas
|
// The size of the tilemap on the canvas
|
||||||
this.size.set(this.numCols * this.tileSize.x, this.numRows * this.tileSize.y);
|
this.size.set(this.numCols * this.tileSize.x, this.numRows * this.tileSize.y);
|
||||||
this.position.copy(this.size);
|
this.position.copy(this.size.scaled(0.5));
|
||||||
this.data = layer.data;
|
this.data = layer.data;
|
||||||
this.visible = layer.visible;
|
this.visible = layer.visible;
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,12 @@ export default class BasicPhysicsManager extends PhysicsManager {
|
||||||
resolveCollision(node1: Physical, node2: Physical, firstContact: Vec2, lastContact: Vec2, collidingX: boolean, collidingY: boolean): void {
|
resolveCollision(node1: Physical, node2: Physical, firstContact: Vec2, lastContact: Vec2, collidingX: boolean, collidingY: boolean): void {
|
||||||
// Handle collision
|
// Handle collision
|
||||||
if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){
|
if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){
|
||||||
|
if(node1.isPlayer){
|
||||||
|
node1.isColliding = true;
|
||||||
|
} else if(node2.isPlayer){
|
||||||
|
node2.isColliding = true;
|
||||||
|
}
|
||||||
|
|
||||||
// We are colliding. Check for any triggers
|
// We are colliding. Check for any triggers
|
||||||
let group1 = node1.group;
|
let group1 = node1.group;
|
||||||
let group2 = node2.group;
|
let group2 = node2.group;
|
||||||
|
@ -109,7 +115,7 @@ export default class BasicPhysicsManager extends PhysicsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(collidingX && collidingY){
|
if(collidingX && collidingY){
|
||||||
// If we're already intersecting, freak out I guess? Probably should handle this in some way for if nodes get spawned inside of tiles
|
// If we're already intersecting, resolve the current collision
|
||||||
} else if(node1.isCollidable && node2.isCollidable) {
|
} else if(node1.isCollidable && node2.isCollidable) {
|
||||||
// We aren't already colliding, and both nodes can collide, so this is a new collision.
|
// We aren't already colliding, and both nodes can collide, so this is a new collision.
|
||||||
|
|
||||||
|
@ -264,6 +270,11 @@ export default class BasicPhysicsManager extends PhysicsManager {
|
||||||
node.onCeiling = false;
|
node.onCeiling = false;
|
||||||
node.onWall = false;
|
node.onWall = false;
|
||||||
node.collidedWithTilemap = false;
|
node.collidedWithTilemap = false;
|
||||||
|
node.isColliding = false;
|
||||||
|
|
||||||
|
if(node.isPlayer){
|
||||||
|
Debug.log("pvel", "Player Velocity:", node._velocity.toString());
|
||||||
|
}
|
||||||
|
|
||||||
// Update the swept shapes of each node
|
// Update the swept shapes of each node
|
||||||
if(node.moving){
|
if(node.moving){
|
||||||
|
|
229
src/Physics/TestPhysicsManager.ts
Normal file
229
src/Physics/TestPhysicsManager.ts
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
import GameNode from "../Nodes/GameNode";
|
||||||
|
import { Physical, Updateable } from "../DataTypes/Interfaces/Descriptors";
|
||||||
|
import Tilemap from "../Nodes/Tilemap";
|
||||||
|
import PhysicsManager from "./PhysicsManager";
|
||||||
|
import Vec2 from "../DataTypes/Vec2";
|
||||||
|
import Debug from "../Debug/Debug";
|
||||||
|
import Color from "../Utils/Color";
|
||||||
|
import AABB from "../DataTypes/Shapes/AABB";
|
||||||
|
import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap";
|
||||||
|
|
||||||
|
export default class TestPhysicsManager extends PhysicsManager {
|
||||||
|
|
||||||
|
/** The array of static nodes */
|
||||||
|
protected staticNodes: Array<Physical>;
|
||||||
|
|
||||||
|
/** The array of dynamic nodes */
|
||||||
|
protected dynamicNodes: Array<Physical>;
|
||||||
|
|
||||||
|
/** The array of tilemaps */
|
||||||
|
protected tilemaps: Array<Tilemap>;
|
||||||
|
|
||||||
|
constructor(){
|
||||||
|
super();
|
||||||
|
this.staticNodes = new Array();
|
||||||
|
this.dynamicNodes = new Array();
|
||||||
|
this.tilemaps = new Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new physics object to be updated with the physics system
|
||||||
|
* @param node The node to be added to the physics system
|
||||||
|
*/
|
||||||
|
registerObject(node: GameNode): void {
|
||||||
|
if(node.isStatic){
|
||||||
|
// Static and not collidable
|
||||||
|
this.staticNodes.push(node);
|
||||||
|
} else {
|
||||||
|
// Dynamic and not collidable
|
||||||
|
this.dynamicNodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a tilemap with this physics manager
|
||||||
|
* @param tilemap
|
||||||
|
*/
|
||||||
|
registerTilemap(tilemap: Tilemap): void {
|
||||||
|
this.tilemaps.push(tilemap);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayer(node: GameNode, layer: string): void {
|
||||||
|
node.physicsLayer = this.layerMap.get(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the physics
|
||||||
|
* @param deltaT
|
||||||
|
*/
|
||||||
|
update(deltaT: number): void {
|
||||||
|
/* ALGORITHM:
|
||||||
|
In an effort to keep things simple and working effectively, each dynamic node will resolve its
|
||||||
|
collisions considering the rest of the world as static.
|
||||||
|
|
||||||
|
Collision detecting will happen first. This can be considered a broad phase, but it is not especially
|
||||||
|
efficient, as it does not need to be for this game engine. Every dynamic node is checked against every
|
||||||
|
other node for collision area. If collision area is non-zero (meaning the current node sweeps into another),
|
||||||
|
it is added to a list of hits.
|
||||||
|
|
||||||
|
INITIALIZATION:
|
||||||
|
- Physics constants are reset
|
||||||
|
- Swept shapes are recalculated. If a node isn't moving, it is skipped.
|
||||||
|
|
||||||
|
COLLISION DETECTION:
|
||||||
|
- For a node, collision area will be calculated using the swept AABB of the node against every other AABB in a static state
|
||||||
|
- These collisions will be sorted by area in descending order
|
||||||
|
|
||||||
|
COLLISION RESOLUTION:
|
||||||
|
- For each hit, time of collision is calculated using a swept line through the AABB of the static node expanded
|
||||||
|
with minkowski sums (discretely, but the concept is there)
|
||||||
|
- The collision is resolved based on the near time of the collision (from method of separated axes)
|
||||||
|
- X is resolved by near x, Y by near y.
|
||||||
|
- There is some fudging to allow for sliding along walls of separate colliders. Sorting by area also helps with this.
|
||||||
|
- Corner to corner collisions are resolve to favor x-movement. This is in consideration of platformers, to give
|
||||||
|
the player some help with jumps
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Everything happens with a consistent time. There is a distinct before and after for each resolution.
|
||||||
|
- No back-tracking needs to be done. Once we resolve a node, it is definitively resolved.
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- Nodes that are processed early have movement priority over other nodes. This can lead to some undesirable interactions.
|
||||||
|
*/
|
||||||
|
for(let node of this.dynamicNodes){
|
||||||
|
/*---------- INITIALIZATION PHASE ----------*/
|
||||||
|
// Clear frame dependent boolean values for each node
|
||||||
|
node.onGround = false;
|
||||||
|
node.onCeiling = false;
|
||||||
|
node.onWall = false;
|
||||||
|
node.collidedWithTilemap = false;
|
||||||
|
|
||||||
|
// Update the swept shapes of each node
|
||||||
|
if(node.moving){
|
||||||
|
// If moving, reflect that in the swept shape
|
||||||
|
node.sweptRect.sweep(node._velocity, node.collisionShape.center, node.collisionShape.halfSize);
|
||||||
|
} else {
|
||||||
|
// If our node isn't moving, don't bother to check it (other nodes will detect if they run into it)
|
||||||
|
node._velocity.zero();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*---------- DETECTION PHASE ----------*/
|
||||||
|
// Gather a set of overlaps
|
||||||
|
let overlaps = new Array<AreaCollision>();
|
||||||
|
|
||||||
|
// First, check this node against every static node (order doesn't actually matter here, since we sort anyways)
|
||||||
|
for(let other of this.staticNodes){
|
||||||
|
let collider = other.collisionShape.getBoundingRect();
|
||||||
|
let area = node.sweptRect.overlapArea(collider);
|
||||||
|
if(area > 0){
|
||||||
|
// We had a collision
|
||||||
|
overlaps.push(new AreaCollision(area, collider));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, check it against every dynamic node
|
||||||
|
for(let other of this.dynamicNodes){
|
||||||
|
let collider = other.collisionShape.getBoundingRect();
|
||||||
|
let area = node.sweptRect.overlapArea(collider);
|
||||||
|
if(area > 0){
|
||||||
|
// We had a collision
|
||||||
|
overlaps.push(new AreaCollision(area, collider));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lastly, gather a set of AABBs from the tilemap.
|
||||||
|
// This step involves the most extra work, so it is abstracted into a method
|
||||||
|
for(let tilemap of this.tilemaps){
|
||||||
|
if(tilemap instanceof OrthogonalTilemap){
|
||||||
|
this.collideWithOrthogonalTilemap(node, tilemap, overlaps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the overlaps by area
|
||||||
|
overlaps = overlaps.sort((a, b) => b.area - a.area);
|
||||||
|
|
||||||
|
|
||||||
|
/*---------- RESOLUTION PHASE ----------*/
|
||||||
|
// For every overlap, determine if we need to collide with it and when
|
||||||
|
for(let other of overlaps){
|
||||||
|
// Do a swept line test on the static AABB with this AABB size as padding (this is basically using a minkowski sum!)
|
||||||
|
// Start the sweep at the position of this node with a delta of _velocity
|
||||||
|
const point = node.collisionShape.center;
|
||||||
|
const delta = node._velocity;
|
||||||
|
const padding = node.collisionShape.halfSize;
|
||||||
|
const otherAABB = other.collider;
|
||||||
|
|
||||||
|
|
||||||
|
const hit = otherAABB.intersectSegment(node.collisionShape.center, node._velocity, node.collisionShape.halfSize);
|
||||||
|
|
||||||
|
if(hit !== null){
|
||||||
|
// We got a hit, resolve with the time inside of the hit
|
||||||
|
let tnearx = hit.nearTimes.x;
|
||||||
|
let tneary = hit.nearTimes.y;
|
||||||
|
|
||||||
|
// Allow edge clipping (edge overlaps don't count, only area overlaps)
|
||||||
|
// Importantly don't allow both cases to be true. Then we clip through corners. Favor x to help players land jumps
|
||||||
|
if(tnearx < 1.0 && (point.y === otherAABB.top - padding.y || point.y === otherAABB.bottom + padding.y) && delta.x !== 0) {
|
||||||
|
tnearx = 1.0;
|
||||||
|
} else if(tneary < 1.0 && (point.x === otherAABB.left - padding.x || point.x === otherAABB.right + padding.x) && delta.y !== 0) {
|
||||||
|
tneary = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(hit.nearTimes.x >= 0 && hit.nearTimes.x < 1){
|
||||||
|
node._velocity.x = node._velocity.x * tnearx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hit.nearTimes.y >= 0 && hit.nearTimes.y < 1){
|
||||||
|
node._velocity.y = node._velocity.y * tneary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the collision with the node, and move it
|
||||||
|
node.finishMove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collideWithOrthogonalTilemap(node: Physical, tilemap: OrthogonalTilemap, overlaps: Array<AreaCollision>): void {
|
||||||
|
// Get the min and max x and y coordinates of the moving node
|
||||||
|
let min = new Vec2(node.sweptRect.left, node.sweptRect.top);
|
||||||
|
let max = new Vec2(node.sweptRect.right, node.sweptRect.bottom);
|
||||||
|
|
||||||
|
// Convert the min/max x/y to the min and max row/col in the tilemap array
|
||||||
|
let minIndex = tilemap.getColRowAt(min);
|
||||||
|
let maxIndex = tilemap.getColRowAt(max);
|
||||||
|
|
||||||
|
let tileSize = tilemap.getTileSize();
|
||||||
|
|
||||||
|
// Loop over all possible tiles (which isn't many in the scope of the velocity per frame)
|
||||||
|
for(let col = minIndex.x; col <= maxIndex.x; col++){
|
||||||
|
for(let row = minIndex.y; row <= maxIndex.y; row++){
|
||||||
|
if(tilemap.isTileCollidable(col, row)){
|
||||||
|
// Get the position of this tile
|
||||||
|
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 AABB(tilePos, tileSize.scaled(1/2));
|
||||||
|
|
||||||
|
// Calculate collision area between the node and the tile
|
||||||
|
let area = node.sweptRect.overlapArea(collider);
|
||||||
|
if(area > 0){
|
||||||
|
// We had a collision
|
||||||
|
overlaps.push(new AreaCollision(area, collider));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AreaCollision {
|
||||||
|
area: number;
|
||||||
|
collider: AABB;
|
||||||
|
constructor(area: number, collider: AABB){
|
||||||
|
this.area = area;
|
||||||
|
this.collider = collider;
|
||||||
|
}
|
||||||
|
}
|
|
@ -187,7 +187,7 @@ export default class TilemapFactory {
|
||||||
|
|
||||||
// Now we have sprite. Associate it with our physics object if there is one
|
// Now we have sprite. Associate it with our physics object if there is one
|
||||||
if(hasPhysics){
|
if(hasPhysics){
|
||||||
sprite.addPhysics(sprite.boundary.clone(), isCollidable, isStatic);
|
sprite.addPhysics(sprite.boundary.clone(), Vec2.ZERO, isCollidable, isStatic);
|
||||||
sprite.group = group;
|
sprite.group = group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +1,69 @@
|
||||||
|
import AABB from "../../DataTypes/Shapes/AABB";
|
||||||
|
import { TiledTilemapData } from "../../DataTypes/Tilesets/TiledData";
|
||||||
import Vec2 from "../../DataTypes/Vec2";
|
import Vec2 from "../../DataTypes/Vec2";
|
||||||
|
import Debug from "../../Debug/Debug";
|
||||||
import InputHandler from "../../Input/InputHandler";
|
import InputHandler from "../../Input/InputHandler";
|
||||||
import InputReceiver from "../../Input/InputReceiver";
|
import InputReceiver from "../../Input/InputReceiver";
|
||||||
|
import CanvasNode from "../../Nodes/CanvasNode";
|
||||||
import { GraphicType } from "../../Nodes/Graphics/GraphicTypes";
|
import { GraphicType } from "../../Nodes/Graphics/GraphicTypes";
|
||||||
import BasicPhysicsManager from "../../Physics/BasicPhysicsManager";
|
import TestPhysicsManager from "../../Physics/TestPhysicsManager";
|
||||||
import Scene from "../../Scene/Scene";
|
import Scene from "../../Scene/Scene";
|
||||||
import Color from "../../Utils/Color";
|
import Color from "../../Utils/Color";
|
||||||
|
|
||||||
export default class TestScene extends Scene {
|
export default class TestScene extends Scene {
|
||||||
|
|
||||||
|
loadScene(){
|
||||||
|
this.load.tilemap("test", "assets/tilemaps/PhysicsTest.json");
|
||||||
|
}
|
||||||
|
|
||||||
startScene(){
|
startScene(){
|
||||||
// Opt into a custom physics manager
|
// Opt into a custom physics manager
|
||||||
this.physicsManager = new BasicPhysicsManager(this.sceneOptions.physics);
|
this.physicsManager = new TestPhysicsManager();
|
||||||
|
|
||||||
|
let tilemap = <CanvasNode>this.add.tilemap("test")[0].getItems()[0];
|
||||||
|
|
||||||
|
let layer = this.getLayer("MovingObject");
|
||||||
|
layer.getItems().forEach(item => {
|
||||||
|
let timer = 0;
|
||||||
|
let dir = new Vec2(-1, 0);
|
||||||
|
item.update = (deltaT: number) => {
|
||||||
|
if(timer > 2){
|
||||||
|
timer = 0;
|
||||||
|
dir.scale(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.move(dir.scaled(100*deltaT));
|
||||||
|
|
||||||
|
timer += deltaT;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.addLayer("main");
|
this.addLayer("main");
|
||||||
|
|
||||||
let player = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(100, 100), size: new Vec2(100, 100)});
|
let player = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(50, 100), size: new Vec2(45, 45)});
|
||||||
player.addPhysics();
|
player.color = Color.ORANGE;
|
||||||
|
player.addPhysics(new AABB(new Vec2(0, 0), new Vec2(15, 15)), new Vec2(0, 7.5));
|
||||||
|
|
||||||
player.update = (deltaT: number) => {
|
player.update = (deltaT: number) => {
|
||||||
const input = InputReceiver.getInstance()
|
const input = InputReceiver.getInstance()
|
||||||
|
|
||||||
let xDir = (input.isPressed("a") ? -1 : 0) + (input.isPressed("d") ? 1 : 0);
|
let xDir = (input.isPressed("a") ? -1 : 0) + (input.isPressed("d") ? 1 : 0);
|
||||||
let yDir = (input.isPressed("w") ? -1 : 0) + (input.isPressed("s") ? 1 : 0);
|
let yDir = input.isJustPressed("space") ? -1 : 0;
|
||||||
|
|
||||||
let dir = new Vec2(xDir, yDir);
|
let dir = new Vec2(xDir * 300 * deltaT, yDir*1000 * deltaT);
|
||||||
dir.normalize();
|
|
||||||
|
// Gravity
|
||||||
|
if(dir.y === 0){
|
||||||
|
dir.y = player.getLastVelocity().y + 50 * deltaT;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.log("pvel", player.getLastVelocity());
|
||||||
|
|
||||||
if(!dir.isZero()){
|
if(!dir.isZero()){
|
||||||
player.move(dir.scale(deltaT * 300));
|
player.move(dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let block = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(300, 500), size: new Vec2(100, 100)});
|
player.isPlayer = true;
|
||||||
block.color = Color.CYAN;
|
|
||||||
block.addPhysics(block.boundary, true, true);
|
|
||||||
|
|
||||||
let movingBlock = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(500, 200), size: new Vec2(100, 100)});
|
|
||||||
movingBlock.color = Color.CYAN;
|
|
||||||
movingBlock.addPhysics();
|
|
||||||
|
|
||||||
let timer = 0;
|
|
||||||
let dir = new Vec2(1, 0);
|
|
||||||
movingBlock.update = (deltaT: number) => {
|
|
||||||
if(timer > 0.5){
|
|
||||||
timer = 0;
|
|
||||||
dir.scale(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
movingBlock.move(dir.scaled(200*deltaT));
|
|
||||||
|
|
||||||
timer += deltaT;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,7 +15,7 @@ function main(){
|
||||||
game.start();
|
game.start();
|
||||||
|
|
||||||
let sm = game.getSceneManager();
|
let sm = game.getSceneManager();
|
||||||
sm.addScene(MainMenu, {});
|
sm.addScene(TestScene, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {
|
CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user