Simple Task Manager for AI!

Hello all!

I’ve made a simple Task system for AI that I’d love to share and get some feedback on.

The general idea is this:-

  • Attach the TaskManager.cs script to your AI GameObject.
  • Create a new Task.
  • Add that Task to the TaskManagers list.
  • Done!

To create a new task it has a few requirements (that you can change yourself obviously.) A Task must have a ‘Priority’ so that it can be sorted in the list. Other than that the actual Tasks that you define decide what they need to work correctly, they also contain definitions for when they are finished.

I’ve no idea what I’m going to use this for at the moment but I hope someone else might find it useful =]

TaskManager.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class TaskManager : MonoBehaviour {
 
    public List<Task> TaskList;

    void Awake(){
        TaskList = new List<Task>();
    }
 
    void SortListByPriority(){
        if (TaskList.Count > 0){
            TaskList = TaskList.OrderBy(x => x.Priority).Reverse().ToList();
        }
    }

    void ProcessList(){
        //IF this Task decides it is invalid, then delete it.
        if (TaskList[0].Valid){
            //If its not initialised, intialise it.
            if (TaskList[0].Initialised){
                //If the task isn't finished, execute it.
                if (!TaskList[0].Finished ()){
                    TaskList[0].Execute();                 
                }
                else {
                    Debug.Log ("TaskManager - Task finished, removing!");
                    TaskList.RemoveAt(0);
                }
            }
            else {
                TaskList[0].Initialise();
            }
        }
        else {
            Debug.LogWarning ("TaskManager - Invalid Task detected, removing!");
            TaskList.RemoveAt(0);
        } 
    }

    void Update(){
        //If the TaskList isn't empty and has more than one task sort the list.
        if (TaskList.Count > 1){
            SortListByPriority();
        }
        //If the TaskList isn't empty, Process the list.
        if (TaskList.Count > 0){
            ProcessList();
        }
        else {
            Debug.Log ("TaskManager - TaskList is empty!");
        }
    }

 
}

Task.cs - This has an example of what I might do with the system, it’s just got a real quick and dirty NavMeshAgent usage in there to show how it can work and you can stack Tasks in the list and it’ll handle them, just a nice visual representation.

using UnityEngine;
using System.Collections;
using System;

public abstract class Task {

    //Returns true if the conditions declared by the Task are met.
    public abstract bool Valid { get; }

    //Returns true if the Task has had Initialise() called.
    public bool Initialised { get; set; }

    //Used for sorting Tasks.
    public int Priority { get; set; }
    public int TaskID { get; set; }

    //Reference to the GameObject that is using this Task. Required!
    public GameObject ThisGameObject { get; set; }

    //Constructor.
    public Task(){
        Initialised = false;
    }

    //To be called before Execute or the Task will (probably) fail, depending on whether the task needs to initialise at all.
    public abstract void Initialise();

    //Allows the TaskManager to check if a task has finished, each task defines it's own rules as to what finished means.
    public abstract bool Finished(); 

    //Execute() needs to be called in update of the TaskManager. This will probably hold the majority of the game logic for a task.
    public abstract void Execute();

}

public class MoveTask : Task {

    //The Game World Coordinates for the NavMeshAgent to head towards.
    public Vector3 DestinationPosition { get; set; }
    //Agent reference.
    public NavMeshAgent Agent;

    //Constructor
    public MoveTask(){
        Initialised = false;
    }

    //Called to check if the task has been setup correctly, returns true if everything seems right.
    private bool SetupCheck(){
        if (Agent == null || Priority == 0 || TaskID == 0 || ThisGameObject == null){
            Debug.LogWarning("MoveTask - Task was not setup correctly!");
            return false;
        }
        else {
            return true;
        }
    }

    //This tasks implementation of Valid() simply relays the output of the function SetupCheck() waste of a call right now but maybe useful for later Tasks?
    public override bool Valid{
        get {
            if (!SetupCheck()){
            return false;
            }
            else {
            return true;
            }
        }
    }
    //This Tasks implementation of Initilise() simply sets the NavMeshAgents 'DestinationPosition'.
    public override void Initialise (){
        Agent.SetDestination(DestinationPosition);
        //IMPORTANT that this is now set to true. The TaskManager relies on this variable.
        Initialised = true;
    }

