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.