1

I'm working on an AI script in Unity 5 mostly constructed of coroutines to move around an AI object in my game. It actually works pretty well and is frame independent that way. I'm trying to avoid cluttering the Update function of the class.

However, I'm trying to create a function called wander, where the AI hangs around and randomly chooses a couple of Vector3s in the area of the waypoint and travel to each of them. After that the AI should go to the next waypoint and do the same thing into infinity basically. Collision detection and all that sort is for later parts. I want to get the navigating part fixed first.

void Start(){
    agent = GetComponent<NavMeshAgent> ();

    collisionRange = GetComponent<CapsuleCollider> ();
    collisionRange.radius = detectionRadius;

    if (waypoints.Length > 0) {
        StartCoroutine (patrol ());
    } else {
        StartCoroutine (idle (idleTime));
    }
}

IEnumerator patrol(){
    agent.SetDestination(waypoints[waypointIndex].position);

    Debug.Log ("Patrol started, moving to " + agent.destination);
    while (agent.pathPending) {
        Debug.Log ("not having path");
        yield return null;
    }   

    Debug.Log ("I have a path, distance is " + agent.remainingDistance);

    while (float.Epsilon < agent.remainingDistance) {
        //Debug.Log ("Moving...");
        yield return null;
    }

    StartCoroutine (nextWaypoint ());
}

IEnumerator idle(int time){
    Debug.Log ("Idleing for "+ time + " seconds");
    agent.Stop ();

    yield return new WaitForSeconds(time);

    if(waypoints.Length > 2){
        agent.Resume ();
        StartCoroutine(patrol());
    }
}

IEnumerator wander(){
    agent.Stop ();
    Debug.Log ("Going to look around here for a while.");

    Vector3[] points = new Vector3[wanderPoints];

    for (int i = 0; i < wanderPoints; i++) {
        agent.Stop ();
        Vector3 point = Random.insideUnitSphere * wanderRadius;
        point.y = transform.position.y;

        points [i] = point;
    }

    agent.ResetPath ();
    agent.SetDestination(points[0]);
    agent.Resume ();

    Debug.Log ("point: " + points [0]);
    Debug.Log ("Destination: " + agent.destination);

    while (float.Epsilon < agent.remainingDistance) {
        Debug.Log ("Moving...");
        yield return null;
    }

    //StartCoroutine(patrol());
    yield return null;

}

IEnumerator nextWaypoint(){
    Debug.Log ("Arrived at my waypoint at " + transform.position);

    if (waypointIndex < waypoints.Length -1) {
        waypointIndex +=1;
    } else {
        waypointIndex = 0;
    }

    StartCoroutine(wander ());
    yield return null;
}

If I swap the wander function with the idle function in nextWaypoint, everything works as expected, but this bit will never work:

agent.ResetPath ();
agent.SetDestination(points[0]);
agent.Resume ();

Debug.Log ("point: " + points [0]);
Debug.Log ("Destination: " + agent.destination);

This is a bit of test code (manually setting only 1 position to go point[0], but it never will travel to that destination. SetDestination will never get updated to the points I want to set them. I've tried calculating paths (NavMeshPath) up front and everything but the destination path will not change or reset weirdly enough. I've also had the while (float.Epsilon < agent.remainingDistance) loop in the wander function as well, but with no luck since it will remain in that loop forever since there's no path to go to.

I might be missing something here, or my logic is wrong in this case. Hopefully someone could give me a little push or maybe some extra debugging options, because I have no idea why the destination doesn't get updated in my wander function.

user3071284
  • 6,955
  • 6
  • 43
  • 57
Jurgen
  • 311
  • 3
  • 10
  • I used coroutines for AI when I first designed it. There were some things that were hard to debug, so I went with an interface-based state machine similar to what's in this tutorial: https://unity3d.com/learn/tutorials/modules/beginner/live-training-archive/state-machine-interface – user3071284 Nov 16 '15 at 14:51

1 Answers1

1

Consider using triggers on waypoints instead, as this will be even less taxing for the system. Below is an example that will allow you to have a variable number of waypoints easily, by getting all WayPoint type children from a parent transform. The only Coroutine that is used is for idle as you said you didn't want your Update function cluttered.

I've also exposed a couple of additional variables, including a min and max idle time, and a patrol chance, which should be set to a value less than or equal to 1, representing a percentage chance of patrolling vs idle. You can also choose a min and max number of patrol points for the agent to navigate to.

In RandomActivity you can also see that there is error handling for infinitely idle (no WayPoint children under parent). The agent will also not include the current Waypoint in it's list of points to navigate to, and will not navigate to a point already navigated to during it's current patrol (every time an element is added to m_targets, it's removed from m_availableTargets).

AgentController.cs

[RequireComponent(typeof(NavMeshAgent))]
public class AgentController : MonoBehaviour
{
    [SerializeField]
    private Transform       m_waypointParent;
    [SerializeField]
    private float           m_minIdle;
    [SerializeField]
    private float           m_maxIdle;
    [SerializeField]
    private float           m_patrolChance;
    [SerializeField]
    private int             m_minPatrolPoints;
    [SerializeField]
    private int             m_maxPatrolPoints;

    private Waypoint[]      m_waypoints;
    private List<Waypoint>  m_availableTargets;
    private List<Waypoint>  m_targets;
    private Waypoint        m_tempWaypoint;
    private int             m_currentTargetIndex;
    private NavMeshAgent    m_navMeshAgent;

    public void Start()
    {
        m_waypoints     = m_waypointParent.GetComponentsInChildren<Waypoint>();
        m_targets       = new List<Waypoint>();
        m_navMeshAgent  = GetComponent<NavMeshAgent>();
        RandomActivity();
    }

    private void RandomActivity()
    {
        if (m_waypoints.Length == 0)
        {
            Debug.Log("Enemy will idle forever (no waypoints found)");
            StartCoroutine(Idle(Random.Range(m_minIdle, m_maxIdle)));
            return;
        }

        if(Random.Range(0f, 1f) <= m_patrolChance)
        {
            //Available waypoints
            m_availableTargets = new List<Waypoint>(m_waypoints);

            //Remove currentpoint
            if(m_targets.Count > 0)
                m_availableTargets.Remove(m_targets[m_targets.Count - 1]);

            //Reset list
            m_targets.Clear();
            m_currentTargetIndex = -1;

            //Add patrol points
            for (int i = 0; i < Random.Range(m_minPatrolPoints, m_maxPatrolPoints + 1); i++)
            {
                m_tempWaypoint = m_availableTargets[Random.Range(0, m_availableTargets.Count)];
                m_targets.Add(m_tempWaypoint);
                m_availableTargets.Remove(m_tempWaypoint);
            }

            NextWaypoint(null);
        }
        else
            StartCoroutine(Idle(Random.Range(m_minIdle, m_maxIdle)));
    }

    public void NextWaypoint(Waypoint p_waypoint)
    {
        //Collided with current waypoint target?
        if ((m_currentTargetIndex == -1) || (p_waypoint == m_targets[m_currentTargetIndex]))
        {
            m_currentTargetIndex++;

            if (m_currentTargetIndex == m_targets.Count)
                RandomActivity();
            else
            {
                Debug.Log("Target: " + (m_currentTargetIndex + 1) + "/" + m_targets.Count + " (" + m_targets[m_currentTargetIndex].transform.position + ")");
                m_navMeshAgent.SetDestination(m_targets[m_currentTargetIndex].transform.position);
            }
        }
    }

    private IEnumerator Idle(float p_time)
    {
        Debug.Log("Idling for " + p_time + "s");
        yield return new WaitForSeconds(p_time);
        RandomActivity();
    }
}

Note that for this to work, I have created a tag called Enemy so as to easily differentiate between enemies and any other tiggers in your game.

WayPoint.cs

[RequireComponent(typeof(BoxCollider))]
public class Waypoint : MonoBehaviour
{
    public void OnTriggerEnter(Collider p_collider)
    {
        if (p_collider.tag == "Enemy")
            p_collider.GetComponent<AgentController>().NextWaypoint(this);
    }
}

I know this is an old post but hope this helps someone.

MrDiVolo
  • 151
  • 5