In this module, we will delve into advanced AI techniques using state machines and behavior trees. These concepts are crucial for creating complex and responsive AI behaviors in your Unity projects.

State Machines

What is a State Machine?

A state machine is a computational model used to design algorithms. It consists of:

  • States: Different conditions or modes in which an object can exist.
  • Transitions: Rules that determine when and how the object moves from one state to another.
  • Events: Triggers that cause transitions between states.

Key Concepts

  1. State: Represents a specific condition or situation.
  2. Transition: The movement from one state to another.
  3. Event: An occurrence that triggers a transition.

Example: Simple State Machine for an Enemy AI

using UnityEngine;

public class EnemyAI : MonoBehaviour
{
    private enum State
    {
        Idle,
        Patrol,
        Chase,
        Attack
    }

    private State currentState;

    void Start()
    {
        currentState = State.Idle;
    }

    void Update()
    {
        switch (currentState)
        {
            case State.Idle:
                // Idle behavior
                if (SeePlayer())
                {
                    currentState = State.Chase;
                }
                break;
            case State.Patrol:
                // Patrol behavior
                if (SeePlayer())
                {
                    currentState = State.Chase;
                }
                break;
            case State.Chase:
                // Chase behavior
                if (InAttackRange())
                {
                    currentState = State.Attack;
                }
                break;
            case State.Attack:
                // Attack behavior
                if (!InAttackRange())
                {
                    currentState = State.Chase;
                }
                break;
        }
    }

    bool SeePlayer()
    {
        // Logic to detect player
        return false;
    }

    bool InAttackRange()
    {
        // Logic to check if player is in attack range
        return false;
    }
}

Explanation

  • State Enum: Defines the possible states (Idle, Patrol, Chase, Attack).
  • currentState: Tracks the current state of the AI.
  • Update Method: Checks conditions and transitions between states based on events (e.g., seeing the player, being in attack range).

Behavior Trees

What is a Behavior Tree?

A behavior tree is a hierarchical model used to control the decision-making process of AI. It consists of:

  • Nodes: Represent tasks or actions.
  • Branches: Define the flow of execution.
  • Root: The starting point of the tree.

Key Concepts

  1. Root Node: The entry point of the behavior tree.
  2. Composite Nodes: Control the flow of execution (e.g., Sequence, Selector).
  3. Leaf Nodes: Perform actions or check conditions.

Example: Simple Behavior Tree for an Enemy AI

using UnityEngine;

public class EnemyBehaviorTree : MonoBehaviour
{
    private enum NodeState
    {
        Success,
        Failure,
        Running
    }

    private abstract class Node
    {
        public abstract NodeState Evaluate();
    }

    private class Sequence : Node
    {
        private Node[] nodes;

        public Sequence(params Node[] nodes)
        {
            this.nodes = nodes;
        }

        public override NodeState Evaluate()
        {
            foreach (Node node in nodes)
            {
                NodeState result = node.Evaluate();
                if (result == NodeState.Failure)
                {
                    return NodeState.Failure;
                }
                if (result == NodeState.Running)
                {
                    return NodeState.Running;
                }
            }
            return NodeState.Success;
        }
    }

    private class Selector : Node
    {
        private Node[] nodes;

        public Selector(params Node[] nodes)
        {
            this.nodes = nodes;
        }

        public override NodeState Evaluate()
        {
            foreach (Node node in nodes)
            {
                NodeState result = node.Evaluate();
                if (result == NodeState.Success)
                {
                    return NodeState.Success;
                }
                if (result == NodeState.Running)
                {
                    return NodeState.Running;
                }
            }
            return NodeState.Failure;
        }
    }

    private class CheckPlayerInRange : Node
    {
        public override NodeState Evaluate()
        {
            // Logic to check if player is in range
            return NodeState.Success;
        }
    }

    private class AttackPlayer : Node
    {
        public override NodeState Evaluate()
        {
            // Logic to attack player
            return NodeState.Success;
        }
    }

    private Node rootNode;

    void Start()
    {
        rootNode = new Sequence(
            new CheckPlayerInRange(),
            new AttackPlayer()
        );
    }

