using UnityEngine; using System.Collections.Generic; namespace Pathfinding.RVO.Sampled { using Pathfinding; using Pathfinding.RVO; using Pathfinding.Util; /// <summary> /// Internal agent for the RVO system. /// Usually you will interface with the IAgent interface instead. /// /// See: IAgent /// </summary> public class Agent : IAgent { //Current values for double buffer calculation internal float radius, height, desiredSpeed, maxSpeed, agentTimeHorizon, obstacleTimeHorizon; internal bool locked = false; RVOLayer layer, collidesWith; int maxNeighbours; internal Vector2 position; float elevationCoordinate; Vector2 currentVelocity; /// <summary>Desired target point - position</summary> Vector2 desiredTargetPointInVelocitySpace; Vector2 desiredVelocity; Vector2 nextTargetPoint; float nextDesiredSpeed; float nextMaxSpeed; Vector2 collisionNormal; bool manuallyControlled; bool debugDraw; #region IAgent Properties /// <summary>\copydoc Pathfinding::RVO::IAgent::Position</summary> public Vector2 Position { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::ElevationCoordinate</summary> public float ElevationCoordinate { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::CalculatedTargetPoint</summary> public Vector2 CalculatedTargetPoint { get; private set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::CalculatedSpeed</summary> public float CalculatedSpeed { get; private set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::Locked</summary> public bool Locked { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::Radius</summary> public float Radius { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::Height</summary> public float Height { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::AgentTimeHorizon</summary> public float AgentTimeHorizon { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::ObstacleTimeHorizon</summary> public float ObstacleTimeHorizon { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::MaxNeighbours</summary> public int MaxNeighbours { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::NeighbourCount</summary> public int NeighbourCount { get; private set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::Layer</summary> public RVOLayer Layer { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::CollidesWith</summary> public RVOLayer CollidesWith { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::DebugDraw</summary> public bool DebugDraw { get { return debugDraw; } set { debugDraw = value && simulator != null && !simulator.Multithreading; } } /// <summary>\copydoc Pathfinding::RVO::IAgent::Priority</summary> public float Priority { get; set; } /// <summary>\copydoc Pathfinding::RVO::IAgent::PreCalculationCallback</summary> public System.Action PreCalculationCallback { private get; set; } #endregion #region IAgent Methods /// <summary>\copydoc Pathfinding::RVO::IAgent::SetTarget</summary> public void SetTarget (Vector2 targetPoint, float desiredSpeed, float maxSpeed) { maxSpeed = System.Math.Max(maxSpeed, 0); desiredSpeed = System.Math.Min(System.Math.Max(desiredSpeed, 0), maxSpeed); nextTargetPoint = targetPoint; nextDesiredSpeed = desiredSpeed; nextMaxSpeed = maxSpeed; } /// <summary>\copydoc Pathfinding::RVO::IAgent::SetCollisionNormal</summary> public void SetCollisionNormal (Vector2 normal) { collisionNormal = normal; } /// <summary>\copydoc Pathfinding::RVO::IAgent::ForceSetVelocity</summary> public void ForceSetVelocity (Vector2 velocity) { // A bit hacky, but it is approximately correct // assuming the agent does not move significantly nextTargetPoint = CalculatedTargetPoint = position + velocity * 1000; nextDesiredSpeed = CalculatedSpeed = velocity.magnitude; manuallyControlled = true; } #endregion /// <summary>Used internally for a linked list</summary> internal Agent next; float calculatedSpeed; Vector2 calculatedTargetPoint; /// <summary> /// Simulator which handles this agent. /// Used by this script as a reference and to prevent /// adding this agent to multiple simulations. /// </summary> internal Simulator simulator; List<Agent> neighbours = new List<Agent>(); List<float> neighbourDists = new List<float>(); List<ObstacleVertex> obstaclesBuffered = new List<ObstacleVertex>(); List<ObstacleVertex> obstacles = new List<ObstacleVertex>(); const float DesiredVelocityWeight = 0.1f; /// <summary>Extra weight that walls will have</summary> const float WallWeight = 5; public List<ObstacleVertex> NeighbourObstacles { get { return null; } } public Agent (Vector2 pos, float elevationCoordinate) { AgentTimeHorizon = 2; ObstacleTimeHorizon = 2; Height = 5; Radius = 5; MaxNeighbours = 10; Locked = false; Position = pos; ElevationCoordinate = elevationCoordinate; Layer = RVOLayer.DefaultAgent; CollidesWith = (RVOLayer)(-1); Priority = 0.5f; CalculatedTargetPoint = pos; CalculatedSpeed = 0; SetTarget(pos, 0, 0); } /// <summary> /// Reads public properties and stores them in internal fields. /// This is required because multithreading is used and if another script /// updated the fields at the same time as this class used them in another thread /// weird things could happen. /// /// Will also set CalculatedTargetPoint and CalculatedSpeed to the result /// which was last calculated. /// </summary> public void BufferSwitch () { // <== Read public properties radius = Radius; height = Height; maxSpeed = nextMaxSpeed; desiredSpeed = nextDesiredSpeed; agentTimeHorizon = AgentTimeHorizon; obstacleTimeHorizon = ObstacleTimeHorizon; maxNeighbours = MaxNeighbours; // Manually controlled overrides the agent being locked // (if one for some reason uses them at the same time) locked = Locked && !manuallyControlled; position = Position; elevationCoordinate = ElevationCoordinate; collidesWith = CollidesWith; layer = Layer; if (locked) { // Locked agents do not move at all desiredTargetPointInVelocitySpace = position; desiredVelocity = currentVelocity = Vector2.zero; } else { desiredTargetPointInVelocitySpace = nextTargetPoint - position; // Estimate our current velocity // This is necessary because other agents need to know // how this agent is moving to be able to avoid it currentVelocity = (CalculatedTargetPoint - position).normalized * CalculatedSpeed; // Calculate the desired velocity from the point we want to reach desiredVelocity = desiredTargetPointInVelocitySpace.normalized*desiredSpeed; if (collisionNormal != Vector2.zero) { collisionNormal.Normalize(); var dot = Vector2.Dot(currentVelocity, collisionNormal); // Check if the velocity is going into the wall if (dot < 0) { // If so: remove that component from the velocity currentVelocity -= collisionNormal * dot; } // Clear the normal collisionNormal = Vector2.zero; } } } public void PreCalculation () { if (PreCalculationCallback != null) { PreCalculationCallback(); } } public void PostCalculation () { // ==> Set public properties if (!manuallyControlled) { CalculatedTargetPoint = calculatedTargetPoint; CalculatedSpeed = calculatedSpeed; } List<ObstacleVertex> tmp = obstaclesBuffered; obstaclesBuffered = obstacles; obstacles = tmp; manuallyControlled = false; } /// <summary>Populate the neighbours and neighbourDists lists with the closest agents to this agent</summary> public void CalculateNeighbours () { neighbours.Clear(); neighbourDists.Clear(); if (MaxNeighbours > 0 && !locked) simulator.Quadtree.Query(position, maxSpeed, agentTimeHorizon, radius, this); NeighbourCount = neighbours.Count; } /// <summary>Square a number</summary> static float Sqr (float x) { return x*x; } /// <summary> /// Used by the Quadtree. /// See: CalculateNeighbours /// </summary> internal float InsertAgentNeighbour (Agent agent, float rangeSq) { // Check if this agent collides with the other agent if (this == agent || (agent.layer & collidesWith) == 0) return rangeSq; // 2D distance float dist = (agent.position - position).sqrMagnitude; if (dist < rangeSq) { if (neighbours.Count < maxNeighbours) { neighbours.Add(null); neighbourDists.Add(float.PositiveInfinity); } // Insert the agent into the ordered list of neighbours int i = neighbours.Count-1; if (dist < neighbourDists[i]) { while (i != 0 && dist < neighbourDists[i-1]) { neighbours[i] = neighbours[i-1]; neighbourDists[i] = neighbourDists[i-1]; i--; } neighbours[i] = agent; neighbourDists[i] = dist; } if (neighbours.Count == maxNeighbours) { rangeSq = neighbourDists[neighbourDists.Count-1]; } } return rangeSq; } /// <summary>(x, 0, y)</summary> static Vector3 FromXZ (Vector2 p) { return new Vector3(p.x, 0, p.y); } /// <summary>(x, z)</summary> static Vector2 ToXZ (Vector3 p) { return new Vector2(p.x, p.z); } /// <summary> /// Converts a 3D vector to a 2D vector in the movement plane. /// If movementPlane is XZ it will be projected onto the XZ plane /// and the elevation coordinate will be the Y coordinate /// otherwise it will be projected onto the XY plane and elevation /// will be the Z coordinate. /// </summary> Vector2 To2D (Vector3 p, out float elevation) { if (simulator.movementPlane == MovementPlane.XY) { elevation = -p.z; return new Vector2(p.x, p.y); } else { elevation = p.y; return new Vector2(p.x, p.z); } } static void DrawVO (Vector2 circleCenter, float radius, Vector2 origin) { float alpha = Mathf.Atan2((origin - circleCenter).y, (origin - circleCenter).x); float gamma = radius/(origin-circleCenter).magnitude; float delta = gamma <= 1.0f ? Mathf.Abs(Mathf.Acos(gamma)) : 0; Draw.Debug.CircleXZ(FromXZ(circleCenter), radius, Color.black, alpha-delta, alpha+delta); Vector2 p1 = new Vector2(Mathf.Cos(alpha-delta), Mathf.Sin(alpha-delta)) * radius; Vector2 p2 = new Vector2(Mathf.Cos(alpha+delta), Mathf.Sin(alpha+delta)) * radius; Vector2 p1t = -new Vector2(-p1.y, p1.x); Vector2 p2t = new Vector2(-p2.y, p2.x); p1 += circleCenter; p2 += circleCenter; Debug.DrawRay(FromXZ(p1), FromXZ(p1t).normalized*100, Color.black); Debug.DrawRay(FromXZ(p2), FromXZ(p2t).normalized*100, Color.black); } /// <summary> /// Velocity Obstacle. /// This is a struct to avoid too many allocations. /// /// See: https://en.wikipedia.org/wiki/Velocity_obstacle /// </summary> internal struct VO { Vector2 line1, line2, dir1, dir2; Vector2 cutoffLine, cutoffDir; Vector2 circleCenter; bool colliding; float radius; float weightFactor; float weightBonus; Vector2 segmentStart, segmentEnd; bool segment; /// <summary>Creates a VO for avoiding another agent.</summary> /// <param name="center">The position of the other agent relative to this agent.</param> /// <param name="offset">Offset of the velocity obstacle. For example to account for the agents' relative velocities.</param> /// <param name="radius">Combined radius of the two agents (radius1 + radius2).</param> /// <param name="inverseDt">1 divided by the local avoidance time horizon (e.g avoid agents that we will hit within the next 2 seconds).</param> /// <param name="inverseDeltaTime">1 divided by the time step length.</param> public VO (Vector2 center, Vector2 offset, float radius, float inverseDt, float inverseDeltaTime) { // Adjusted so that a parameter weightFactor of 1 will be the default ("natural") weight factor this.weightFactor = 1; weightBonus = 0; //this.radius = radius; Vector2 globalCenter; circleCenter = center*inverseDt + offset; this.weightFactor = 4*Mathf.Exp(-Sqr(center.sqrMagnitude/(radius*radius))) + 1; // Collision? if (center.magnitude < radius) { colliding = true; // 0.001 is there to make sure lin1.magnitude is not so small that the normalization // below will return Vector2.zero as that will make the VO invalid and it will be ignored. line1 = center.normalized * (center.magnitude - radius - 0.001f) * 0.3f * inverseDeltaTime; dir1 = new Vector2(line1.y, -line1.x).normalized; line1 += offset; cutoffDir = Vector2.zero; cutoffLine = Vector2.zero; dir2 = Vector2.zero; line2 = Vector2.zero; this.radius = 0; } else { colliding = false; center *= inverseDt; radius *= inverseDt; globalCenter = center+offset; // 0.001 is there to make sure cutoffDistance is not so small that the normalization // below will return Vector2.zero as that will make the VO invalid and it will be ignored. var cutoffDistance = center.magnitude - radius + 0.001f; cutoffLine = center.normalized * cutoffDistance; cutoffDir = new Vector2(-cutoffLine.y, cutoffLine.x).normalized; cutoffLine += offset; float alpha = Mathf.Atan2(-center.y, -center.x); float delta = Mathf.Abs(Mathf.Acos(radius/center.magnitude)); this.radius = radius; // Bounding Lines // Point on circle line1 = new Vector2(Mathf.Cos(alpha+delta), Mathf.Sin(alpha+delta)); // Vector tangent to circle which is the correct line tangent // Note that this vector is normalized dir1 = new Vector2(line1.y, -line1.x); // Point on circle line2 = new Vector2(Mathf.Cos(alpha-delta), Mathf.Sin(alpha-delta)); // Vector tangent to circle which is the correct line tangent // Note that this vector is normalized dir2 = new Vector2(line2.y, -line2.x); line1 = line1 * radius + globalCenter; line2 = line2 * radius + globalCenter; } segmentStart = Vector2.zero; segmentEnd = Vector2.zero; segment = false; } /// <summary> /// Creates a VO for avoiding another agent. /// Note that the segment is directed, the agent will want to be on the left side of the segment. /// </summary> public static VO SegmentObstacle (Vector2 segmentStart, Vector2 segmentEnd, Vector2 offset, float radius, float inverseDt, float inverseDeltaTime) { var vo = new VO(); // Adjusted so that a parameter weightFactor of 1 will be the default ("natural") weight factor vo.weightFactor = 1; // Just higher than anything else vo.weightBonus = Mathf.Max(radius, 1)*40; var closestOnSegment = VectorMath.ClosestPointOnSegment(segmentStart, segmentEnd, Vector2.zero); // Collision? if (closestOnSegment.magnitude <= radius) { vo.colliding = true; vo.line1 = closestOnSegment.normalized * (closestOnSegment.magnitude - radius) * 0.3f * inverseDeltaTime; vo.dir1 = new Vector2(vo.line1.y, -vo.line1.x).normalized; vo.line1 += offset; vo.cutoffDir = Vector2.zero; vo.cutoffLine = Vector2.zero; vo.dir2 = Vector2.zero; vo.line2 = Vector2.zero; vo.radius = 0; vo.segmentStart = Vector2.zero; vo.segmentEnd = Vector2.zero; vo.segment = false; } else { vo.colliding = false; segmentStart *= inverseDt; segmentEnd *= inverseDt; radius *= inverseDt; var cutoffTangent = (segmentEnd - segmentStart).normalized; vo.cutoffDir = cutoffTangent; vo.cutoffLine = segmentStart + new Vector2(-cutoffTangent.y, cutoffTangent.x) * radius; vo.cutoffLine += offset; // See documentation for details // The call to Max is just to prevent floating point errors causing NaNs to appear var startSqrMagnitude = segmentStart.sqrMagnitude; var normal1 = -VectorMath.ComplexMultiply(segmentStart, new Vector2(radius, Mathf.Sqrt(Mathf.Max(0, startSqrMagnitude - radius*radius)))) / startSqrMagnitude; var endSqrMagnitude = segmentEnd.sqrMagnitude; var normal2 = -VectorMath.ComplexMultiply(segmentEnd, new Vector2(radius, -Mathf.Sqrt(Mathf.Max(0, endSqrMagnitude - radius*radius)))) / endSqrMagnitude; vo.line1 = segmentStart + normal1 * radius + offset; vo.line2 = segmentEnd + normal2 * radius + offset; // Note that the normals are already normalized vo.dir1 = new Vector2(normal1.y, -normal1.x); vo.dir2 = new Vector2(normal2.y, -normal2.x); vo.segmentStart = segmentStart; vo.segmentEnd = segmentEnd; vo.radius = radius; vo.segment = true; } return vo; } /// <summary> /// Returns a negative number of if p lies on the left side of a line which with one point in a and has a tangent in the direction of dir. /// The number can be seen as the double signed area of the triangle {a, a+dir, p} multiplied by the length of dir. /// If dir.magnitude=1 this is also the distance from p to the line {a, a+dir}. /// </summary> public static float SignedDistanceFromLine (Vector2 a, Vector2 dir, Vector2 p) { return (p.x - a.x) * (dir.y) - (dir.x) * (p.y - a.y); } /// <summary> /// Gradient and value of the cost function of this VO. /// Very similar to the <see cref="Gradient"/> method however the gradient /// and value have been scaled and tweaked slightly. /// </summary> public Vector2 ScaledGradient (Vector2 p, out float weight) { var grad = Gradient(p, out weight); if (weight > 0) { const float Scale = 2; grad *= Scale * weightFactor; weight *= Scale * weightFactor; weight += 1 + weightBonus; } return grad; } /// <summary> /// Gradient and value of the cost function of this VO. /// The VO has a cost function which is 0 outside the VO /// and increases inside it as the point moves further into /// the VO. /// /// This is the negative gradient of that function as well as its /// value (the weight). The negative gradient points in the direction /// where the function decreases the fastest. /// /// The value of the function is the distance to the closest edge /// of the VO and the gradient is normalized. /// </summary> public Vector2 Gradient (Vector2 p, out float weight) { if (colliding) { // Calculate double signed area of the triangle consisting of the points // {line1, line1+dir1, p} float l1 = SignedDistanceFromLine(line1, dir1, p); // Serves as a check for which side of the line the point p is if (l1 >= 0) { weight = l1; return new Vector2(-dir1.y, dir1.x); } else { weight = 0; return new Vector2(0, 0); } } float det3 = SignedDistanceFromLine(cutoffLine, cutoffDir, p); if (det3 <= 0) { weight = 0; return Vector2.zero; } else { // Signed distances to the two edges along the sides of the VO float det1 = SignedDistanceFromLine(line1, dir1, p); float det2 = SignedDistanceFromLine(line2, dir2, p); if (det1 >= 0 && det2 >= 0) { // We are inside both of the half planes // (all three if we count the cutoff line) // and thus inside the forbidden region in velocity space // Actually the negative gradient because we want the // direction where it slopes the most downwards, not upwards Vector2 gradient; // Check if we are in the semicircle region near the cap of the VO if (Vector2.Dot(p - line1, dir1) > 0 && Vector2.Dot(p - line2, dir2) < 0) { if (segment) { // This part will only be reached for line obstacles (i.e not other agents) if (det3 < radius) { var closestPointOnLine = (Vector2)VectorMath.ClosestPointOnSegment(segmentStart, segmentEnd, p); var dirFromCenter = p - closestPointOnLine; float distToCenter; gradient = VectorMath.Normalize(dirFromCenter, out distToCenter); // The weight is the distance to the edge weight = radius - distToCenter; return gradient; } } else { var dirFromCenter = p - circleCenter; float distToCenter; gradient = VectorMath.Normalize(dirFromCenter, out distToCenter); // The weight is the distance to the edge weight = radius - distToCenter; return gradient; } } if (segment && det3 < det1 && det3 < det2) { weight = det3; gradient = new Vector2(-cutoffDir.y, cutoffDir.x); return gradient; } // Just move towards the closest edge // The weight is the distance to the edge if (det1 < det2) { weight = det1; gradient = new Vector2(-dir1.y, dir1.x); } else { weight = det2; gradient = new Vector2(-dir2.y, dir2.x); } return gradient; } weight = 0; return Vector2.zero; } } } /// <summary> /// Very simple list. /// Cannot use a List<T> because when indexing into a List<T> and T is /// a struct (which VO is) then the whole struct will be copied. /// When indexing into an array, that copy can be skipped. /// </summary> internal class VOBuffer { public VO[] buffer; public int length; public void Clear () { length = 0; } public VOBuffer (int n) { buffer = new VO[n]; length = 0; } public void Add (VO vo) { if (length >= buffer.Length) { var nbuffer = new VO[buffer.Length * 2]; buffer.CopyTo(nbuffer, 0); buffer = nbuffer; } buffer[length++] = vo; } } internal void CalculateVelocity (Pathfinding.RVO.Simulator.WorkerContext context) { if (manuallyControlled) { return; } if (locked) { calculatedSpeed = 0; calculatedTargetPoint = position; return; } // Buffer which will be filled up with velocity obstacles (VOs) var vos = context.vos; vos.Clear(); GenerateObstacleVOs(vos); GenerateNeighbourAgentVOs(vos); bool insideAnyVO = BiasDesiredVelocity(vos, ref desiredVelocity, ref desiredTargetPointInVelocitySpace, simulator.symmetryBreakingBias); if (!insideAnyVO) { // Desired velocity can be used directly since it was not inside any velocity obstacle. // No need to run optimizer because this will be the global minima. // This is also a special case in which we can set the // calculated target point to the desired target point // instead of calculating a point based on a calculated velocity // which is an important difference when the agent is very close // to the target point // TODO: Not actually guaranteed to be global minima if desiredTargetPointInVelocitySpace.magnitude < desiredSpeed // maybe do something different here? calculatedTargetPoint = desiredTargetPointInVelocitySpace + position; calculatedSpeed = desiredSpeed; if (DebugDraw) Draw.Debug.CrossXZ(FromXZ(calculatedTargetPoint), Color.white); return; } Vector2 result = Vector2.zero; result = GradientDescent(vos, currentVelocity, desiredVelocity); if (DebugDraw) Draw.Debug.CrossXZ(FromXZ(result+position), Color.white); //Debug.DrawRay (To3D (position), To3D (result)); calculatedTargetPoint = position + result; calculatedSpeed = Mathf.Min(result.magnitude, maxSpeed); } static Color Rainbow (float v) { Color c = new Color(v, 0, 0); if (c.r > 1) { c.g = c.r - 1; c.r = 1; } if (c.g > 1) { c.b = c.g - 1; c.g = 1; } return c; } void GenerateObstacleVOs (VOBuffer vos) { var range = maxSpeed * obstacleTimeHorizon; // Iterate through all obstacles that we might need to avoid for (int i = 0; i < simulator.obstacles.Count; i++) { var obstacle = simulator.obstacles[i]; var vertex = obstacle; // Iterate through all edges (defined by vertex and vertex.dir) in the obstacle do { // Ignore the edge if the agent should not collide with it if (vertex.ignore || (vertex.layer & collidesWith) == 0) { vertex = vertex.next; continue; } // Start and end points of the current segment float elevation1, elevation2; var p1 = To2D(vertex.position, out elevation1); var p2 = To2D(vertex.next.position, out elevation2); Vector2 dir = (p2 - p1).normalized; // Signed distance from the line (not segment, lines are infinite) // TODO: Can be optimized float dist = VO.SignedDistanceFromLine(p1, dir, position); if (dist >= -0.01f && dist < range) { float factorAlongSegment = Vector2.Dot(position - p1, p2 - p1) / (p2 - p1).sqrMagnitude; // Calculate the elevation (y) coordinate of the point on the segment closest to the agent var segmentY = Mathf.Lerp(elevation1, elevation2, factorAlongSegment); // Calculate distance from the segment (not line) var sqrDistToSegment = (Vector2.Lerp(p1, p2, factorAlongSegment) - position).sqrMagnitude; // Ignore the segment if it is too far away // or the agent is too high up (or too far down) on the elevation axis (usually y axis) to avoid it. // If the XY plane is used then all elevation checks are disabled if (sqrDistToSegment < range*range && (simulator.movementPlane == MovementPlane.XY || (elevationCoordinate <= segmentY + vertex.height && elevationCoordinate+height >= segmentY))) { vos.Add(VO.SegmentObstacle(p2 - position, p1 - position, Vector2.zero, radius * 0.01f, 1f / ObstacleTimeHorizon, 1f / simulator.DeltaTime)); } } vertex = vertex.next; } while (vertex != obstacle && vertex != null && vertex.next != null); } } void GenerateNeighbourAgentVOs (VOBuffer vos) { float inverseAgentTimeHorizon = 1.0f/agentTimeHorizon; // The RVO algorithm assumes we will continue to // move in roughly the same direction Vector2 optimalVelocity = currentVelocity; for (int o = 0; o < neighbours.Count; o++) { Agent other = neighbours[o]; // Don't avoid ourselves if (other == this) continue; // Interval along the y axis in which the agents overlap float maxY = System.Math.Min(elevationCoordinate + height, other.elevationCoordinate + other.height); float minY = System.Math.Max(elevationCoordinate, other.elevationCoordinate); // The agents cannot collide since they are on different y-levels if (maxY - minY < 0) { continue; } float totalRadius = radius + other.radius; // Describes a circle on the border of the VO Vector2 voBoundingOrigin = other.position - position; float avoidanceStrength; if (other.locked || other.manuallyControlled) { avoidanceStrength = 1; } else if (other.Priority > 0.00001f || Priority > 0.00001f) { avoidanceStrength = other.Priority / (Priority + other.Priority); } else { // Both this agent's priority and the other agent's priority is zero or negative // Assume they have the same priority avoidanceStrength = 0.5f; } // We assume that the other agent will continue to move with roughly the same velocity if the priorities for the agents are similar. // If the other agent has a higher priority than this agent (avoidanceStrength > 0.5) then we will assume it will move more along its // desired velocity. This will have the effect of other agents trying to clear a path for where a high priority agent wants to go. // If this is not done then even high priority agents can get stuck when it is really crowded and they have had to slow down. Vector2 otherOptimalVelocity = Vector2.Lerp(other.currentVelocity, other.desiredVelocity, 2*avoidanceStrength - 1); var voCenter = Vector2.Lerp(optimalVelocity, otherOptimalVelocity, avoidanceStrength); vos.Add(new VO(voBoundingOrigin, voCenter, totalRadius, inverseAgentTimeHorizon, 1 / simulator.DeltaTime)); if (DebugDraw) DrawVO(position + voBoundingOrigin * inverseAgentTimeHorizon + voCenter, totalRadius * inverseAgentTimeHorizon, position + voCenter); } } Vector2 GradientDescent (VOBuffer vos, Vector2 sampleAround1, Vector2 sampleAround2) { float score1; var minima1 = Trace(vos, sampleAround1, out score1); if (DebugDraw) Draw.Debug.CrossXZ(FromXZ(minima1 + position), Color.yellow, 0.5f); // Can be uncommented for higher quality local avoidance // for ( int i = 0; i < 3; i++ ) { // Vector2 p = desiredVelocity + new Vector2(Mathf.Cos(Mathf.PI*2*(i/3.0f)), Mathf.Sin(Mathf.PI*2*(i/3.0f))); // float score;Vector2 res = Trace ( vos, p, velocity.magnitude*simulator.qualityCutoff, out score ); // // if ( score < best ) { // result = res; // best = score; // } // } float score2; Vector2 minima2 = Trace(vos, sampleAround2, out score2); if (DebugDraw) Draw.Debug.CrossXZ(FromXZ(minima2 + position), Color.magenta, 0.5f); return score1 < score2 ? minima1 : minima2; } /// <summary> /// Bias towards the right side of agents. /// Rotate desiredVelocity at most [value] number of radians. 1 radian ≈ 57° /// This breaks up symmetries. /// /// The desired velocity will only be rotated if it is inside a velocity obstacle (VO). /// If it is inside one, it will not be rotated further than to the edge of it /// /// The targetPointInVelocitySpace will be rotated by the same amount as the desired velocity /// /// Returns: True if the desired velocity was inside any VO /// </summary> static bool BiasDesiredVelocity (VOBuffer vos, ref Vector2 desiredVelocity, ref Vector2 targetPointInVelocitySpace, float maxBiasRadians) { var desiredVelocityMagn = desiredVelocity.magnitude; var maxValue = 0f; for (int i = 0; i < vos.length; i++) { float value; // The value is approximately the distance to the edge of the VO // so taking the maximum will give us the distance to the edge of the VO // which the desired velocity is furthest inside vos.buffer[i].Gradient(desiredVelocity, out value); maxValue = Mathf.Max(maxValue, value); } // Check if the agent was inside any VO var inside = maxValue > 0; // Avoid division by zero below if (desiredVelocityMagn < 0.001f) { return inside; } // Rotate the desired velocity clockwise (to the right) at most maxBiasRadians number of radians // Assuming maxBiasRadians is small, we can just move it instead and it will give approximately the same effect // See https://en.wikipedia.org/wiki/Small-angle_approximation var angle = Mathf.Min(maxBiasRadians, maxValue / desiredVelocityMagn); desiredVelocity += new Vector2(desiredVelocity.y, -desiredVelocity.x) * angle; targetPointInVelocitySpace += new Vector2(targetPointInVelocitySpace.y, -targetPointInVelocitySpace.x) * angle; return inside; } /// <summary>Evaluate gradient and value of the cost function at velocity p</summary> Vector2 EvaluateGradient (VOBuffer vos, Vector2 p, out float value) { Vector2 gradient = Vector2.zero; value = 0; // Avoid other agents for (int i = 0; i < vos.length; i++) { float w; var grad = vos.buffer[i].ScaledGradient(p, out w); if (w > value) { value = w; gradient = grad; } } // Move closer to the desired velocity var dirToDesiredVelocity = desiredVelocity - p; var distToDesiredVelocity = dirToDesiredVelocity.magnitude; if (distToDesiredVelocity > 0.0001f) { gradient += dirToDesiredVelocity * (DesiredVelocityWeight/distToDesiredVelocity); value += distToDesiredVelocity * DesiredVelocityWeight; } // Prefer speeds lower or equal to the desired speed // and avoid speeds greater than the max speed var sqrSpeed = p.sqrMagnitude; if (sqrSpeed > desiredSpeed*desiredSpeed) { var speed = Mathf.Sqrt(sqrSpeed); if (speed > maxSpeed) { const float MaxSpeedWeight = 3; value += MaxSpeedWeight * (speed - maxSpeed); gradient -= MaxSpeedWeight * (p/speed); } // Scale needs to be strictly greater than DesiredVelocityWeight // otherwise the agent will not prefer the desired speed over // the maximum speed float scale = 2*DesiredVelocityWeight; value += scale * (speed - desiredSpeed); gradient -= scale * (p/speed); } return gradient; } /// <summary> /// Traces the vector field constructed out of the velocity obstacles. /// Returns the position which gives the minimum score (approximately). /// /// See: https://en.wikipedia.org/wiki/Gradient_descent /// </summary> Vector2 Trace (VOBuffer vos, Vector2 p, out float score) { // Pick a reasonable initial step size float stepSize = Mathf.Max(radius, 0.2f * desiredSpeed); float bestScore = float.PositiveInfinity; Vector2 bestP = p; // TODO: Add momentum to speed up convergence? const int MaxIterations = 50; for (int s = 0; s < MaxIterations; s++) { float step = 1.0f - (s/(float)MaxIterations); step = Sqr(step) * stepSize; float value; var gradient = EvaluateGradient(vos, p, out value); if (value < bestScore) { bestScore = value; bestP = p; } // TODO: Add cutoff for performance gradient.Normalize(); gradient *= step; Vector2 prev = p; p += gradient; if (DebugDraw) Debug.DrawLine(FromXZ(prev + position), FromXZ(p + position), Rainbow(s*0.1f) * new Color(1, 1, 1, 1f)); } score = bestScore; return bestP; } } }