218 lines
7.9 KiB
TypeScript
218 lines
7.9 KiB
TypeScript
import GameNode from "../Nodes/GameNode";
|
|
import Physical from "../DataTypes/Interfaces/Physical";
|
|
import Tilemap from "../Nodes/Tilemap";
|
|
import PhysicsManager from "./PhysicsManager";
|
|
import Vec2 from "../DataTypes/Vec2";
|
|
import AABB from "../DataTypes/Shapes/AABB";
|
|
import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap";
|
|
import AreaCollision from "../DataTypes/Physics/AreaCollision";
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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();
|
|
}
|
|
|
|
// @override
|
|
registerObject(node: GameNode): void {
|
|
if(node.isStatic){
|
|
// Static and not collidable
|
|
this.staticNodes.push(node);
|
|
} else {
|
|
// Dynamic and not collidable
|
|
this.dynamicNodes.push(node);
|
|
}
|
|
}
|
|
|
|
// @override
|
|
registerTilemap(tilemap: Tilemap): void {
|
|
this.tilemaps.push(tilemap);
|
|
}
|
|
|
|
// @override
|
|
setLayer(node: GameNode, layer: string): void {
|
|
node.physicsLayer = this.layerMap.get(layer);
|
|
}
|
|
|
|
// @override
|
|
update(deltaT: number): void {
|
|
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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles a collision between this node and an orthogonal tilemap
|
|
* @param node The node
|
|
* @param tilemap The tilemap the node may be colliding with
|
|
* @param overlaps The list of overlaps
|
|
*/
|
|
protected 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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |