import { SteeringBehavior } from '../SteeringBehavior.js';
import { Vector3 } from '../../math/Vector3.js';
import { BoundingSphere } from '../../math/BoundingSphere.js';
import { Matrix4 } from '../../math/Matrix4.js';
import { Ray } from '../../math/Ray.js';
const inverse = new Matrix4();
const localPositionOfObstacle = new Vector3();
const localPositionOfClosestObstacle = new Vector3();
const intersectionPoint = new Vector3();
const boundingSphere = new BoundingSphere();
const ray = new Ray( new Vector3( 0, 0, 0 ), new Vector3( 0, 0, 1 ) );
/**
* This steering behavior produces a force so a vehicle avoids obstacles lying in its path.
*
* @author {@link https://github.com/Mugen87|Mugen87}
* @author {@link https://github.com/robp94|robp94}
* @augments SteeringBehavior
*/
class ObstacleAvoidanceBehavior extends SteeringBehavior {
/**
* Constructs a new obstacle avoidance behavior.
*
* @param {Array<GameEntity>} obstacles - An Array with obstacle of type {@link GameEntity}.
*/
constructor( obstacles = new Array() ) {
super();
/**
* An Array with obstacle of type {@link GameEntity}.
* @type {Array<GameEntity>}
*/
this.obstacles = obstacles;
/**
* This factor determines how much the vehicle decelerates if an intersection occurs.
* @type {Number}
* @default 0.2
*/
this.brakingWeight = 0.2;
/**
* Minimum length of the detection box used for intersection tests.
* @type {Number}
* @default 4
*/
this.dBoxMinLength = 4; //
}
/**
* Calculates the steering force for a single simulation step.
*
* @param {Vehicle} vehicle - The game entity the force is produced for.
* @param {Vector3} force - The force/result vector.
* @param {Number} delta - The time delta.
* @return {Vector3} The force/result vector.
*/
calculate( vehicle, force /*, delta */ ) {
const obstacles = this.obstacles;
// this will keep track of the closest intersecting obstacle
let closestObstacle = null;
// this will be used to track the distance to the closest obstacle
let distanceToClosestObstacle = Infinity;
// the detection box length is proportional to the agent's velocity
const dBoxLength = this.dBoxMinLength + ( vehicle.getSpeed() / vehicle.maxSpeed ) * this.dBoxMinLength;
vehicle.worldMatrix.getInverse( inverse );
for ( let i = 0, l = obstacles.length; i < l; i ++ ) {
const obstacle = obstacles[ i ];
if ( obstacle === vehicle ) continue;
// calculate this obstacle's position in local space of the vehicle
localPositionOfObstacle.copy( obstacle.position ).applyMatrix4( inverse );
// if the local position has a positive z value then it must lay behind the agent.
// besides the absolute z value must be smaller than the length of the detection box
if ( localPositionOfObstacle.z > 0 && Math.abs( localPositionOfObstacle.z ) < dBoxLength ) {
// if the distance from the x axis to the object's position is less
// than its radius + half the width of the detection box then there is a potential intersection
const expandedRadius = obstacle.boundingRadius + vehicle.boundingRadius;
if ( Math.abs( localPositionOfObstacle.x ) < expandedRadius ) {
// do intersection test in local space of the vehicle
boundingSphere.center.copy( localPositionOfObstacle );
boundingSphere.radius = expandedRadius;
ray.intersectBoundingSphere( boundingSphere, intersectionPoint );
// compare distances
if ( intersectionPoint.z < distanceToClosestObstacle ) {
// save new minimum distance
distanceToClosestObstacle = intersectionPoint.z;
// save closest obstacle
closestObstacle = obstacle;
// save local position for force calculation
localPositionOfClosestObstacle.copy( localPositionOfObstacle );
}
}
}
}
// if we have found an intersecting obstacle, calculate a steering force away from it
if ( closestObstacle !== null ) {
// the closer the agent is to an object, the stronger the steering force should be
const multiplier = 1 + ( ( dBoxLength - localPositionOfClosestObstacle.z ) / dBoxLength );
// calculate the lateral force
force.x = ( closestObstacle.boundingRadius - localPositionOfClosestObstacle.x ) * multiplier;
// apply a braking force proportional to the obstacles distance from the vehicle
force.z = ( closestObstacle.boundingRadius - localPositionOfClosestObstacle.z ) * this.brakingWeight;
// finally, convert the steering vector from local to world space (just apply the rotation)
force.applyRotation( vehicle.rotation );
}
return force;
}
/**
* Transforms this instance into a JSON object.
*
* @return {Object} The JSON object.
*/
toJSON() {
const json = super.toJSON();
json.obstacles = new Array();
json.brakingWeight = this.brakingWeight;
json.dBoxMinLength = this.dBoxMinLength;
// obstacles
for ( let i = 0, l = this.obstacles.length; i < l; i ++ ) {
json.obstacles.push( this.obstacles[ i ].uuid );
}
return json;
}
/**
* Restores this instance from the given JSON object.
*
* @param {Object} json - The JSON object.
* @return {ObstacleAvoidanceBehavior} A reference to this behavior.
*/
fromJSON( json ) {
super.fromJSON( json );
this.obstacles = json.obstacles;
this.brakingWeight = json.brakingWeight;
this.dBoxMinLength = json.dBoxMinLength;
return this;
}
/**
* Restores UUIDs with references to GameEntity objects.
*
* @param {Map<String,GameEntity>} entities - Maps game entities to UUIDs.
* @return {ObstacleAvoidanceBehavior} A reference to this behavior.
*/
resolveReferences( entities ) {
const obstacles = this.obstacles;
for ( let i = 0, l = obstacles.length; i < l; i ++ ) {
obstacles[ i ] = entities.get( obstacles[ i ] );
}
}
}
export { ObstacleAvoidanceBehavior };