Using State Machine for AI in Unity

Just a few days ago I made a post about implementing State Machine in Unity. I know that having design pattern implementation and nothing else is less useful, so I decided to make small AI that will use State Machine in a bigger context.

What this example is?

In the example that I’ve prepared we are making logic for units in some RTS game. By default, they are aggressive, and they are trying to get rid of the enemies nearby them.
We are going to have 2 types of units. First one will be a knight who will attack other groups. The second type will siege machine that will only be able to attack buildings. Both will have the same components, but parameters might differ a little bit.

We are going to make 2 groups of knights with 5 unit each. Team Blue will have the castle that they will need to defend, and Team Red will have a machine that can destroy that castle.

Now it’s time for coding

We already have working implementation of State Machine, right? We are not going to change much with it besides adding new states that are going to communicate with the rest of the “environment”.

In terms of making our units, I like to split logic into a few different scripts like movement, attack, health or AI in our case. In additional I’m adding one “master” component which will have references to its all parts. This will make our lifes much easier. Also, each of these components will have a reference to the “master” component.

These units also need to identify one another, for that, I’m going to use an enum with values for each group (or in-game, for each player). They also need to know what type of thing other entity might be. For that, we need another enum with types: Character, Vehicle or Structure.

public enum OwnerEnum
{
    P1,
    P2
}

public enum EntityTypeEnum
{
    Character,
    Vehicle,
    Structure
}

Entities

Awesome! Next on our checklist will be Entity component for storing references. Because all units and buildings are going to be attackable, we can make one base script that let us identify others.

using UnityEngine;

public class Entity : MonoBehaviour
{
    [Header("Player Setup")]
    [SerializeField]
    private OwnerEnum owner;
    public OwnerEnum Owner => owner;

    public virtual EntityTypeEnum Type => EntityTypeEnum.Character;

    [Header("References")]
    [SerializeField]
    private EntityHealth health;
    public EntityHealth Health => health;
}

And we can create health script because that one also will be universal.

using UnityEngine;
using UnityEngine.Events;

public class EntityHealth : MonoBehaviour
{
    [SerializeField]
    private int health = 100;
    public bool IsAlive => health > 0;

    public UnityAction OnEntityKilled;

    public void TakeDamage(int dmg)
    {
        health -= dmg;

        if (health <= 0)
        {
            health = 0;
            OnEntityKilled?.Invoke();
        }
    }

    public void KillEntity()
    {
        Destroy(gameObject);
    }
}

Buildings

Now let’s make our scripts for buildings because it will be a very quick thing to do here. So our master building component will override a few things add it will attach itself to health script.

using UnityEngine;

public class Building : Entity
{
    public override EntityTypeEnum Type => EntityTypeEnum.Structure;

    private void Awake()
    {
        (Health as BuildingHealth).Owner = this;

        Health.OnEntityKilled += Health_OnEntityKilled;
    }

    private void Health_OnEntityKilled()
    {
        Health.KillEntity();
    }
}

Great, now we need to add one property to building health script!

using UnityEngine;

public class BuildingHealth : EntityHealth
{
    [HideInInspector]
    public Building Owner;
}

Units

Units are going to be very similar but a little bit bigger as they will use more components! So let’s start from the top – master script for our knights.

using UnityEngine;

public class Character : Entity
{
    public override EntityTypeEnum Type => EntityTypeEnum.Character;

    [Header("References")]
    [SerializeField]
    private CharacterAttact attact;
    public CharacterAttact Attact => attact;

    [SerializeField]
    private CharacterMovement movement;
    public CharacterMovement Movement => movement;

    [Space]
    [SerializeField]
    private StateMachine ai;

    private void Awake()
    {
        attact.Owner = this;
        (Health as CharacterHealth).Owner = this;
        movement.Owner = this;
        ai.Character = this;

        Health.OnEntityKilled += Health_OnEntityKilled;
    }

    private void Start()
    {
        ai.ChangeState(new WaitState());
    }

    private void Health_OnEntityKilled()
    {
        ai.ChangeState(new DiedState());
    }
}

You can already take notice this time I’m configuring State Machine inside Character script, or I’m just passing starting states to it. In terms of health, character health is almost the same as building health script.

using UnityEngine;

public class CharacterHealth : EntityHealth
{
    [HideInInspector]
    public Character Owner;
}

Attack script is the biggest script here because it has the most properties. We need to define damage, attack range, attack speed and that type of units it can attack. We also need a few methods that will find us a new target to attack and of course attacking itself.

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

public class CharacterAttact : MonoBehaviour
{
    [HideInInspector]
    public Character Owner;

    [SerializeField]
    private int attackDamage = 20;
    public int AttackDamage => attackDamage;

