RVOController.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. using UnityEngine.Serialization;
  4. namespace Pathfinding.RVO {
  5. using Pathfinding.Util;
  6. /// <summary>
  7. /// RVO Character Controller.
  8. /// Similar to Unity's CharacterController. It handles movement calculations and takes other agents into account.
  9. /// It does not handle movement itself, but allows the calling script to get the calculated velocity and
  10. /// use that to move the object using a method it sees fit (for example using a CharacterController, using
  11. /// transform.Translate or using a rigidbody).
  12. ///
  13. /// <code>
  14. /// public void Update () {
  15. /// // Just some point far away
  16. /// var targetPoint = transform.position + transform.forward * 100;
  17. ///
  18. /// // Set the desired point to move towards using a desired speed of 10 and a max speed of 12
  19. /// controller.SetTarget(targetPoint, 10, 12);
  20. ///
  21. /// // Calculate how much to move during this frame
  22. /// // This information is based on movement commands from earlier frames
  23. /// // as local avoidance is calculated globally at regular intervals by the RVOSimulator component
  24. /// var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime);
  25. /// transform.position = transform.position + delta;
  26. /// }
  27. /// </code>
  28. ///
  29. /// For documentation of many of the variables of this class: refer to the Pathfinding.RVO.IAgent interface.
  30. ///
  31. /// Note: Requires a single RVOSimulator component in the scene
  32. ///
  33. /// See: Pathfinding.RVO.IAgent
  34. /// See: RVOSimulator
  35. /// See: local-avoidance (view in online documentation for working links)
  36. /// </summary>
  37. [AddComponentMenu("Pathfinding/Local Avoidance/RVO Controller")]
  38. [HelpURL("http://arongranberg.com/astar/documentation/stable/class_pathfinding_1_1_r_v_o_1_1_r_v_o_controller.php")]
  39. public class RVOController : VersionedMonoBehaviour {
  40. [SerializeField][FormerlySerializedAs("radius")]
  41. internal float radiusBackingField = 0.5f;
  42. [SerializeField][FormerlySerializedAs("height")]
  43. float heightBackingField = 2;
  44. [SerializeField][FormerlySerializedAs("center")]
  45. float centerBackingField = 1;
  46. /// <summary>
  47. /// Radius of the agent in world units.
  48. /// 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.
  49. /// </summary>
  50. public float radius {
  51. get {
  52. if (ai != null) return ai.radius;
  53. return radiusBackingField;
  54. }
  55. set {
  56. if (ai != null) ai.radius = value;
  57. radiusBackingField = value;
  58. }
  59. }
  60. /// <summary>
  61. /// Height of the agent in world units.
  62. /// 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.
  63. /// </summary>
  64. public float height {
  65. get {
  66. if (ai != null) return ai.height;
  67. return heightBackingField;
  68. }
  69. set {
  70. if (ai != null) ai.height = value;
  71. heightBackingField = value;
  72. }
  73. }
  74. /// <summary>A locked unit cannot move. Other units will still avoid it but avoidance quality is not the best.</summary>
  75. [Tooltip("A locked unit cannot move. Other units will still avoid it. But avoidance quality is not the best")]
  76. public bool locked;
  77. /// <summary>
  78. /// Automatically set <see cref="locked"/> to true when desired velocity is approximately zero.
  79. /// This prevents other units from pushing them away when they are supposed to e.g block a choke point.
  80. ///
  81. /// When this is true every call to <see cref="SetTarget"/> or <see cref="Move"/> will set the <see cref="locked"/> field to true if the desired velocity
  82. /// was non-zero or false if it was zero.
  83. /// </summary>
  84. [Tooltip("Automatically set #locked to true when desired velocity is approximately zero")]
  85. public bool lockWhenNotMoving = false;
  86. /// <summary>How far into the future to look for collisions with other agents (in seconds)</summary>
  87. [Tooltip("How far into the future to look for collisions with other agents (in seconds)")]
  88. public float agentTimeHorizon = 2;
  89. /// <summary>How far into the future to look for collisions with obstacles (in seconds)</summary>
  90. [Tooltip("How far into the future to look for collisions with obstacles (in seconds)")]
  91. public float obstacleTimeHorizon = 2;
  92. /// <summary>
  93. /// Max number of other agents to take into account.
  94. /// A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.
  95. /// </summary>
  96. [Tooltip("Max number of other agents to take into account.\n" +
  97. "A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.")]
  98. public int maxNeighbours = 10;
  99. /// <summary>
  100. /// Specifies the avoidance layer for this agent.
  101. /// The <see cref="collidesWith"/> mask on other agents will determine if they will avoid this agent.
  102. /// </summary>
  103. public RVOLayer layer = RVOLayer.DefaultAgent;
  104. /// <summary>
  105. /// Layer mask specifying which layers this agent will avoid.
  106. /// You can set it as CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ...
  107. ///
  108. /// This can be very useful in games which have multiple teams of some sort. For example you usually
  109. /// want the agents in one team to avoid each other, but you do not want them to avoid the enemies.
  110. ///
  111. /// This field only affects which other agents that this agent will avoid, it does not affect how other agents
  112. /// react to this agent.
  113. ///
  114. /// See: bitmasks (view in online documentation for working links)
  115. /// See: http://en.wikipedia.org/wiki/Mask_(computing)
  116. /// </summary>
  117. [Pathfinding.EnumFlag]
  118. public RVOLayer collidesWith = (RVOLayer)(-1);
  119. /// <summary>
  120. /// An extra force to avoid walls.
  121. /// This can be good way to reduce "wall hugging" behaviour.
  122. ///
  123. /// 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.
  124. /// </summary>
  125. [HideInInspector]
  126. [System.Obsolete]
  127. public float wallAvoidForce = 1;
  128. /// <summary>
  129. /// How much the wallAvoidForce decreases with distance.
  130. /// The strenght of avoidance is:
  131. /// <code> str = 1/dist*wallAvoidFalloff </code>
  132. ///
  133. /// See: wallAvoidForce
  134. ///
  135. /// 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.
  136. /// </summary>
  137. [HideInInspector]
  138. [System.Obsolete]
  139. public float wallAvoidFalloff = 1;
  140. /// <summary>\copydoc Pathfinding::RVO::IAgent::Priority</summary>
  141. [Tooltip("How strongly other agents will avoid this agent")]
  142. [UnityEngine.Range(0, 1)]
  143. public float priority = 0.5f;
  144. /// <summary>
  145. /// Center of the agent relative to the pivot point of this game object.
  146. /// 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.
  147. /// </summary>
  148. public float center {
  149. get {
  150. // 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
  151. if (ai != null) return ai.height/2;
  152. return centerBackingField;
  153. }
  154. set {
  155. centerBackingField = value;
  156. }
  157. }
  158. /// <summary>\details Deprecated:</summary>
  159. [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
  160. public LayerMask mask { get { return 0; } set {} }
  161. /// <summary>\details Deprecated:</summary>
  162. [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
  163. public bool enableRotation { get { return false; } set {} }
  164. /// <summary>\details Deprecated:</summary>
  165. [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
  166. public float rotationSpeed { get { return 0; } set {} }
  167. /// <summary>\details Deprecated:</summary>
  168. [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")]
  169. public float maxSpeed { get { return 0; } set {} }
  170. /// <summary>Determines if the XY (2D) or XZ (3D) plane is used for movement</summary>
  171. public MovementPlane movementPlane {
  172. get {
  173. if (simulator != null) return simulator.movementPlane;
  174. else if (RVOSimulator.active) return RVOSimulator.active.movementPlane;
  175. else return MovementPlane.XZ;
  176. }
  177. }
  178. /// <summary>Reference to the internal agent</summary>
  179. public IAgent rvoAgent { get; private set; }
  180. /// <summary>Reference to the rvo simulator</summary>
  181. public Simulator simulator { get; private set; }
  182. /// <summary>Cached tranform component</summary>
  183. protected Transform tr;
  184. [SerializeField]
  185. [FormerlySerializedAs("ai")]
  186. IAstarAI aiBackingField;
  187. /// <summary>Cached reference to a movement script (if one is used)</summary>
  188. protected IAstarAI ai {
  189. get {
  190. #if UNITY_EDITOR
  191. if (aiBackingField == null && !Application.isPlaying) aiBackingField = GetComponent<IAstarAI>();
  192. #endif
  193. // Note: have to cast to MonoBehaviour to get Unity's special overloaded == operator.
  194. // If we didn't do this then this property could return a non-null value that pointed to a destroyed component.
  195. if ((aiBackingField as MonoBehaviour) == null) aiBackingField = null;
  196. return aiBackingField;
  197. }
  198. set {
  199. aiBackingField = value;
  200. }
  201. }
  202. /// <summary>Enables drawing debug information in the scene view</summary>
  203. public bool debug;
  204. /// <summary>
  205. /// Current position of the agent.
  206. /// Note that this is only updated every local avoidance simulation step, not every frame.
  207. /// </summary>
  208. public Vector3 position {
  209. get {
  210. return To3D(rvoAgent.Position, rvoAgent.ElevationCoordinate);
  211. }
  212. }
  213. /// <summary>
  214. /// Current calculated velocity of the agent.
  215. /// This is not necessarily the velocity the agent is actually moving with
  216. /// (that is up to the movement script to decide) but it is the velocity
  217. /// that the RVO system has calculated is best for avoiding obstacles and
  218. /// reaching the target.
  219. ///
  220. /// See: CalculateMovementDelta
  221. ///
  222. /// You can also set the velocity of the agent. This will override the local avoidance input completely.
  223. /// It is useful if you have a player controlled character and want other agents to avoid it.
  224. ///
  225. /// Setting the velocity using this property will mark the agent as being externally controlled for 1 simulation step.
  226. /// Local avoidance calculations will be skipped for the next simulation step but will be resumed
  227. /// after that unless this property is set again.
  228. ///
  229. /// Note that if you set the velocity the value that can be read from this property will not change until
  230. /// the next simulation step.
  231. ///
  232. /// See: <see cref="Pathfinding::RVO::IAgent::ForceSetVelocity"/>
  233. /// See: ManualRVOAgent.cs (view in online documentation for working links)
  234. /// </summary>
  235. public Vector3 velocity {
  236. get {
  237. // For best accuracy and to allow other code to do things like Move(agent.velocity * Time.deltaTime)
  238. // the code bases the velocity on how far the agent should move during this frame.
  239. // Unless the game is paused (timescale is zero) then just use a very small dt.
  240. var dt = Time.deltaTime > 0.0001f ? Time.deltaTime : 0.02f;
  241. return CalculateMovementDelta(dt) / dt;
  242. }
  243. set {
  244. rvoAgent.ForceSetVelocity(To2D(value));
  245. }
  246. }
  247. /// <summary>
  248. /// Direction and distance to move in a single frame to avoid obstacles.
  249. ///
  250. /// The position of the agent is taken from the attached movement script's position (see <see cref="Pathfinding.IAstarAI.position)"/> or if none is attached then transform.position.
  251. /// </summary>
  252. /// <param name="deltaTime">How far to move [seconds].
  253. /// Usually set to Time.deltaTime.</param>
  254. public Vector3 CalculateMovementDelta (float deltaTime) {
  255. if (rvoAgent == null) return Vector3.zero;
  256. return To3D(Vector2.ClampMagnitude(rvoAgent.CalculatedTargetPoint - To2D(ai != null ? ai.position : tr.position), rvoAgent.CalculatedSpeed * deltaTime), 0);
  257. }
  258. /// <summary>
  259. /// Direction and distance to move in a single frame to avoid obstacles.
  260. ///
  261. /// <code>
  262. /// public void Update () {
  263. /// // Just some point far away
  264. /// var targetPoint = transform.position + transform.forward * 100;
  265. ///
  266. /// // Set the desired point to move towards using a desired speed of 10 and a max speed of 12
  267. /// controller.SetTarget(targetPoint, 10, 12);
  268. ///
  269. /// // Calculate how much to move during this frame
  270. /// // This information is based on movement commands from earlier frames
  271. /// // as local avoidance is calculated globally at regular intervals by the RVOSimulator component
  272. /// var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime);
  273. /// transform.position = transform.position + delta;
  274. /// }
  275. /// </code>
  276. /// </summary>
  277. /// <param name="position">Position of the agent.</param>
  278. /// <param name="deltaTime">How far to move [seconds].
  279. /// Usually set to Time.deltaTime.</param>
  280. public Vector3 CalculateMovementDelta (Vector3 position, float deltaTime) {
  281. return To3D(Vector2.ClampMagnitude(rvoAgent.CalculatedTargetPoint - To2D(position), rvoAgent.CalculatedSpeed * deltaTime), 0);
  282. }
  283. /// <summary>\copydoc Pathfinding::RVO::IAgent::SetCollisionNormal</summary>
  284. public void SetCollisionNormal (Vector3 normal) {
  285. rvoAgent.SetCollisionNormal(To2D(normal));
  286. }
  287. /// <summary>
  288. /// \copydoc Pathfinding::RVO::IAgent::ForceSetVelocity.
  289. /// Deprecated: Set the <see cref="velocity"/> property instead
  290. /// </summary>
  291. [System.Obsolete("Set the 'velocity' property instead")]
  292. public void ForceSetVelocity (Vector3 velocity) {
  293. this.velocity = velocity;
  294. }
  295. /// <summary>
  296. /// Converts a 3D vector to a 2D vector in the movement plane.
  297. /// If movementPlane is XZ it will be projected onto the XZ plane
  298. /// otherwise it will be projected onto the XY plane.
  299. /// </summary>
  300. public Vector2 To2D (Vector3 p) {
  301. float dummy;
  302. return To2D(p, out dummy);
  303. }
  304. /// <summary>
  305. /// Converts a 3D vector to a 2D vector in the movement plane.
  306. /// If movementPlane is XZ it will be projected onto the XZ plane
  307. /// and the elevation coordinate will be the Y coordinate
  308. /// otherwise it will be projected onto the XY plane and elevation
  309. /// will be the Z coordinate.
  310. /// </summary>
  311. public Vector2 To2D (Vector3 p, out float elevation) {
  312. if (movementPlane == MovementPlane.XY) {
  313. elevation = -p.z;
  314. return new Vector2(p.x, p.y);
  315. } else {
  316. elevation = p.y;
  317. return new Vector2(p.x, p.z);
  318. }
  319. }
  320. /// <summary>
  321. /// Converts a 2D vector in the movement plane as well as an elevation to a 3D coordinate.
  322. /// See: To2D
  323. /// See: movementPlane
  324. /// </summary>
  325. public Vector3 To3D (Vector2 p, float elevationCoordinate) {
  326. if (movementPlane == MovementPlane.XY) {
  327. return new Vector3(p.x, p.y, -elevationCoordinate);
  328. } else {
  329. return new Vector3(p.x, elevationCoordinate, p.y);
  330. }
  331. }
  332. void OnDisable () {
  333. if (simulator == null) return;
  334. // Remove the agent from the simulation but keep the reference
  335. // this component might get enabled and then we can simply
  336. // add it to the simulation again
  337. simulator.RemoveAgent(rvoAgent);
  338. }
  339. void OnEnable () {
  340. tr = transform;
  341. ai = GetComponent<IAstarAI>();
  342. var aiBase = ai as AIBase;
  343. // Make sure the AI finds this component
  344. // This is useful if the RVOController was added during runtime.
  345. if (aiBase != null) aiBase.FindComponents();
  346. if (RVOSimulator.active == null) {
  347. Debug.LogError("No RVOSimulator component found in the scene. Please add one.");
  348. enabled = false;
  349. } else {
  350. simulator = RVOSimulator.active.GetSimulator();
  351. // We might already have an rvoAgent instance which was disabled previously
  352. // if so, we can simply add it to the simulation again
  353. if (rvoAgent != null) {
  354. simulator.AddAgent(rvoAgent);
  355. } else {
  356. rvoAgent = simulator.AddAgent(Vector2.zero, 0);
  357. rvoAgent.PreCalculationCallback = UpdateAgentProperties;
  358. }
  359. }
  360. }
  361. protected void UpdateAgentProperties () {
  362. var scale = tr.localScale;
  363. rvoAgent.Radius = Mathf.Max(0.001f, radius * scale.x);
  364. rvoAgent.AgentTimeHorizon = agentTimeHorizon;
  365. rvoAgent.ObstacleTimeHorizon = obstacleTimeHorizon;
  366. rvoAgent.Locked = locked;
  367. rvoAgent.MaxNeighbours = maxNeighbours;
  368. rvoAgent.DebugDraw = debug;
  369. rvoAgent.Layer = layer;
  370. rvoAgent.CollidesWith = collidesWith;
  371. rvoAgent.Priority = priority;
  372. float elevation;
  373. // Use the position from the movement script if one is attached
  374. // as the movement script's position may not be the same as the transform's position
  375. // (in particular if IAstarAI.updatePosition is false).
  376. rvoAgent.Position = To2D(ai != null ? ai.position : tr.position, out elevation);
  377. if (movementPlane == MovementPlane.XZ) {
  378. rvoAgent.Height = height * scale.y;
  379. rvoAgent.ElevationCoordinate = elevation + (center - 0.5f * height) * scale.y;
  380. } else {
  381. rvoAgent.Height = 1;
  382. rvoAgent.ElevationCoordinate = 0;
  383. }
  384. }
  385. /// <summary>
  386. /// Set the target point for the agent to move towards.
  387. /// Similar to the <see cref="Move"/> method but this is more flexible.
  388. /// It is also better to use near the end of the path as when using the Move
  389. /// method the agent does not know where to stop, so it may overshoot the target.
  390. /// When using this method the agent will not overshoot the target.
  391. /// The agent will assume that it will stop when it reaches the target so make sure that
  392. /// you don't place the point too close to the agent if you actually just want to move in a
  393. /// particular direction.
  394. ///
  395. /// The target point is assumed to stay the same until something else is requested (as opposed to being reset every frame).
  396. ///
  397. /// See: Also take a look at the documentation for <see cref="IAgent.SetTarget"/> which has a few more details.
  398. /// See: <see cref="Move"/>
  399. /// </summary>
  400. /// <param name="pos">Point in world space to move towards.</param>
  401. /// <param name="speed">Desired speed in world units per second.</param>
  402. /// <param name="maxSpeed">Maximum speed in world units per second.
  403. /// The agent will use this speed if it is necessary to avoid collisions with other agents.
  404. /// 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).</param>
  405. public void SetTarget (Vector3 pos, float speed, float maxSpeed) {
  406. if (simulator == null) return;
  407. rvoAgent.SetTarget(To2D(pos), speed, maxSpeed);
  408. if (lockWhenNotMoving) {
  409. locked = speed < 0.001f;
  410. }
  411. }
  412. /// <summary>
  413. /// Set the desired velocity for the agent.
  414. /// Note that this is a velocity (units/second), not a movement delta (units/frame).
  415. ///
  416. /// This is assumed to stay the same until something else is requested (as opposed to being reset every frame).
  417. ///
  418. /// Note: In most cases the SetTarget method is better to use.
  419. /// What this will actually do is call SetTarget with (position + velocity).
  420. /// See the note in the documentation for IAgent.SetTarget about the potential
  421. /// issues that this can cause (in particular that it might be hard to get the agent
  422. /// to stop at a precise point).
  423. ///
  424. /// See: <see cref="SetTarget"/>
  425. /// </summary>
  426. public void Move (Vector3 vel) {
  427. if (simulator == null) return;
  428. var velocity2D = To2D(vel);
  429. var speed = velocity2D.magnitude;
  430. rvoAgent.SetTarget(To2D(ai != null ? ai.position : tr.position) + velocity2D, speed, speed);
  431. if (lockWhenNotMoving) {
  432. locked = speed < 0.001f;
  433. }
  434. }
  435. /// <summary>
  436. /// Teleport the agent to a new position.
  437. /// Deprecated: Use transform.position instead, the RVOController can now handle that without any issues.
  438. /// </summary>
  439. [System.Obsolete("Use transform.position instead, the RVOController can now handle that without any issues.")]
  440. public void Teleport (Vector3 pos) {
  441. tr.position = pos;
  442. }
  443. void OnDrawGizmos () {
  444. tr = transform;
  445. // The AI script will draw similar gizmos
  446. if (ai == null) {
  447. var color = AIBase.ShapeGizmoColor * (locked ? 0.5f : 1.0f);
  448. var pos = transform.position;
  449. var scale = tr.localScale;
  450. if (movementPlane == MovementPlane.XY) {
  451. Draw.Gizmos.Cylinder(pos, Vector3.forward, 0, radius * scale.x, color);
  452. } else {
  453. Draw.Gizmos.Cylinder(pos + To3D(Vector2.zero, center - height * 0.5f) * scale.y, To3D(Vector2.zero, 1), height * scale.y, radius * scale.x, color);
  454. }
  455. }
  456. }
  457. protected override int OnUpgradeSerializedData (int version, bool unityThread) {
  458. if (version <= 1) {
  459. if (!unityThread) return -1;
  460. if (transform.localScale.y != 0) centerBackingField /= Mathf.Abs(transform.localScale.y);
  461. if (transform.localScale.y != 0) heightBackingField /= Mathf.Abs(transform.localScale.y);
  462. if (transform.localScale.x != 0) radiusBackingField /= Mathf.Abs(transform.localScale.x);
  463. }
  464. return 2;
  465. }
  466. }
  467. }