Sponsored By

Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs.

The Unity3d Task System: An AI Controller

I share a system I developed to control the AI in my unnamed mobile game. The system and examples are designed to work in Unity3d but I explain the idea in hopes that everyone can get grasp it an potentially use it in their games.

Mike Pellecchia, Blogger

April 22, 2015

12 Min Read

Today I am going to share the system I use to control the AI of the characters in my current game. The focus is more on the ideas and less on the implementation specifics. I do this because what I am presenting is more idea based that a strict algorithm. Plus once you grok the idea you can much more easily tweak it to your needs..

The Need:

A system that I can use to control the AI for all my little runners. The first use of it will be for the Mascot character seen in the background but I need a system that I can easily extend to other characters seen in the game. Also, I want to be able to use the nice features Unity gives me by using MonoBehaviors and property setting via the editor.

The Evolution of The System:

I bet by now most of you are already screaming FINITE STATE MACHINE at your screen. Trust me that was my first thought as well. I even started to implement it before I found it wasn’t going to work for what I wanted. I walked away from a FSM was because the states were very restrictive. If I wanted a character to do something even slightly different I would have to rewrite 1 or more states just for that character. Thus came to mind the idea which I have named the Task Machine. Even better it can still be using along side a FSM.

The Code:

The code is broken down into a collection of single objective “Tasks” and one central piece I have affectionately called “The Brain”. Tasks are designed to be easily reused on any character. While The Brain is specific to the character and controls how it behaves by activating Tasks.

Tasks:

Each Task inherits from MonoBehavior so things about the task can be configured from the editor. This also allows you to add any combination of tasks to a character. Each of task has a single entry point ex. RunTo(Vector2). As the Tast starts it enables itself and then disables as it finishes its task. This allows us to control the default Method calls like Update. Where applicable the task also fires an event when it is finished.

Side Note: All of these tasks could extend from a Task base  class and Task could inherit from MonoBehavior. This would keep the system more abstract But for my system I decided to keep it simple. Since I am the only coder on this project I can code less defensively to gain a bit of speed.

Here is an example of my “MoveTo” task:

using UnityEngine;
using System.Collections;
using System;

public class NonPhysicsRunner : MonoBehaviour
{
    public GameObject AnimationTarget = null;
    public event EventHandler OnMoveCompleted;

    private Animator animator;
    private Vector3 targetPosition;
    private float speed;

    void Awake()
    {
        animator = this.AnimationTarget.GetComponent<Animator>();
    }

    void OnEnable()
    {
        animator.SetFloat("Speed", speed);
    }

    void OnDisable()
    {
        speed = 0;
        targetPosition = Vector3.zero;
        animator.SetFloat("Speed", 0);
    }

    public void RunTo(Vector3 target, float s)
    {
        speed = s;
        targetPosition = target;

        this.enabled = true;

        Hashtable runTween = new Hashtable();
        runTween.Add(iT.MoveTo.position, targetPosition);
        runTween.Add(iT.MoveTo.speed, speed);
        runTween.Add(iT.MoveTo.easetype, iTween.EaseType.linear);
        runTween.Add(iT.MoveTo.oncomplete, "RunTweenComplete");
        runTween.Add(iT.MoveTo.oncompletetarget, this.gameObject);

        bool movingLeft = targetPosition.x < this.transform.position.x;
        if (movingLeft && this.AnimationTarget.transform.localScale.x < 0)
        {
            Vector3 newScale = this.AnimationTarget.transform.localScale;
            newScale.x *= -1f;
            this.AnimationTarget.transform.localScale = newScale;
        }
        else if (!movingLeft && this.AnimationTarget.transform.localScale.x > 0)
        {
            Vector3 newScale = this.AnimationTarget.transform.localScale;
            newScale.x *= -1f;
            this.AnimationTarget.transform.localScale = newScale;
        }


        iTween.MoveTo(this.gameObject, runTween);
    }

    private void RunTweenComplete()
    {
        this.enabled = false;

        if (OnMoveCompleted != null)
            OnMoveCompleted(this, new EventArgs());
    }
}

The Brain:

Each different AI character needs its own Brain, This is a special class inhering from MonoBehavior that will control which tasks are executed and in which order. For me I made each task my brain controls a public member so that it can be set from the editor. But there is no reason you couldn't find a task using FindComponent(). How each brain is implemented is specific to the task. The example coming up next will show an implementation that can be tweaked to fit in for any AI character.

using UnityEngine;
using System.Collections;

public enum MascotState
{
    Waving,
    WalkingOn,
    WalkingOff,
    EndingDay,
    Angry,
    OffScreen
}
public class MascotBrain : MonoBehaviour
{
    public Waver Waver;
    public ObjectSwapper Swapper;
    public NonPhysicsRunner Mover;
    public AngryState Angry;

    public float ChanceAtGettingAngry = 0.05f;
    public float MinSecondsTillObstacle = 20;
    public float MaxSecondsTillObstacle = 40;

    public Transform OnScreenPosition;
    public Transform OffScreenPosition;

    private MascotState currentState;

    private float timeWaving;
    private float timeTillObstacle;

    void Start()
    {
        GameManager.Instance.GameStart += GameManager_OnGameStart;
        GameManager.Instance.GameOver += GameManager_OnGameOver;
        GameManager.Instance.DayStart += GameManager_DayStart;
        GameManager.Instance.DayOver += GameManager_DayEnd;
        this.currentState = MascotState.OffScreen;
        ResetObstacleTimer();
    }

