Search Unity

[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]
     
  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. shotoutgames

    shotoutgames

    Joined:
    Dec 29, 2013
    Posts:
    290
    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.
     
    njyxster likes this.
  7. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Posts:
    1,353
    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:
    6,362
    @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!
     
    phobos2077 likes this.
  9. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Posts:
    1,353
    @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
    NeatWolf likes this.
  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.
     
    Hexahwave, clarHandsome and kavanavak like this.
  11. unitynoob24

    unitynoob24

    Joined:
    Dec 27, 2014
    Posts:
    398
    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.  
     
    ProvingUser likes this.
  12. kavanavak

    kavanavak

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

    RyanNguyen

    Joined:
    Jul 20, 2016
    Posts:
    8
    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. massey_digital

    massey_digital

    Joined:
    Apr 28, 2013
    Posts:
    94
    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.     }
     
  16. HavanaBanana

    HavanaBanana

    Joined:
    Jul 3, 2017
    Posts:
    4
    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:
    725
    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:
    122
    You might try this:
    https://docs.unity3d.com/ScriptReference/AI.NavMesh.Raycast.html
     
  20. roshanshaffeq

    roshanshaffeq

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

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    129
    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.             }
     
    JBR-games, paulbuck86 and twobob like this.
  22. tarahugger

    tarahugger

    Joined:
    Jul 18, 2014
    Posts:
    129
    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
    Katana_Lan, SimDevs, EduDude and 8 others like this.
  23. Kitty001

    Kitty001

    Joined:
    Oct 1, 2017
    Posts:
    3
    Thanks so much, solved a major issue for me in no time!

    One thing to note, need to add a reference to using System.Collections.Generic in the new system
     
    JBR-games likes this.
  24. astracat111

    astracat111

    Joined:
    Sep 21, 2016
    Posts:
    725


    One easy way to do it is to have your guy, and then he has waypoints as his children. You have a loop (not a while loop but like timer based in your update method):

    Code (CSharp):
    1. public class Guy {
    2.  
    3. public enum State { WaitForNextAction, Decide }
    4. public State state;
    5. public float NextActionTimer = 240f;
    6. public int NextWaypointToGoToChildIndex = 1;
    7. public int NumberOfRndWaypoints = 3;
    8.  
    9. private void GotoNextState() {
    10.    state += 1;
    11. }
    12. private void ResetStateMachine() {
    13.    state = 0;
    14. }
    15.  
    16. void Update() {
    17.    switch (state) {
    18.       case State.WaitForNextAction:
    19.        NextActionTimer = NextActionTimer - 1f * (Time.deltaTime * 60f); //< - I think I got this part right.
    20.  
    21.         if (NextActionTimer <= 0f) {
    22.            GotoNextState();
    23.         }
    24.  
    25.       break;
    26.  
    27.       case State.Decide:
    28.          NextWaypointToGoToChildIndex = UnityEngine.Random(1,NumberOfRndWaypoints);
    29.          //Find the child by index number here and do navMeshAgent.SetDestination(gameObject);
    30.      
    31.         ResetStateMachine();
    32.       break;
    33.  
    34.    }
    35. }
    36.  
    37. }

    So then, to expand on this, say you don't want your guys to just follow the waypoints in the triangle formation. Well, in State.Decide you just scramble the position of the waypoints.
     
    legolegoxan77 and JBR-games like this.
  25. mrCharli3

    mrCharli3

    Joined:
    Mar 22, 2017
    Posts:
    976
  26. legolegoxan77

    legolegoxan77

    Joined:
    Jan 16, 2020
    Posts:
    2
    I'm still learning how to code C#, and your code came in clutch!

    Thanks for the help!!!
     
  27. legolegoxan77

    legolegoxan77

    Joined:
    Jan 16, 2020
    Posts:
    2
    OMG thanks!!
     
  28. Kitty001

    Kitty001

    Joined:
    Oct 1, 2017
    Posts:
    3
    This is really fantastic! Thanks for ending a long search. Now that I have this random walk I want to extend, and can't quite figure out how. So I want the random point selected to bias towards the agents "forward direction". So, instead of selecting a random point in a a set radius, I want it to select a random direction in the forward face 90 degree arc. This will result in a random walk that biases towards going straight with a little bit of error, resulting in a wobbling forwrd walk (like a drunk).

    In pseudo code what I want is something like this:

    Code (CSharp):
    1. Wandering
    2. {
    3.         timer += Time.deltaTime;
    4.    
    5.         if (timer >= wanderTimer)
    6.         {
    7.             this.transform.LookAt(this.transform.forward);
    8.             Vector3 newPos = RandomNavSphere(transform.position,
    9. wanderRadius [B]Only in the forward 180 degrees of the sphere[/B], -1);
    10.             agent.SetDestination(newPos);
    11.        
    12.             timer = 0;
    13.         }
    But I can't seem to nail down how to get only the forward 180 degrees of the insideUnitSphere.

    I have tried adding this check, but it doesn't bias towards the center of the forward arc, but samples from the whole area:

    Code (CSharp):
    1. bool isFront = Vector3.Dot(Vector3.forward, transform.InverseTransformPoint(transform.position)) > 0;
    2.             if (isFront == false)
    3.             {
    4.  
    5.                 agent.SetDestination(newPos);
    6.             }
    Any ideas?
     
    Last edited: Dec 14, 2020
  29. Blurpytu2

    Blurpytu2

    Joined:
    Jun 27, 2021
    Posts:
    2
    im lost
     
    CAWMUN_GAMES likes this.
  30. ChrisKurhan

    ChrisKurhan

    Joined:
    Dec 28, 2015
    Posts:
    268
    In the AI Series I show how to implement this "wander" mechanic in the "idle" state in the state machine video you can find here:
    that may be easier to follow along with than the forum posts in this thread.

    Some people have mentioned waypoints, which are also done in that video if that's what you're looking for.
     
    Katana_Lan, MuntyMcFly and WakeZero like this.