    //Execute() needs to be called in update of the TaskManager. Setting the destination doesn't need to be done in each update,
    //but might need to change later to check for updates to the destination. As it is, this Task will only move to a fixed point.
    public override void Execute(){
        //Debug.Log (Agent.remainingDistance);
    }

    bool HasReachedDestination(){
        //IMPORTANT! If the agent is still calculating its route then leave it alone or program flow will be fucked.
        if (!Agent.pathPending){
            //If the path is NOT complete or totally invalid, return false.
            if (Agent.pathStatus == NavMeshPathStatus.PathInvalid || Agent.pathStatus == NavMeshPathStatus.PathPartial){
                Debug.Log ("MoveTask - Destination Un-Reachable!");
                return false;
            }
            //If the path is complete (valid) and it is within 0.1f of the target then return true.
            if (Agent.pathStatus == NavMeshPathStatus.PathComplete && Agent.remainingDistance < 0.1f){
                Debug.Log ("MoveTask - Destination Reached!");
                return true;
            }
            return false;
        }
        //Otherwise, just return false and allow the agent to continue processing or moving.
        else return false;     
    }

    //This Task defines if it is finished purely by the return of HasReachedDestination(). Note 'HasReachedDe...' currently cannot tell if it hasnt reached
    //its destination because it is blocked or unreachable, possibly need to just set this Task to invalid if the spot is blocked and allow the AI to set
    //itself another Task when needed.
    public override bool Finished(){
        if (HasReachedDestination()){
            return true;
        }
        else return false;
    }

}

    }
}
///////////////////////////////////////////////////////////////
//Purely for copy and pasting!

public class TaskTemplate : Task {

    public override bool Valid {
        get {
            throw new NotImplementedException ();
        }
    }
 
    public override void Initialise ()
    {
        throw new NotImplementedException ();
    }
 
    public override bool Finished (){
        throw new NotImplementedException ();
    }
 
    //Execute() needs to be called in update of the TaskManager.
    public override void Execute(){
     
    }
}

////////////////////////////////////////////////////////////////

What did concern me is the problem that would arise from this priority system. If for example you wanted an AI to first GO somewhere then DO something, you would need to set the priorities correctly. Most likely you would need to have a constant idea or what needs to be most important in EVERY case and fiddle about with priorities just to get the character to walk over and then do something.

So I basically just dumped another task manager inside one of the Task objects, linked a few bits up and now I can create a ‘ComplexTask’ that contains its own set of tasks, but is still treated by the TaskManager as one task! The ComplexTask is constructed simply by making the individual Tasks and adding them to the ComplexTasks list. Then the ComplexTask is added to the TaskManager. The ComplexTask reports it is finished when it is all out of tasks. Simple.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;

public class ComplexTask : Task {

    //List of Tasks to complete.
    public List<Task> ComplexTaskList;
    //Current Task to execute.
    public delegate void CurrentTask();
    //Constructor
    public ComplexTask(){
        ComplexTaskList = new List<Task>();
    }

  
    void ProcessComplexTask(){
        //If this task is not initialised, initialise it.
        if (ComplexTaskList[0].Valid){
            if (ComplexTaskList[0].Initialised){
                if (!ComplexTaskList[0].Finished ()){
                    ComplexTaskList[0].Execute();                                     
                }
                else {
                    Debug.Log ("ComplexTask - Task finished, removing!");
                    ComplexTaskList.RemoveAt(0);
                }
            }
            else {
                ComplexTaskList[0].Initialise();
            }
        }
        else {
            Debug.LogWarning ("ComplexTask - Invalid Task detected, removing!");
            ComplexTaskList.RemoveAt(0);
        } 
    }


    private bool SetupCheck(){
        if (Priority == 0 || TaskID == 0 || ThisGameObject == null){
            Debug.LogWarning("ComplexTask - Task was not setup correctly!");
            return false;
        }
        else {
            return true;
        }
    }

    public override bool Valid {
        get {
            if (!SetupCheck()){
                return false;
            }
            else {
                return true;
            }
        }
    }
 