    [SerializeField]
    private float attackRange = 0.2f;
    public float AttackRange => attackRange;

    [SerializeField]
    private float attackCooldown = 0.5f;
    private float lastAttackTime;

    [Space]
    [SerializeField]
    private float maxDetectionRange = 20;

    [Space]
    [SerializeField]
    private List<EntityTypeEnum> targetList;
    public List<EntityTypeEnum> TargetList => targetList;

    [HideInInspector]
    public Entity targetEntity;


    private bool isAttacking = false;
    public bool IsAttacking => isAttacking;

    public UnityAction OnTargetKilled;
    public UnityAction OnTargetToFar;

    public Entity FindNewTarget()
    {
        // We are looking for entities that are not us, they have different owner and we have their type on our targetList. Oh, and they are in detection range
        var targets = FindObjectsOfType<Entity>().Where(e => e != Owner && e.Owner != Owner.Owner && targetList.Contains(e.Type) && Vector3.Distance(transform.position, e.transform.position) < maxDetectionRange).ToList();

        if (targets.Count > 0)
        {
            var closestTarget = targets[0];
            for (int i = 1; i < targets.Count; i++)
            {
                if (Vector3.Distance(transform.position, closestTarget.transform.position) > Vector3.Distance(transform.position, targets[i].transform.position))
                {
                    closestTarget = targets[i];
                }
            }
            return closestTarget;
        }

        return null;
    }

    public bool CanAttackTarget(Entity target)
    {
        if (!target)
            return false;

        return Vector3.Distance(transform.position, target.transform.position) < attackRange;
    }

    public void Attack(Entity target)
    {
        targetEntity = target;
        isAttacking = true;
    }

    private void Update()
    {
        if (!isAttacking)
        {
            return;
        }

        if (!CanAttackTarget(targetEntity))
        {
            OnTargetToFar?.Invoke();
            isAttacking = false;
            return;
        }

        Owner.Movement.RotateTowardsPoint(targetEntity.transform.position);

        if (Time.time - lastAttackTime >= attackCooldown)
        {
            lastAttackTime = Time.time;

            targetEntity.Health.TakeDamage(attackDamage);

            if (!targetEntity.Health.IsAlive)
            {
                OnTargetKilled?.Invoke();
                isAttacking = false;
            }
        }
    }
}

Next on our list is movement. I’ve made it by using Unity’s Character Controller to move my unit. It’s not the best movement ever created, but it gives me enough for example. Of course parameters and a few useful methods come as a standard option. ?

using UnityEngine;
using UnityEngine.Events;

public class CharacterMovement : MonoBehaviour
{
    [HideInInspector]
    public Character Owner;

    [SerializeField]
    private float speed = 5f;
    public float Speed => speed;

    [SerializeField]
    private float rotateSpeed = 30f;
    public float RotateSpeed => rotateSpeed;

    [HideInInspector]
    public float ReachDistance;

    [SerializeField]
    private CharacterController movement;

    private bool isMoving = false;
    public bool IsMoving => isMoving;

    private Vector3 targetPosition;

    public UnityAction OnPositionReached;

    public void MoveToPosition(Vector3 target)
    {
        targetPosition = target;
        isMoving = true;
    }

    public void StopMoving()
    {
        isMoving = false;
    }

    private void TargetReached()
    {
        OnPositionReached?.Invoke();
    }

    public void RotateTowardsPoint(Vector3 point)
    {
        var dir = point - transform.position;
        dir.Normalize();

        transform.Rotate(Vector3.up, Vector3.SignedAngle(-transform.right, dir, Vector3.up) * rotateSpeed * Time.deltaTime);
    }

    private void Update()
    {
        if (!isMoving)
        {
            return;
        }

        // movement
        var dir = targetPosition - transform.position;
        dir.Normalize();

        transform.Rotate(Vector3.up, Vector3.SignedAngle(-transform.right, dir, Vector3.up) * rotateSpeed * Time.deltaTime);

        var dot = Vector3.Dot(dir, -transform.right);
        movement.SimpleMove(Mathf.Max(dot, 0) * dir * speed * Time.deltaTime);

        if (Vector3.Distance(transform.position, targetPosition) < ReachDistance)
        {
            TargetReached();
        }
    }
}

The last script is just another master script for vehicles this time, and it looks like that:

using UnityEngine;

public class Vehicle : Character
{
    public override EntityTypeEnum Type => EntityTypeEnum.Vehicle;
}

AI State Machine

Oh, state machine! We meet again! There are literally 2 lines added to this script comparing to what we had in the previous post, and this is just a reference that we are receiving from master character script.

using UnityEngine;

public class StateMachine : MonoBehaviour
{
    [HideInInspector]
    public Character Character;

