Search Unity

  1. Check out the Unite LA keynote for updates on the Visual Effect Editor, the FPS Sample, ECS, Unity for Film and more! Watch it now!
    Dismiss Notice
  2. The Unity Pro & Visual Studio Professional Bundle gives you the tools you need to develop faster & collaborate more efficiently. Learn more.
    Dismiss Notice
  3. Improved Prefab workflow (includes Nested Prefabs!), 2D isometric Tilemap and more! Get the 2018.3 Beta now.
    Dismiss Notice
  4. Improve your Unity skills with a certified instructor in a private, interactive classroom. Watch the overview now.
    Dismiss Notice
  5. Want to see the most recent patch releases? Take a peek at the patch release page.
    Dismiss Notice

[SOLVED] Random "Wander" AI using NavMesh

Discussion in 'Navigation' started by Cnc96, May 24, 2015.

  1. Cnc96

    Cnc96

    Joined:
    Dec 17, 2013
    Posts:
    57
    Hi all,
    I am trying to create an AI system for a topdown shooter and I'd like some of the enemies to wander randomly instead of following the player. I was wandering if I could somehow use Random.insideUnitSphere to get a point relatively close to the enemy every so often and have him travel to that point using the NavMeshAgent.SetDestination?

    If this is the incorrect way of doing it, does anyone have any suggestions on how else I can get a wandering AI?
     
    Last edited: May 25, 2015
    kavanavak and TUNG_LP like this.
  2. MysterySoftware

    MysterySoftware

    Joined:
    Sep 18, 2014
    Posts:
    46
    I would go with your first instinct of using Random.insideUnitSphere. It's what I did anyways.. :)

    Code (CSharp):
    1. public static Vector3 RandomNavSphere (Vector3 origin, float distance, int layermask) {
    2.             Vector3 randomDirection = UnityEngine.Random.insideUnitSphere * distance;
    3.            
    4.             randomDirection += origin;
    5.            
    6.             NavMeshHit navHit;
    7.            
    8.             NavMesh.SamplePosition (randomDirection, out navHit, distance, layermask);
    9.            
    10.             return navHit.position;
    11.         }
    You'll need an origin (your AI agent), a maximum distance, and a layer mask (I'd recommend -1 meaning all layers) and the method is going to return a random point on the NavMesh within a given distance to the origin.
     
  3. Cnc96

    Cnc96

    Joined:
    Dec 17, 2013
    Posts:
    57
    I thought it would as it gives that semi-random look to the wandering and the code you shared helped a lot :D

    With a few additions to the code to make it travel for a few seconds and then select a new target, it's wandering around awesomely!
    Below will be the code so if anyone needs help with a similar topic, then they can see how I've done it! :D

    Code (CSharp):
    1. [code=CSharp]using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class WanderingAI : MonoBehaviour {
    5.  
    6.     public float wanderRadius;
    7.     public float wanderTimer;
    8.  
    9.     private Transform target;
    10.     private NavMeshAgent agent;
    11.     private float timer;
    12.  
    13.     // Use this for initialization
    14.     void OnEnable () {
    15.         agent = GetComponent<NavMeshAgent> ();
    16.         timer = wanderTimer;
    17.     }
    18.  
    19.     // Update is called once per frame
    20.     void Update () {
    21.         timer += Time.deltaTime;
    22.  
    23.         if (timer >= wanderTimer) {
    24.             Vector3 newPos = RandomNavSphere(transform.position, wanderRadius, -1);
    25.             agent.SetDestination(newPos);
    26.             timer = 0;
    27.         }
    28.     }
    29.  
    30.     public static Vector3 RandomNavSphere(Vector3 origin, float dist, int layermask) {
    31.         Vector3 randDirection = Random.insideUnitSphere * dist;
    32.  
    33.         randDirection += origin;
    34.  
    35.         NavMeshHit navHit;
    36.  
    37.         NavMesh.SamplePosition (randDirection, out navHit, dist, layermask);
    38.  
    39.         return navHit.position;
    40.     }
    41. }
    42.  
    [/code]
     
    Rewaken, waleedmm, HeyBishop and 10 others like this.
  4. tsdavs

    tsdavs

    Joined:
    Dec 12, 2015
    Posts:
    1
    You legend! I've been struggling with this for a whole week! Finally found you in the dankest corners of the Internet :)
     
    MysterySoftware likes this.
  5. marvalshot

    marvalshot

    Joined:
    Dec 29, 2013
    Posts:
    133
    Why is Navemesh.SamplePosition needed and not just the random point generated??? Thanks

    I think I know now???
    SamplePosition is used to detect for barriers and if blocked change point to barrier??
     
    Last edited: Mar 9, 2016
  6. MysterySoftware

    MysterySoftware

    Joined:
    Sep 18, 2014
    Posts:
    46
    The random point generated will be a random point in 3d space. So it might not be accessible via the navmesh, which is why we need to get the closest point on the navmesh to the random point in 3d space.
     
  7. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Posts:
    1,289
    Hi,

    I know your problem is solved. But I've noticed you are using a MonoBehaviour to define the wandering behaviour of your AI (WanderingAI). I guess that your are writing a new class for each type of behaviour.

    Writing AI exclusively in C# can quickly become harder as your AI grows in complexity. Particularly when dealing with time dependent logic for implement long running actions, and it becomes even harder when your actions are triggered or interrupted by some conditions. Behaviour Tree is the ideal tool for defining such long running actions and their logic in clear and intuitive way and keeps your code organized and maintainable, therefore less likely to contain bugs.

    You might be interested by Panda BT to implement your AI, it's a script based Behaviour Tree engine:

    http://www.pandabehaviour.com

    Let me know if you have any question about using this tool.
     
  8. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    1,999
    @ericbegue I just tried out PandaBT and really like it but notice a lot of GC spikes in HasLOS, didn't dig further but one easy way to prevent hammering expensive routines is to stagger the updates of some of the trees, or part of the tree.
    One thing I am curious about is AI pooling, your shooter example runs each AI on each agent.
    PS: hijacking thread ftw!
     
  9. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Posts:
    1,289
    @laurentlavigne Thank you for trying Panda BT. Great that you like it!

    The examples are not GC optimized, they are intended to illustrate usages of Panda BT with straigth forward code with focus on readability. However, since version 1.2.3 (published today) the core engine is GC optimized and allocates 0B after initialization. But it's always possible to write tasks that are not GC optimized. If you use the tasks from the examples, you have to take care of the GC optimization if you are concerned.

    The cpu time spent in executing the tree itself is negligeable, the real work is done by the tasks. So they are where the focus should be put on.

    In the Shooter example, each AI agent executes is own BT. It has to be that way since each agent has its own data/memory that can't be shared (expl: each agent has its own position), so each agent has its own set of variables, which also mean that each agent must have its own copy of the BT in a specific state.

    I would provide some stats, if there are particular performance issues you are concerned about?
     
    Last edited: Apr 5, 2016
  10. thegamerguynz

    thegamerguynz

    Joined:
    Apr 1, 2016
    Posts:
    20
    For users of latest unity don't forget to put "using UnityEngine.AI;" in the class defining stage.
     
    kavanavak likes this.
  11. unitynoob24

    unitynoob24

    Joined:
    Dec 27, 2014
    Posts:
    292
    If anyone is looking for this in UnityScript/JS I just converted it! Cheers! Thanks @Cnc96

    Code (csharp):
    1.  
    2. #pragma strict
    3. import UnityEngine;
    4. import System.Collections;
    5. import UnityEngine.AI;
    6.  
    7.  
    8. //Public Variables
    9. public var wanderRadius : float;
    10. public var wanderTimer : float;
    11.  
    12. //Private Variables
    13. private var target : Transform;
    14. private var agent : NavMeshAgent;
    15. private var timer : float;
    16.  
    17. function Start ()
    18. {  
    19.    agent = GetComponent(NavMeshAgent);
    20.    timer = wanderTimer;
    21. }
    22.  
    23. function Update()
    24. {
    25.    timer += Time.deltaTime;
    26.    
    27.    if(timer >= wanderTimer)
    28.    {
    29.        var newPos : Vector3 = RandomNavSphere(transform.position, wanderRadius, -1);
    30.        agent.SetDestination(newPos);
    31.        timer = 0;
    32.    }
    33. }
    34.  
    35. function RandomNavSphere(origin : Vector3, dist : float, layermask : int):Vector3
    36. {
    37.    var randDirection : Vector3 = Random.insideUnitSphere * dist;
    38.    
    39.    randDirection += origin;
    40.    
    41.    var navHit : NavMeshHit;
    42.    
    43.    NavMesh.SamplePosition(randDirection, navHit, dist, layermask);
    44.    
    45.    return navHit.position;
    46. }
    47.  
    48.  
     
  12. kavanavak

    kavanavak

    Joined:
    Sep 30, 2015
    Posts:
    27
    This is solid @Cnc96 thanks for sharing your code.
     
  13. RyanNguyen

    RyanNguyen

    Joined:
    Jul 20, 2016
    Posts:
    6
    Hi!
    Thanks @Cnc96 for sharing your code. It's very helpful for the beginner.

    I have tried to develop more features for Wander such as using Coroutine to check whether object reached to a destination. But if RandomNavSphere creates a random position that cannot reach and can only get close to the destination (or an internal position), then object can only stand and wait.

    How to make this wait a little time and randomly move to the next position?

    I've tried NavMeshPathStatus.PathInvalid and NavMeshPathStatus.PathPartial but it still not work.
    Sorry, I'm a beginner and I lost 3 day(even today) for this problem.
    Thanks!.

    Code (CSharp):
    1.     IEnumerator GoToDestination()
    2.     {
    3.         nav.SetDestination(destination);
    4.         while (nav.pathPending)
    5.             yield return null;
    6.         float remain = nav.remainingDistance;
    7.  
    8.         while (remain == Mathf.Infinity || remain - nav.stoppingDistance > float.Epsilon || nav.pathStatus != NavMeshPathStatus.PathComplete)
    9.         {
    10.             if(nav.pathStatus != NavMeshPathStatus.PathInvalid)
    11.             {
    12.                    // it don't send event after nav agent can only get close
    13.             }
    14.             if(nav.pathStatus != NavMeshPathStatus.PathInvalid)
    15.             {
    16.                    // not work
    17.             }
    18.             remain = nav.remainingDistance;
    19.             yield return null;
    20.         }
    21.         isMoving = false;
    22.     }
     
  14. Acem

    Acem

    Joined:
    Apr 28, 2013
    Posts:
    93
    Ryan,

    For mine I do a velocity check to see if the player's velocity has stopped. You can use agent.velocity.magniute to get the velocity of a nav mesh agent.

    I have one issue with the Unit Sphere solution though. If your radius is much larger than the area you are trying to wander in (i.e. if you want multiple floors), the probability that your agent moves to the sides is higher because the sides of the room are more likely than the rest of the room. This is because if the sphere is larger than the room, the closest point for anything outside of the room is the sides/corners of the room.

    Does anybody have a solution to the above?

    -Drew
     
  15. ManuelHuber

    ManuelHuber

    Joined:
    Jul 3, 2017
    Posts:
    3
    @Acem
    Let's say you have 3 floors and want him to wander randomly between them and inside of them. Here's how I'd do it:
    The "wander" script has a public list of objects for wander spots. Create a empty game object inside each room and asign it to the script. Then inside the script you chose one of the spots randomly and then do the whole unit sphere solution around this point.

    I expanded the code of the previous guys:

    Code (CSharp):
    1.     public class WanderSpot {
    2.         public GameObject Center;
    3.         public float WanderRadius;
    4.     }
    5.  
    6.     public class WanderScript {
    7.         public List<WanderSpot> Spots;
    8.  
    9.         private NavMeshAgent agent;
    10.  
    11.         private void Update() {
    12.             if (!hasReachedDestination()) return;
    13.             var newSpot = Spots[Random.Range(0, Spots.Count)];
    14.             agent.SetDestination(RandomPosition(newSpot));
    15.         }
    16.  
    17.         private bool hasReachedDestination() {
    18.             return agent.remainingDistance <= agent.stoppingDistance;
    19.         }
    20.  
    21.         private Vector3 RandomPosition(WanderSpot spot) {
    22.             var randDirection = Random.insideUnitSphere * spot.WanderRadius;
    23.  
    24.             randDirection += spot.Center.transform.position;
    25.  
    26.             NavMeshHit navHit;
    27.  
    28.             NavMesh.SamplePosition(randDirection, out navHit, spot.WanderRadius, -1);
    29.  
    30.             return navHit.position;
    31.         }
    32.     }
     
    bojko108 and christougher like this.
  16. HavanaBanana

    HavanaBanana

    Joined:
    Jul 3, 2017
    Posts:
    1
    Is out there another solution for multiple floors? I think a list of objects attached to every enemy is a huge performance limitation or am I wrong?
     
  17. astracat111

    astracat111

    Joined:
    Sep 21, 2016
    Posts:
    395
    I did something a little bit different. I simply attached 3 empty game objects to the base object with the navmesh. I made these 3 game objects children and just had the script attached to the base object switch randomly between these and at random delays.

    Then, when the player gets within a certain distance of this object, it switches state to thinking about how to respond to the player's actions.
     
  18. nazalas

    nazalas

    Joined:
    Dec 2, 2012
    Posts:
    3
    I am doing something very similar to this but I have one concern. Lets say that your agent is wondering with this script and he is very close to a wall. On the other side of the wall is another navigable area that he can actually get to but you don't want him wondering all the way around the map to get to that spot. Whats the best way to limit this? Can you check the distance that he would have to travel? Should you just do a raycast from origin to destination to make sure its within line of sight?
     
  19. Multithreaded_Games

    Multithreaded_Games

    Joined:
    Jul 6, 2015
    Posts:
    117
    You might try this:
    https://docs.unity3d.com/ScriptReference/AI.NavMesh.Raycast.html
     
  20. roshanshaffeq

    roshanshaffeq

    Joined:
    Apr 9, 2018
    Posts:
    2
    thank you so much for the script bro
     
  21. tarahugger

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    54
    I'll throw in a variation, i didn't want to have to pre-define regions, so this just picks a random spot anywhere on the mesh within range.

    Code (CSharp):
    1. public static class NavMeshExtensions
    2. {
    3.     public static Vector3 RandomPosition(this NavMeshAgent agent, float radius)
    4.     {
    5.         var randDirection = Random.insideUnitSphere * radius;
    6.         randDirection += agent.transform.position;
    7.         NavMeshHit navHit;
    8.         NavMesh.SamplePosition(randDirection, out navHit, radius, -1);
    9.         return navHit.position;
    10.     }
    11. }
    and so...

    Code (CSharp):
    1.             if (IsWanderingAllowed)
    2.             {
    3.                 _agent.SetDestination(_agent.RandomPosition(20f));              
    4.             }
     
    twobob likes this.
  22. tarahugger

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    54
    So I've had a bit more of a play around and have something that might be of interest.



    The call to NavMesh.SamplePosition (with a high radius) is taking around 0.0090ms on my machine. Caching the positions takes about 1ms then further position requests would be around 0.0009ms.


    Code (CSharp):
    1. public static class Wanderer
    2. {
    3.     private static List<Vector3> _validPositions = new List<Vector3>();
    4.     private static List<Vector3> _gridPositions;
    5.     private const int BoxSize = 2;
    6.  
    7.     public static void UpdatePositions()
    8.     {
    9.         NavMeshHit hit;
    10.         var validPositions = new List<Vector3>();
    11.         var gridPositions = new List<Vector3>();
    12.  
    13.         var sw = Stopwatch.StartNew();
    14.         foreach (var surface in NavMeshSurface.activeSurfaces)
    15.         {
    16.             var min = surface.navMeshData.sourceBounds.min + surface.navMeshData.position;
    17.             var max = surface.navMeshData.sourceBounds.max + surface.navMeshData.position;
    18.             var surfaceCenterY = surface.navMeshData.sourceBounds.center.y;
    19.  
    20.             for (var x = min.x; x <= max.x; x = x + BoxSize)
    21.             {
    22.                 for (var z = min.z; z <= max.z; z = z + BoxSize)
    23.                 {                  
    24.                     var pos = new Vector3(x, surfaceCenterY, z);
    25.  
    26.                     //if (NavMesh.FindClosestEdge(pos, out hit, -1))
    27.                     if (NavMesh.SamplePosition(pos, out hit, 1f, -1))
    28.                     {
    29.                         validPositions.Add(hit.position);
    30.                     }
    31.                     gridPositions.Add(pos);
    32.                 }
    33.             }      
    34.  
    35.         }
    36.         sw.Stop();
    37.         Debug.Log($"UpdatePositions took {sw.Elapsed.TotalMilliseconds:N4}ms");
    38.         _validPositions = validPositions;
    39.         _gridPositions = gridPositions;
    40.     }
    41.  
    42.     public static void DrawGizmos()
    43.     {      
    44.         Gizmos.color = Color.blue;
    45.  
    46.         if (_validPositions != null)
    47.         {
    48.             foreach (var pos in _validPositions)
    49.             {
    50.                 Gizmos.DrawSphere(pos, 0.3f);
    51.             }
    52.         }
    53.  
    54.         if (_gridPositions != null)
    55.         {
    56.             Gizmos.color = Color.gray;
    57.             foreach (var pos in _gridPositions)
    58.             {
    59.                 var halfBox = BoxSize;
    60.                 Gizmos.DrawSphere(pos, 0.1f);
    61.                 var a = pos - new Vector3(-halfBox, 0, -halfBox);
    62.                 var b = pos - new Vector3(-halfBox, 0, halfBox);
    63.                 var c = pos - new Vector3(halfBox, 0, halfBox);
    64.                 var d = pos - new Vector3(halfBox, 0, -halfBox);
    65.                 Gizmos.DrawLine(a, b);
    66.                 Gizmos.DrawLine(b, c);
    67.                 Gizmos.DrawLine(c, d);
    68.                 Gizmos.DrawLine(d, a);
    69.             }
    70.         }
    71.     }
    72.  
    73.     public static Vector3 RandomPosition(Vector3 origin, float radius)
    74.     {
    75.         var randDirection = UnityEngine.Random.insideUnitSphere * radius;
    76.         randDirection += origin;
    77.         NavMeshHit navHit;
    78.         var t1 = Stopwatch.StartNew();
    79.         NavMesh.SamplePosition(randDirection, out navHit, radius, -1);
    80.         t1.Stop();
    81.         Debug.Log($"RandomPosition took {t1.Elapsed.TotalMilliseconds:N4}ms");    
    82.         return navHit.position;
    83.     }
    84.  
    85.     public static Vector3 RandomPosition()
    86.     {
    87.         var t1 = Stopwatch.StartNew();
    88.         var position = _validPositions.ElementAtOrDefault(UnityEngine.Random.Range(0, _validPositions.Count));
    89.         t1.Stop();
    90.         Debug.Log($"RandomPosition2 took {t1.Elapsed.TotalMilliseconds:N4}ms");
    91.         return position;
    92.     }
    93.  
    94. }
     
    Last edited: Jun 13, 2018
    twobob likes this.