    void Update()
    {
        rootNode.Evaluate();
    }
}

Explanation

  • NodeState Enum: Represents the possible states of a node (Success, Failure, Running).
  • Node Class: Abstract base class for all nodes.
  • Sequence and Selector Classes: Composite nodes that control the flow of execution.
  • CheckPlayerInRange and AttackPlayer Classes: Leaf nodes that perform specific actions.

Practical Exercise

Task

Create a state machine and a behavior tree for an AI character that can:

  1. Patrol between waypoints.
  2. Chase the player when in sight.
  3. Attack the player when in range.

Solution

State Machine

using UnityEngine;

public class PatrolChaseAttackAI : MonoBehaviour
{
    private enum State
    {
        Patrol,
        Chase,
        Attack
    }

    private State currentState;
    public Transform[] waypoints;
    private int currentWaypointIndex = 0;

    void Start()
    {
        currentState = State.Patrol;
    }

    void Update()
    {
        switch (currentState)
        {
            case State.Patrol:
                Patrol();
                if (SeePlayer())
                {
                    currentState = State.Chase;
                }
                break;
            case State.Chase:
                Chase();
                if (InAttackRange())
                {
                    currentState = State.Attack;
                }
                break;
            case State.Attack:
                Attack();
                if (!InAttackRange())
                {
                    currentState = State.Chase;
                }
                break;
        }
    }

    void Patrol()
    {
        // Patrol logic
    }

    void Chase()
    {
        // Chase logic
    }

    void Attack()
    {
        // Attack logic
    }

    bool SeePlayer()
    {
        // Logic to detect player
        return false;
    }

    bool InAttackRange()
    {
        // Logic to check if player is in attack range
        return false;
    }
}

Behavior Tree

using UnityEngine;

public class PatrolChaseAttackBehaviorTree : MonoBehaviour
{
    private enum NodeState
    {
        Success,
        Failure,
        Running
    }

    private abstract class Node
    {
        public abstract NodeState Evaluate();
    }

    private class Sequence : Node
    {
        private Node[] nodes;

        public Sequence(params Node[] nodes)
        {
            this.nodes = nodes;
        }

        public override NodeState Evaluate()
        {
            foreach (Node node in nodes)
            {
                NodeState result = node.Evaluate();
                if (result == NodeState.Failure)
                {
                    return NodeState.Failure;
                }
                if (result == NodeState.Running)
                {
                    return NodeState.Running;
                }
            }
            return NodeState.Success;
        }
    }

    private class Selector : Node
    {
        private Node[] nodes;

        public Selector(params Node[] nodes)
        {
            this.nodes = nodes;
        }

        public override NodeState Evaluate()
        {
            foreach (Node node in nodes)
            {
                NodeState result = node.Evaluate();
                if (result == NodeState.Success)
                {
                    return NodeState.Success;
                }
                if (result == NodeState.Running)
                {
                    return NodeState.Running;
                }
            }
            return NodeState.Failure;
        }
    }

    private class Patrol : Node
    {
        public override NodeState Evaluate()
        {
            // Patrol logic
            return NodeState.Success;
        }
    }

    private class Chase : Node
    {
        public override NodeState Evaluate()
        {
            // Chase logic
            return NodeState.Success;
        }
    }

    private class Attack : Node
    {
        public override NodeState Evaluate()
        {
            // Attack logic
            return NodeState.Success;
        }
    }

    private class CheckPlayerInRange : Node
    {
        public override NodeState Evaluate()
        {
            // Logic to check if player is in range
            return NodeState.Success;
        }
    }

    private Node rootNode;

    void Start()
    {
        rootNode = new Selector(
            new Sequence(
                new CheckPlayerInRange(),
                new Attack()
            ),
            new Chase(),
            new Patrol()
        );
    }

    void Update()
    {
        rootNode.Evaluate();
    }
}

Conclusion

In this module, we explored state machines and behavior trees, two powerful tools for creating complex AI behaviors in Unity. We covered the basic concepts, provided practical examples, and included an exercise to reinforce your understanding. By mastering these techniques, you can create more dynamic and responsive AI characters in your games.

© Copyright 2024. All rights reserved