    private BaseState currentState;

    private void Update()
    {
        if (currentState != null)
        {
            currentState.UpdateState();
        }
    }

    public void ChangeState(BaseState newState)
    {
        if (currentState != null)
        {
            currentState.DestroyState();
        }

        currentState = newState;

        if (currentState != null)
        {
            currentState.owner = this;
            currentState.PrepareState();
        }
    }
}

Do you remember BaseState class? Great, because there is no change in it! ?

public class BaseState
{
    public StateMachine owner;

    public virtual void PrepareState() { }
    public virtual void UpdateState() { }
    public virtual void DestroyState() { }
}

Now time for AI!

We finally get to the point where we can write some AI! So the question is what should our unit do and how he should do it?
My version would be that:

  • wait a moment and think what to do
  • try to find something to attack
  • if you saw something, go for it!
  • if not, move a little bit, maybe later you will have more luck!

So we are going to implement these states:

using UnityEngine;

public class WaitState : BaseState
{
    public float minWaitTime = 1;
    public float maxWaitTime = 2.5f;

    private float timeout;
    private float startTime;

    public override void PrepareState()
    {
        base.PrepareState();

        timeout = Random.Range(minWaitTime, maxWaitTime);
        startTime = Time.time;
    }

    public override void UpdateState()
    {
        base.UpdateState();

        if (Time.time - startTime >= timeout)
        {
            owner.ChangeState(new FindTargetState());
        }
    }
}
using UnityEngine;

public class FindTargetState : BaseState
{
    public override void PrepareState()
    {
        base.PrepareState();

        var newTarget = owner.Character.Attact.FindNewTarget();
        var move = new MoveState();

        if (newTarget)
        {
            move.entity = newTarget;
        }
        else
        {
            move.position = owner.transform.position + new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5));
        }

        owner.ChangeState(move);
    }
}
using UnityEngine;

public class MoveState : BaseState
{
    public Vector3 position;
    public Entity entity;

    public override void PrepareState()
    {
        base.PrepareState();

        owner.Character.Movement.ReachDistance = owner.Character.Attact.AttackRange * 0.9f;
        owner.Character.Movement.OnPositionReached += Movement_OnPositionReached;

        if (!entity)
        {
            owner.Character.Movement.MoveToPosition(position);
        }
    }

    public override void UpdateState()
    {
        base.UpdateState();

        if (!entity)
        {
            return;
        }

        owner.Character.Movement.MoveToPosition(entity.transform.position);
    }

    public override void DestroyState()
    {
        owner.Character.Movement.OnPositionReached -= Movement_OnPositionReached;

        base.DestroyState();
    }

    private void Movement_OnPositionReached()
    {
        owner.Character.Movement.StopMoving();

        if (entity)
        {
            var attack = new AttackState();
            attack.targetEntity = entity;
            owner.ChangeState(attack);
        }
        else
        {
            owner.ChangeState(new WaitState());
        }
    }

}
using UnityEngine;

public class AttackState : BaseState
{
    public Entity targetEntity;

    public override void PrepareState()
    {
        base.PrepareState();

        owner.Character.Attact.OnTargetKilled += Attact_OnTargetKilled;
        owner.Character.Attact.OnTargetToFar += Attact_OnTargetToFar;

        owner.Character.Attact.Attack(targetEntity);
    }

    public override void DestroyState()
    {
        owner.Character.Attact.OnTargetKilled -= Attact_OnTargetKilled;
        owner.Character.Attact.OnTargetToFar -= Attact_OnTargetToFar;

        base.DestroyState();
    }

    private void Attact_OnTargetKilled()
    {
        owner.ChangeState(new WaitState());
    }

    private void Attact_OnTargetToFar()
    {
        var move = new MoveState();
        move.entity = targetEntity;
        owner.ChangeState(move);
    }
}

All of those states will enable one another. But there is one more state that will be activated by some external for State Machine event. This will be the event called after our unit die. And that last state will just try to remove itself and unit from the game.

using UnityEngine;

public class DiedState : BaseState
{
    public override void PrepareState()
    {
        base.PrepareState();

        owner.ChangeState(null);
    }

    public override void DestroyState()
    {
        // kill entity!
        owner.Character.Health.KillEntity();

        base.DestroyState();
    }
}

Awesome! Now we have all the components together! We should check how they are working!

Maybe this is not a Hollywood quality, especially without special effects and animations but they are doing their job well! ?

As always I’m publishing a small repository with that project so you can check it yourself. This one is available here.

I’ve used Kenney.nl assets if you already can’t tell. They made a lot of assets for games, and they are sharing them for free! I highly recommend going to their website! ?

I hope you enjoyed that, and until next time!

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x