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