    public override void Initialise ()
    {
        Initialised = true;
    }
 
    public override bool Finished (){
        if (ComplexTaskList.Count <= 0){
            return true;
        }
        else {
            return false;
        }
    }
 
    //Execute() needs to be called in update of the TaskManager.
    public override void Execute(){
        ProcessComplexTask();
    }
}

Here’s just a test file if you wanna try it out. It just makes a few tasks and adds them.
Testies.cs

using UnityEngine;
using System.Collections;


public class Testies : MonoBehaviour {

    TaskManager taskManager;

    MoveTask mt;
    ComplexTask ct;

    // Use this for initialization

    void Awake(){
        taskManager = GetComponent<TaskManager>();

    }
    void Start () {
        Invoke("NewTask",0.1f);
        Invoke("NewComplexTask",0.5f);

    }
    //Just for testing, constructs a new task after half a second and adds it to the list.
    void NewTask(){
        mt = new MoveTask(){
            Priority = 1,
            TaskID = 001,
            ThisGameObject = gameObject,
            Agent = GetComponent<NavMeshAgent>(),
            DestinationPosition = new Vector3(0,0,0)
        };
        taskManager.TaskList.Add(mt);
        mt = new MoveTask(){
            Priority = -10,
            TaskID = 002,
            ThisGameObject = gameObject,
            Agent = GetComponent<NavMeshAgent>(),
            DestinationPosition = new Vector3(5,0,5)
        };
        taskManager.TaskList.Add(mt);
    }

    void NewComplexTask(){
        //Create new ComplexTask.
        ct = new ComplexTask(){
            Priority = -10,
            TaskID = 002,
            ThisGameObject = gameObject
        };
        //Create new MoveTask.
        mt = new MoveTask(){
            Priority = 1,
            TaskID = 003,
            ThisGameObject = gameObject,
            Agent = GetComponent<NavMeshAgent>(),
            DestinationPosition = new Vector3(-5,0,-5)
        };
        //Add it to the complex task.
        ct.ComplexTaskList.Add(mt);
        //Make another.
        mt = new MoveTask(){
            Priority = 2,
            TaskID = 004,
            ThisGameObject = gameObject,
            Agent = GetComponent<NavMeshAgent>(),
            DestinationPosition = new Vector3(5,0,5)
        };
        //Add that one aswell.
        ct.ComplexTaskList.Add(mt);
        //Finally add the complex task we have just built to the tasklist.
        taskManager.TaskList.Add (ct);
    }

    // Update is called once per frame
    void Update () {
 
    }
}

Feedback would be great, how could I have done this more easily etc?

Also thanks to lordofduct for pointing out a few things!

Halbera.

Nice script, I can see this being quite useful!

The best suggestion that I can offer is that you expand this with some demos etc of it working so that you can showcase it etc, that’ll help you get some feedback of how easy it use to use as well!

Also, “Testies” is usually considered an inappropriate class name :wink:

Jamie

1 Like

Well I mean it’s not exactly designed around anyone else using it, just thought I would get it out there in case anyone has any ideas for me or if they might find it useful =]

Hlabera

I might actually use an adaptation of this… Will test for fun! :smile:

Cool! Ive got a much better version in the works. Ill post it after ive actually written it. It will include timed tasks, tasks with time to live in the manager, tasks that know if they are active, and a few other useful functions such as a Task.Reset() function that will be necessary if you want to reorder the tasks by priority effectivly. A few bits ive already added are things like OnTaskEnd() and OnTaskStart() in keeping with the MonoBehaviour ideas.

OnTaskEnd could be useful for quite a few gameplay concepts i should imagine but im aware it might function similarly to a default deconstructor.

Im wondering if a task history of sorts might be an interesting thing to include also.

Dunno, just kind of want to cover all the bases. I should really try and use it in a game and see what else it needs.

Halbera.

Wow, that task manager looks really useful.
Did you ever get around to make this more complex version of it you last posted about?
My implementation looked quite similar but I tied in a finite state machine to actually do most of the checks for my tasks such as path validity and other checks as well as executing the tasks. Still funny to see how similar both our approaches were.