    void OnDestroy()
    {
        GameManager.Instance.GameStart -= GameManager_OnGameStart;
        GameManager.Instance.GameOver -= GameManager_OnGameOver;
        GameManager.Instance.DayStart -= GameManager_DayStart;
        GameManager.Instance.DayOver -= GameManager_DayEnd;
    }

    void OnMouseUp()
    {
        if(this.currentState == MascotState.Waving)
        {
            float rnd = Random.Range(0f, 1f);
            if (rnd <= ChanceAtGettingAngry)
            {
                Angry.DoAngry(5);
                this.currentState = MascotState.Angry;
                Angry.OnAngryCompleted += AngryComplete;
            }
        }
    }

    void Update()
    {
        if(this.currentState == MascotState.Waving)
        {
            timeWaving += Time.deltaTime;
            if(timeWaving >= timeTillObstacle)
            {
                this.Swapper.SwapToSideState();
                this.Mover.OnMoveCompleted += ObstacleWalkoffCompleted;
                this.Mover.RunTo(OffScreenPosition.transform.position, 4);
                this.currentState = MascotState.WalkingOff;

                ResetObstacleTimer();
            }
        }
    }

    void GameManager_OnGameStart(object sender, System.EventArgs e)
    {
        this.transform.position = OffScreenPosition.position;
        currentState = MascotState.OffScreen;
        ResetObstacleTimer();
    }

    void GameManager_OnGameOver(object sender, System.EventArgs e)
    {
        this.transform.position = OffScreenPosition.position;
        currentState = MascotState.OffScreen;
    }

    void GameManager_DayStart(object sender, System.EventArgs e)
    {
        this.Mover.OnMoveCompleted -= DayEndWalkoffCompleted;
        this.Mover.OnMoveCompleted -= ObstacleWalkoffCompleted;
        this.Mover.OnMoveCompleted -= WalkOnCompleted;
        this.Angry.OnAngryCompleted -= AngryComplete;

        this.transform.position = OffScreenPosition.position;

        //do a walk on.
        this.Swapper.SwapToSideState();
        this.Mover.OnMoveCompleted += WalkOnCompleted;
        this.Mover.RunTo(OnScreenPosition.transform.position, 4);
        this.currentState = MascotState.WalkingOn;
    }

    void GameManager_DayEnd(object sender, System.EventArgs e)
    {
        if(currentState == MascotState.Waving)
        {
            // walk off to end the day
            this.Swapper.SwapToSideState();
            this.Mover.OnMoveCompleted += DayEndWalkoffCompleted;
            this.Mover.RunTo(OffScreenPosition.transform.position, 4);
            this.currentState = MascotState.EndingDay;
        }
    }

    void WalkOnCompleted(object sender, System.EventArgs e)
    {
        this.Mover.OnMoveCompleted -= WalkOnCompleted;

        if (GameManager.Instance.State == GameState.Daytime)
        {
            Swapper.SwapToFrontState();
            Waver.DoWave();
            this.currentState = MascotState.Waving;
        }
        else
        {
            this.Swapper.SwapToSideState();
            this.Mover.OnMoveCompleted += DayEndWalkoffCompleted;
            this.Mover.RunTo(OffScreenPosition.transform.position, 4);
            this.currentState = MascotState.EndingDay;
        }
    }

    void DayEndWalkoffCompleted(object sender, System.EventArgs e)
    {
        this.Mover.OnMoveCompleted -= DayEndWalkoffCompleted;
        Swapper.SwapToFrontState();
        this.currentState = MascotState.OffScreen;
    }

    void ObstacleWalkoffCompleted(object sender, System.EventArgs e)
    {
        this.Mover.OnMoveCompleted -= ObstacleWalkoffCompleted;
        Swapper.SwapToFrontState();
        this.currentState = MascotState.OffScreen;

        Swapper.SwapToSideState();
        this.Mover.OnMoveCompleted += WalkOnCompleted;
        this.Mover.RunTo(OnScreenPosition.transform.position, 4);
        this.currentState = MascotState.WalkingOn;
    }

    void AngryComplete(object sender, System.EventArgs e)
    {
        Angry.OnAngryCompleted -= AngryComplete;

        if (GameManager.Instance.State == GameState.Daytime)
        {
            this.transform.position = OffScreenPosition.position;
            this.Swapper.SwapToSideState();
            this.Mover.OnMoveCompleted += WalkOnCompleted;
            this.Mover.RunTo(OnScreenPosition.transform.position, 4);
            this.currentState = MascotState.WalkingOn;
        }
        else
        {
            this.transform.position = OffScreenPosition.position;
            this.currentState = MascotState.OffScreen;
        }
    }

    private void ResetObstacleTimer()
    {
        timeTillObstacle = Random.Range(MinSecondsTillObstacle, MaxSecondsTillObstacle);
        timeWaving = 0;
    }
}

This is where I was able to work a simple state machine into this specific character. The MascotBrain kicks off tasks based on the state of a brain. As a task completes the Brain determines what to do next based on the state of the Brain, the Game and what task just completed.

Wrapping Up:

So far I am finding this system great. It came together quickly and I was able to get the Mascot doing his thing in just a couple of hours. The tasks I created for the Mascot can easily be used for the other runners I plan on implementing this week.

Here is what My Mascot looks like in the editor.

image

 

 

Hope this inspired new ideas,
Mike 

Keep in touch: 
https://twitter.com/OnslaughtStudio
https://www.facebook.com/onslaughtstudios
http://www.twitch.tv/onslaughtstudios

Read more about:

Featured Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like