using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Serialization;
namespace Pathfinding.RVO {
using Pathfinding.Util;
///
/// RVO Character Controller.
/// Similar to Unity's CharacterController. It handles movement calculations and takes other agents into account.
/// It does not handle movement itself, but allows the calling script to get the calculated velocity and
/// use that to move the object using a method it sees fit (for example using a CharacterController, using
/// transform.Translate or using a rigidbody).
///
///
/// public void Update () {
/// // Just some point far away
/// var targetPoint = transform.position + transform.forward * 100;
///
/// // Set the desired point to move towards using a desired speed of 10 and a max speed of 12
/// controller.SetTarget(targetPoint, 10, 12);
///
/// // Calculate how much to move during this frame
/// // This information is based on movement commands from earlier frames
/// // as local avoidance is calculated globally at regular intervals by the RVOSimulator component
/// var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime);
/// transform.position = transform.position + delta;
/// }
///
///
/// For documentation of many of the variables of this class: refer to the Pathfinding.RVO.IAgent interface.
///
/// Note: Requires a single RVOSimulator component in the scene
///
/// See: Pathfinding.RVO.IAgent
/// See: RVOSimulator
/// See: local-avoidance (view in online documentation for working links)
///
[AddComponentMenu("Pathfinding/Local Avoidance/RVO Controller")]
[HelpURL("http://arongranberg.com/astar/documentation/stable/class_pathfinding_1_1_r_v_o_1_1_r_v_o_controller.php")]
public class RVOController : VersionedMonoBehaviour {
[SerializeField][FormerlySerializedAs("radius")]
internal float radiusBackingField = 0.5f;
[SerializeField][FormerlySerializedAs("height")]
float heightBackingField = 2;
[SerializeField][FormerlySerializedAs("center")]
float centerBackingField = 1;
///
/// Radius of the agent in world units.
/// Note: If a movement script (AIPath/RichAI/AILerp, anything implementing the IAstarAI interface) is attached to the same GameObject, this value will be driven by that script.
///
public float radius {
get {
if (ai != null) return ai.radius;
return radiusBackingField;
}
set {
if (ai != null) ai.radius = value;
radiusBackingField = value;
}
}
///
/// Height of the agent in world units.
/// Note: If a movement script (AIPath/RichAI/AILerp, anything implementing the IAstarAI interface) is attached to the same GameObject, this value will be driven by that script.
///
public float height {
get {
if (ai != null) return ai.height;
return heightBackingField;
}
set {
if (ai != null) ai.height = value;
heightBackingField = value;
}
}
/// A locked unit cannot move. Other units will still avoid it but avoidance quality is not the best.
[Tooltip("A locked unit cannot move. Other units will still avoid it. But avoidance quality is not the best")]
public bool locked;
///
/// Automatically set to true when desired velocity is approximately zero.
/// This prevents other units from pushing them away when they are supposed to e.g block a choke point.
///
/// When this is true every call to or will set the field to true if the desired velocity
/// was non-zero or false if it was zero.
///
[Tooltip("Automatically set #locked to true when desired velocity is approximately zero")]
public bool lockWhenNotMoving = false;
/// How far into the future to look for collisions with other agents (in seconds)
[Tooltip("How far into the future to look for collisions with other agents (in seconds)")]
public float agentTimeHorizon = 2;
/// How far into the future to look for collisions with obstacles (in seconds)
[Tooltip("How far into the future to look for collisions with obstacles (in seconds)")]
public float obstacleTimeHorizon = 2;
///
/// Max number of other agents to take into account.
/// A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.
///
[Tooltip("Max number of other agents to take into account.\n" +
"A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.")]
public int maxNeighbours = 10;
///
/// Specifies the avoidance layer for this agent.
/// The mask on other agents will determine if they will avoid this agent.
///
public RVOLayer layer = RVOLayer.DefaultAgent;
///
/// Layer mask specifying which layers this agent will avoid.
/// You can set it as CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ...
///
/// This can be very useful in games which have multiple teams of some sort. For example you usually
/// want the agents in one team to avoid each other, but you do not want them to avoid the enemies.
///
/// This field only affects which other agents that this agent will avoid, it does not affect how other agents
/// react to this agent.
///
/// See: bitmasks (view in online documentation for working links)
/// See: http://en.wikipedia.org/wiki/Mask_(computing)
///
[Pathfinding.EnumFlag]
public RVOLayer collidesWith = (RVOLayer)(-1);
///
/// An extra force to avoid walls.
/// This can be good way to reduce "wall hugging" behaviour.
///
/// Deprecated: This feature is currently disabled as it didn't work that well and was tricky to support after some changes to the RVO system. It may be enabled again in a future version.
///
[HideInInspector]
[System.Obsolete]
public float wallAvoidForce = 1;
///
/// How much the wallAvoidForce decreases with distance.
/// The strenght of avoidance is:
/// str = 1/dist*wallAvoidFalloff
///
/// See: wallAvoidForce
///
/// Deprecated: This feature is currently disabled as it didn't work that well and was tricky to support after some changes to the RVO system. It may be enabled again in a future version.
///
[HideInInspector]
[System.Obsolete]
public float wallAvoidFalloff = 1;
/// \copydoc Pathfinding::RVO::IAgent::Priority
[Tooltip("How strongly other agents will avoid this agent")]
[UnityEngine.Range(0, 1)]
public float priority = 0.5f;
///
/// Center of the agent relative to the pivot point of this game object.
/// Note: If a movement script (AIPath/RichAI/AILerp, anything implementing the IAstarAI interface) is attached to the same GameObject, this value will be driven by that script.
///
public float center {
get {
// With an AI attached, this will always be driven to height/2 because the movement script expects the object position to be at its feet
if (ai != null) return ai.height/2;
return centerBackingField;
}
set {
centerBackingField = value;
}
}
/// \details Deprecated:
[System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
public LayerMask mask { get { return 0; } set {} }
/// \details Deprecated:
[System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
public bool enableRotation { get { return false; } set {} }
/// \details Deprecated:
[System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
public float rotationSpeed { get { return 0; } set {} }
/// \details Deprecated:
[System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
public float maxSpeed { get { return 0; } set {} }
/// Determines if the XY (2D) or XZ (3D) plane is used for movement
public MovementPlane movementPlane {
get {
if (simulator != null) return simulator.movementPlane;
else if (RVOSimulator.active) return RVOSimulator.active.movementPlane;
else return MovementPlane.XZ;
}
}
/// Reference to the internal agent
public IAgent rvoAgent { get; private set; }
/// Reference to the rvo simulator
public Simulator simulator { get; private set; }
/// Cached tranform component
protected Transform tr;
[SerializeField]
[FormerlySerializedAs("ai")]
IAstarAI aiBackingField;
/// Cached reference to a movement script (if one is used)
protected IAstarAI ai {
get {
#if UNITY_EDITOR
if (aiBackingField == null && !Application.isPlaying) aiBackingField = GetComponent();
#endif
// Note: have to cast to MonoBehaviour to get Unity's special overloaded == operator.
// If we didn't do this then this property could return a non-null value that pointed to a destroyed component.
if ((aiBackingField as MonoBehaviour) == null) aiBackingField = null;
return aiBackingField;
}
set {
aiBackingField = value;
}
}
/// Enables drawing debug information in the scene view
public bool debug;
///
/// Current position of the agent.
/// Note that this is only updated every local avoidance simulation step, not every frame.
///
public Vector3 position {
get {
return To3D(rvoAgent.Position, rvoAgent.ElevationCoordinate);
}
}
///
/// Current calculated velocity of the agent.
/// This is not necessarily the velocity the agent is actually moving with
/// (that is up to the movement script to decide) but it is the velocity
/// that the RVO system has calculated is best for avoiding obstacles and
/// reaching the target.
///
/// See: CalculateMovementDelta
///
/// You can also set the velocity of the agent. This will override the local avoidance input completely.
/// It is useful if you have a player controlled character and want other agents to avoid it.
///
/// Setting the velocity using this property will mark the agent as being externally controlled for 1 simulation step.
/// Local avoidance calculations will be skipped for the next simulation step but will be resumed
/// after that unless this property is set again.
///
/// Note that if you set the velocity the value that can be read from this property will not change until
/// the next simulation step.
///
/// See:
/// See: ManualRVOAgent.cs (view in online documentation for working links)
///
public Vector3 velocity {
get {
// For best accuracy and to allow other code to do things like Move(agent.velocity * Time.deltaTime)
// the code bases the velocity on how far the agent should move during this frame.
// Unless the game is paused (timescale is zero) then just use a very small dt.
var dt = Time.deltaTime > 0.0001f ? Time.deltaTime : 0.02f;
return CalculateMovementDelta(dt) / dt;
}
set {
rvoAgent.ForceSetVelocity(To2D(value));
}
}
///
/// Direction and distance to move in a single frame to avoid obstacles.
///
/// The position of the agent is taken from the attached movement script's position (see or if none is attached then transform.position.
///
/// How far to move [seconds].
/// Usually set to Time.deltaTime.
public Vector3 CalculateMovementDelta (float deltaTime) {
if (rvoAgent == null) return Vector3.zero;
return To3D(Vector2.ClampMagnitude(rvoAgent.CalculatedTargetPoint - To2D(ai != null ? ai.position : tr.position), rvoAgent.CalculatedSpeed * deltaTime), 0);
}
///
/// Direction and distance to move in a single frame to avoid obstacles.
///
///
/// public void Update () {
/// // Just some point far away
/// var targetPoint = transform.position + transform.forward * 100;
///
/// // Set the desired point to move towards using a desired speed of 10 and a max speed of 12
/// controller.SetTarget(targetPoint, 10, 12);
///
/// // Calculate how much to move during this frame
/// // This information is based on movement commands from earlier frames
/// // as local avoidance is calculated globally at regular intervals by the RVOSimulator component
/// var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime);
/// transform.position = transform.position + delta;
/// }
///
///
/// Position of the agent.
/// How far to move [seconds].
/// Usually set to Time.deltaTime.
public Vector3 CalculateMovementDelta (Vector3 position, float deltaTime) {
return To3D(Vector2.ClampMagnitude(rvoAgent.CalculatedTargetPoint - To2D(position), rvoAgent.CalculatedSpeed * deltaTime), 0);
}
/// \copydoc Pathfinding::RVO::IAgent::SetCollisionNormal
public void SetCollisionNormal (Vector3 normal) {
rvoAgent.SetCollisionNormal(To2D(normal));
}
///
/// \copydoc Pathfinding::RVO::IAgent::ForceSetVelocity.
/// Deprecated: Set the property instead
///
[System.Obsolete("Set the 'velocity' property instead")]
public void ForceSetVelocity (Vector3 velocity) {
this.velocity = velocity;
}
///
/// Converts a 3D vector to a 2D vector in the movement plane.
/// If movementPlane is XZ it will be projected onto the XZ plane
/// otherwise it will be projected onto the XY plane.
///
public Vector2 To2D (Vector3 p) {
float dummy;
return To2D(p, out dummy);
}
///
/// 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.
///
public Vector2 To2D (Vector3 p, out float elevation) {
if (movementPlane == MovementPlane.XY) {
elevation = -p.z;
return new Vector2(p.x, p.y);
} else {
elevation = p.y;
return new Vector2(p.x, p.z);
}
}
///
/// Converts a 2D vector in the movement plane as well as an elevation to a 3D coordinate.
/// See: To2D
/// See: movementPlane
///
public Vector3 To3D (Vector2 p, float elevationCoordinate) {
if (movementPlane == MovementPlane.XY) {
return new Vector3(p.x, p.y, -elevationCoordinate);
} else {
return new Vector3(p.x, elevationCoordinate, p.y);
}
}
void OnDisable () {
if (simulator == null) return;
// Remove the agent from the simulation but keep the reference
// this component might get enabled and then we can simply
// add it to the simulation again
simulator.RemoveAgent(rvoAgent);
}
void OnEnable () {
tr = transform;
ai = GetComponent();
var aiBase = ai as AIBase;
// Make sure the AI finds this component
// This is useful if the RVOController was added during runtime.
if (aiBase != null) aiBase.FindComponents();
if (RVOSimulator.active == null) {
Debug.LogError("No RVOSimulator component found in the scene. Please add one.");
enabled = false;
} else {
simulator = RVOSimulator.active.GetSimulator();
// We might already have an rvoAgent instance which was disabled previously
// if so, we can simply add it to the simulation again
if (rvoAgent != null) {
simulator.AddAgent(rvoAgent);
} else {
rvoAgent = simulator.AddAgent(Vector2.zero, 0);
rvoAgent.PreCalculationCallback = UpdateAgentProperties;
}
}
}
protected void UpdateAgentProperties () {
var scale = tr.localScale;
rvoAgent.Radius = Mathf.Max(0.001f, radius * scale.x);
rvoAgent.AgentTimeHorizon = agentTimeHorizon;
rvoAgent.ObstacleTimeHorizon = obstacleTimeHorizon;
rvoAgent.Locked = locked;
rvoAgent.MaxNeighbours = maxNeighbours;
rvoAgent.DebugDraw = debug;
rvoAgent.Layer = layer;
rvoAgent.CollidesWith = collidesWith;
rvoAgent.Priority = priority;
float elevation;
// Use the position from the movement script if one is attached
// as the movement script's position may not be the same as the transform's position
// (in particular if IAstarAI.updatePosition is false).
rvoAgent.Position = To2D(ai != null ? ai.position : tr.position, out elevation);
if (movementPlane == MovementPlane.XZ) {
rvoAgent.Height = height * scale.y;
rvoAgent.ElevationCoordinate = elevation + (center - 0.5f * height) * scale.y;
} else {
rvoAgent.Height = 1;
rvoAgent.ElevationCoordinate = 0;
}
}
///
/// Set the target point for the agent to move towards.
/// Similar to the method but this is more flexible.
/// It is also better to use near the end of the path as when using the Move
/// method the agent does not know where to stop, so it may overshoot the target.
/// When using this method the agent will not overshoot the target.
/// The agent will assume that it will stop when it reaches the target so make sure that
/// you don't place the point too close to the agent if you actually just want to move in a
/// particular direction.
///
/// The target point is assumed to stay the same until something else is requested (as opposed to being reset every frame).
///
/// See: Also take a look at the documentation for which has a few more details.
/// See:
///
/// Point in world space to move towards.
/// Desired speed in world units per second.
/// Maximum speed in world units per second.
/// The agent will use this speed if it is necessary to avoid collisions with other agents.
/// Should be at least as high as speed, but it is recommended to use a slightly higher value than speed (for example speed*1.2).
public void SetTarget (Vector3 pos, float speed, float maxSpeed) {
if (simulator == null) return;
rvoAgent.SetTarget(To2D(pos), speed, maxSpeed);
if (lockWhenNotMoving) {
locked = speed < 0.001f;
}
}
///
/// Set the desired velocity for the agent.
/// Note that this is a velocity (units/second), not a movement delta (units/frame).
///
/// This is assumed to stay the same until something else is requested (as opposed to being reset every frame).
///
/// Note: In most cases the SetTarget method is better to use.
/// What this will actually do is call SetTarget with (position + velocity).
/// See the note in the documentation for IAgent.SetTarget about the potential
/// issues that this can cause (in particular that it might be hard to get the agent
/// to stop at a precise point).
///
/// See:
///
public void Move (Vector3 vel) {
if (simulator == null) return;
var velocity2D = To2D(vel);
var speed = velocity2D.magnitude;
rvoAgent.SetTarget(To2D(ai != null ? ai.position : tr.position) + velocity2D, speed, speed);
if (lockWhenNotMoving) {
locked = speed < 0.001f;
}
}
///
/// Teleport the agent to a new position.
/// Deprecated: Use transform.position instead, the RVOController can now handle that without any issues.
///
[System.Obsolete("Use transform.position instead, the RVOController can now handle that without any issues.")]
public void Teleport (Vector3 pos) {
tr.position = pos;
}
void OnDrawGizmos () {
tr = transform;
// The AI script will draw similar gizmos
if (ai == null) {
var color = AIBase.ShapeGizmoColor * (locked ? 0.5f : 1.0f);
var pos = transform.position;
var scale = tr.localScale;
if (movementPlane == MovementPlane.XY) {
Draw.Gizmos.Cylinder(pos, Vector3.forward, 0, radius * scale.x, color);
} else {
Draw.Gizmos.Cylinder(pos + To3D(Vector2.zero, center - height * 0.5f) * scale.y, To3D(Vector2.zero, 1), height * scale.y, radius * scale.x, color);
}
}
}
protected override int OnUpgradeSerializedData (int version, bool unityThread) {
if (version <= 1) {
if (!unityThread) return -1;
if (transform.localScale.y != 0) centerBackingField /= Mathf.Abs(transform.localScale.y);
if (transform.localScale.y != 0) heightBackingField /= Mathf.Abs(transform.localScale.y);
if (transform.localScale.x != 0) radiusBackingField /= Mathf.Abs(transform.localScale.x);
}
return 2;
}
}
}