import Vector from '../../math/Vector'; import GroupBehavior from '../GroupBehavior'; import Proximity, { ProximityCallback } from '../Proximity'; import Limiter from '../Limiter'; import Steerable from '../Steerable'; import SteeringAcceleration from '../SteeringAcceleration'; /** * {@code CollisionAvoidance} behavior steers the owner to avoid obstacles lying in its path. An obstacle is any object that can be * approximated by a circle (or sphere, if you are working in 3D). *

* This implementation uses collision prediction working out the closest approach of two agents and determining if their distance * at this point is less than the sum of their bounding radius. For avoiding groups of characters, averaging positions and * velocities do not work well with this approach. Instead, the algorithm needs to search for the character whose closest approach * will occur first and to react to this character only. Once this imminent collision is avoided, the steering behavior can then * react to more distant characters. *

* This algorithm works well with small and/or moving obstacles whose shape can be approximately represented by a center and a * radius. * * @param Type of vector, either 2D or 3D, implementing the {@link Vector} interface * * @author davebaol */ class CollisionAvoidance> extends GroupBehavior implements ProximityCallback { private shortestTime: number; private firstNeighbor: Steerable; private firstMinSeparation: number; private firstDistance: number; private firstRelativePosition: T; private firstRelativeVelocity: T; private relativePosition: T; private relativeVelocity: T; /** * Creates a {@code CollisionAvoidance} behavior for the specified owner and proximity. * @param owner the owner of this behavior * @param proximity the proximity of this behavior. */ public constructor(owner: Steerable, proximity: Proximity) { super(owner, proximity); this.firstRelativePosition = this.newVector(owner); this.firstRelativeVelocity = this.newVector(owner); this.relativeVelocity = this.newVector(owner); } public reportNeighbor(neighbor: Steerable): boolean { // Calculate the time to collision this.relativePosition.copy(neighbor.getPosition()).sub(this.owner.getPosition()); this.relativeVelocity.copy(neighbor.getLinearVelocity()).sub(this.owner.getLinearVelocity()); const relativeSpeed2 = this.relativeVelocity.sqrLen(); // Collision can't happen when the agents have the same linear velocity. // Also, note that timeToCollision would be NaN due to the indeterminate form 0/0 and, // since any comparison involving NaN returns false, it would become the shortestTime, // so defeating the algorithm. if (relativeSpeed2 === 0) return false; const timeToCollision = - this.relativePosition.dot(this.relativeVelocity) / relativeSpeed2; // If timeToCollision is negative, i.e. the owner is already moving away from the the neighbor, // or it's not the most imminent collision then no action needs to be taken. if (timeToCollision <= 0 || timeToCollision >= this.shortestTime) return false; // Check if it is going to be a collision at all const distance = this.relativePosition.len(); const minSeparation = distance - Math.sqrt(relativeSpeed2) * timeToCollision /* shortestTime */; if (minSeparation > this.owner.getBoundingRadius() + neighbor.getBoundingRadius()) return false; // Store most imminent collision data this.shortestTime = timeToCollision; this.firstNeighbor = neighbor; this.firstMinSeparation = minSeparation; this.firstDistance = distance; this.firstRelativePosition.copy(this.relativePosition); this.firstRelativeVelocity.copy(this.relativeVelocity); return true; } // // Setters overridden in order to fix the correct return type for chaining // public setOwner(owner: Steerable): CollisionAvoidance { this.owner = owner; return this; } public setEnabled(enabled: boolean): CollisionAvoidance { this.enabled = enabled; return this; } /** * Sets the limiter of this steering behavior. The given limiter must at least take care of the maximum linear acceleration. * @return this behavior for chaining. */ public setLimiter(limiter: Limiter): CollisionAvoidance { this.limiter = limiter; return this; } protected calculateRealSteering (steering: SteeringAcceleration): SteeringAcceleration { this.shortestTime = Infinity; this.firstNeighbor = null; this.firstMinSeparation = 0; this.firstDistance = 0; this.relativePosition = steering.linear; // Take into consideration each neighbor to find the most imminent collision. const neighborCount = this.proximity.findNeighbors(this); // If we have no target, then return no steering acceleration // // NOTE: You might think that the condition below always evaluates to true since // firstNeighbor has been set to null when entering this method. In fact, we have just // executed findNeighbors(this) that has possibly set firstNeighbor to a non null value // through the method reportNeighbor defined below. if (neighborCount === 0 || this.firstNeighbor == null) return steering.setZero(); // If we're going to hit exactly, or if we're already // colliding, then do the steering based on current position. if (this.firstMinSeparation <= 0 || this.firstDistance < this.owner.getBoundingRadius() + this.firstNeighbor.getBoundingRadius() ) { this.relativePosition.copy(this.firstNeighbor.getPosition()).sub(this.owner.getPosition()); } else { // Otherwise calculate the future relative position this.relativePosition.copy(this.firstRelativePosition).scaleAndAdd(this.firstRelativeVelocity, this.shortestTime); } // Avoid the target // Notice that steerling.linear and relativePosition are the same vector this.relativePosition.nor().scale(-this.getActualLimiter().getMaxLinearAcceleration()); // No angular acceleration steering.angular = 0; // Output the steering return steering; } } export default CollisionAvoidance;