Search Unity

State Machine AI

Discussion in 'Scripting' started by Gustav_Bok, Apr 14, 2014.

  1. Gustav_Bok

    Gustav_Bok

    Joined:
    Dec 6, 2012
    Posts:
    35
    Hi,

    Are you interested in building an AI to your game? This oldschool way of making game-AI is still useable for many types of games.

    I also attach a manual that goes a bit more indepth if you dont understand it.

    I hope to see some interesting AI-behaviour comming out from this base-script! :)

    Cheers!
    /Gustav

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public abstract class EnemyBehaviour : MonoBehaviour {
    6.  
    7.     /*If you need guidance see the manual.txt, and if that didn't help you can email me at gustavbok@gmail.com!*/
    8.  
    9.     /*Here you can add and remove states for the enemy, see the manual.txt for guidance!*/
    10.     public enum EnemyState
    11.     {
    12.         initializing,
    13.         idle,
    14.         sawPlayer,
    15.         chasing,
    16.         attacking,
    17.         fleeing
    18.     }
    19.     /*This is the currentState of the Enemy, this is what you'll change in the child-Class*/
    20.     public EnemyState currentState;
    21.  
    22.     public GameObject playerReference;
    23.  
    24.     void Start () {
    25.         currentState = EnemyState.initializing;
    26.     }
    27.    
    28.     /*In here there is a switch-statement which handles which method that is going
    29.     * to be updating, this is chosen by the currentState of the enemy.
    30.      It is in here that you will add your own EnemyState.yourState-case and call for your own method below*/
    31.     public virtual void Update () {
    32.  
    33.         switch (currentState) {
    34.         case EnemyState.initializing:
    35.             /*filling in the player reference for easier access*/
    36.             playerReference = GameObject.Find ("Player");
    37.             currentState = EnemyState.idle;
    38.             break;
    39.         case EnemyState.idle:
    40.             Idle();
    41.             break;
    42.         case EnemyState.sawPlayer:
    43.             SawPlayer();
    44.             break;
    45.         case EnemyState.chasing:
    46.             Chasing();
    47.             break;
    48.         case EnemyState.attacking:
    49.             Attacking();
    50.             break;
    51.         case EnemyState.fleeing:
    52.             Fleeing();
    53.             break;
    54.         default:
    55.             break;
    56.         }
    57.     }
    58.  
    59.     /*When you add your own methods here they need to be virtual, this is so you can in override them in your own
    60.      class*/
    61.  
    62.     public virtual void Idle()
    63.     {
    64.     }
    65.     public virtual void SawPlayer()
    66.     {
    67.     }
    68.     public virtual void Chasing()
    69.     {
    70.     }
    71.     public virtual void Attacking()
    72.     {
    73.     }
    74.     public virtual void Fleeing()
    75.     {
    76.     }
    77. }
    78.  
    Manual(Not completed):
     
  2. Gustav_Bok

    Gustav_Bok

    Joined:
    Dec 6, 2012
    Posts:
    35
    Here is an example class that inherits from the base script.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class Enemy1 : EnemyBehaviour {
    6.  
    7.     // Use this for initialization
    8.     void Start () {
    9.    
    10.     }
    11.  
    12.     /*Now you will need to override the methods you want to use for the enemy, these can be
    13.      * found in the bottom of the EnemyBehaviour-class*/
    14.  
    15.     public override void Idle()
    16.     {
    17.         /*Add your idle-behaviour here*/
    18.  
    19.         /*could be:*/
    20.         /*Update Patrolling the area*/
    21.         /* if in range of the player*/
    22.             /*Raycast toward player*/
    23.             /*if raycast hit - change currentState to sawPlayer*/
    24.  
    25.         //currentState = EnemyState.sawPlayer;
    26.     }
    27.     public override void SawPlayer()
    28.     {
    29.         /*Add your SawPlayer-behaviour here*/
    30.  
    31.         /*could be:*/
    32.         /* face the player */
    33.         /* play surprise animation */
    34.         /* when animation is done - change currentState to chasing*/
    35.  
    36.         //currentState = EnemyState.chasing;
    37.     }
    38.     public override void Chasing()
    39.     {
    40.         /*Add your Chasing-behaviour here*/
    41.  
    42.         /*could be:*/
    43.         /* if hp < 30 - change currentState to Flee*/
    44.             //currentState = EnemyState.fleeing;
    45.  
    46.         /* if not close enough to attack the player - move towards the player. It is here you choose
    47.          * if the enemy is either melee or ranged */
    48.  
    49.         /*else change currentState to attack*/
    50.             //currentState = EnemyState.attacking; 
    51.     }
    52.     public override void Attacking()
    53.     {
    54.         /*Add your Attacking-behaviour here*/
    55.  
    56.         /*could be:*/
    57.         /*if still close enough, do attack*/
    58.         /*else change to chase*/
    59.     }
    60.     public override void Fleeing()
    61.     {
    62.         /*Add your Fleeing-behaviour here*/
    63.  
    64.         /*could be:*/
    65.         /* if EnemyHp < 30*/
    66.             /*raycast to player and run the other way, simplest to implement*/
    67.         /*else*/
    68.             /*change state to chase*/
    69.     }
    70. }
    71.  
    And here is a filled one for an test enemy, ORC:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class EnemyOrc : EnemyBehaviour {
    6.    
    7.     AudioSource attackSoundEffect;
    8.     AudioSource lookSurprisedSoundEffect;
    9.    
    10.     bool hasSpottedPlayer;
    11.     float reactionTime;
    12.    
    13.     GameObject weaponHitBox;
    14.     Vector3 weaponStartPos;
    15.     Vector3 weaponEndPos;
    16.     float weaponSwingTimer;
    17.     float weaponSpeed;
    18.     float attackTimer;
    19.     bool weaponsOut;
    20.  
    21.     float healthRegeneration;
    22.     float healthRegenTimer;
    23.  
    24.     int hp;
    25.     // Use this for initialization
    26.     void Start () {
    27.  
    28.         hp = 10;
    29.         reactionTime = 50;
    30.         healthRegeneration = 2;
    31.         healthRegenTimer = 0;
    32.         weaponSpeed = 3;
    33.         weaponsOut = false;
    34.         attackTimer = 100;
    35.  
    36.         /*have not yet spotted player*/
    37.         hasSpottedPlayer = false;
    38.        
    39.         weaponSwingTimer = 0;
    40.     }
    41.  
    42.     public override void Idle()
    43.     {
    44.         if(Vector3.Distance(transform.position,playerReference.transform.position) < 5)
    45.         {
    46.             currentState = EnemyState.sawPlayer;
    47.         }
    48.     }  
    49.     public override void SawPlayer()
    50.     {
    51.         if(!hasSpottedPlayer)
    52.         {
    53.             /*visualisation of enemy animation*/
    54.             transform.localScale = new Vector3(transform.localScale.x*2,transform.localScale.x*2,transform.localScale.x*2);
    55.             hasSpottedPlayer = true;
    56.         }
    57.  
    58.         if(reactionTime < 0)
    59.         {
    60.             reactionTime = 50;
    61.             currentState = EnemyState.chasing;
    62.         }
    63.         {
    64.             reactionTime -= 1;
    65.         }
    66.        
    67.     }
    68.     public override void Chasing()
    69.     {
    70.         transform.LookAt(playerReference.transform);
    71.         if(Vector3.Distance(transform.position,playerReference.transform.position) > 2)
    72.         {
    73.             Vector3 moveTowards = playerReference.transform.position -transform.position;
    74.             moveTowards.Normalize();
    75.             float dampening = 10;
    76.             transform.position = new Vector3(transform.position.x+moveTowards.x/dampening,
    77.                                                             transform.position.y,
    78.                                                             transform.position.z+moveTowards.z/dampening);
    79.         }
    80.         else
    81.         {
    82.             currentState = EnemyState.attacking;
    83.         }
    84.     }
    85.     public override void Attacking()
    86.     {
    87.         if(weaponHitBox != null)
    88.         {
    89.             weaponSwingTimer += Time.deltaTime* weaponSpeed;
    90.             weaponHitBox.transform.position = Vector3.Slerp(weaponStartPos,
    91.                                                             weaponEndPos,
    92.                                                             weaponSwingTimer);
    93.         }
    94.  
    95.         if(weaponsOut)
    96.         {
    97.             GameObject.Destroy(weaponHitBox);
    98.             weaponHitBox = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
    99.             weaponHitBox.name = "weaponHitBox";
    100.             weaponHitBox.AddComponent<CapsuleCollider>();
    101.             weaponHitBox.transform.position = transform.position;
    102.             weaponStartPos = weaponHitBox.transform.position;
    103.            
    104.             Vector3 towardsPlayer = playerReference.transform.position -
    105.                 transform.position;
    106.            
    107.             towardsPlayer.Normalize();
    108.             towardsPlayer *= 2;
    109.            
    110.             weaponEndPos = new Vector3(
    111.                 transform.position.x + towardsPlayer.x,
    112.                 transform.position.y + towardsPlayer.y,
    113.                 transform.position.z + towardsPlayer.z);
    114.            
    115.             weaponHitBox.transform.parent = transform;
    116.             weaponHitBox.transform.LookAt(playerReference.transform);
    117.             weaponHitBox.transform.rotation = Quaternion.Euler(
    118.                 new Vector3(weaponHitBox.transform.rotation.eulerAngles.x+90,
    119.                         weaponHitBox.transform.rotation.eulerAngles.y,
    120.                         weaponHitBox.transform.rotation.eulerAngles.z));
    121.             weaponsOut = true;
    122.         }
    123.         if(attackTimer < 0)
    124.         {
    125.             GameObject.Destroy(weaponHitBox);
    126.             weaponSwingTimer = 0;
    127.             currentState = EnemyState.chasing;
    128.             weaponsOut = false;
    129.         }
    130.         else
    131.             attackTimer -= 1;
    132.  
    133.         if(hp < 5)
    134.         {
    135.             GameObject.Destroy(weaponHitBox);
    136.             weaponSwingTimer = 0;
    137.             currentState = EnemyState.fleeing;
    138.         }
    139.     }
    140.     public override void Fleeing()
    141.     {
    142.         Vector3 awayFromPlayer = transform.position - playerReference.transform.position;
    143.         awayFromPlayer.Normalize();
    144.        
    145.         transform.LookAt(transform.position + awayFromPlayer);
    146.         if(hp < 5)
    147.         {
    148.             float dampening = 30;
    149.             transform.position = new Vector3(transform.position.x+awayFromPlayer.x/dampening,
    150.                                                             transform.position.y,
    151.                                                             transform.position.z+awayFromPlayer.z/dampening);
    152.             healthRegenTimer += Time.deltaTime;
    153.            
    154.             if(healthRegenTimer >= healthRegeneration)
    155.             {
    156.                 Debug.Log("Enemy new hp: "+ hp);
    157.                 hp++;
    158.                 healthRegenTimer = 0;
    159.             }
    160.         }
    161.         else
    162.         {
    163.             currentState = EnemyState.chasing;
    164.         }
    165.     }
    166. }
    167.  
     
  3. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I prefer object-oriented state machine. :p
     
  4. bigmisterb

    bigmisterb

    Joined:
    Nov 6, 2010
    Posts:
    4,221
    This method is actually the base of an OOP. Each of those states simply calls things. You are still going to need a controller to control the OOP stuff. This is just a simple framework that helps do that.

    Good job, good examples.

    Perhaps a further addition would be sub classes. So An enemy orc could be a Wizard, or a warrior. Then you could have like a Warrior object or Wizard or archer or whatever with the same stats but simple AI already embedded.

    Further emphasize could be that an orc, looks like an orc, but then extends both the warrior and orc classes. The warrior class could be designed to only wear certain types of armor and use certain weapons, and the orc always uses specific models. So the combination could easily produce something along the lines of an MMO. (Though I will never suggest anyone to attempt such a thing without 1000 people working on it.)
     
  5. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Yes and no, I guess.

    Wrapping everything around distinctive object gives you some advantages... Like easy tracking of past and future states, constructor and destructor, polymorphism and so on. On top, it makes for a very easy management of your behaviors, since each state is a different file.

    Here's the machine we use;

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System;
    4. using System.Collections;
    5.  
    6. /// <summary>
    7. /// Basic state machine.
    8. /// Generally used for object that has multiple related or unrelated states.
    9. /// </summary>
    10. public class StateMachine
    11. {
    12.     private object parent;
    13.    
    14.     /// <summary>
    15.     /// Owner of this StateMachine.
    16.     /// </summary>
    17.     public object Parent
    18.     {
    19.         get { return parent; }
    20.     }
    21.  
    22.     private State previousState;
    23.  
    24.     /// <summary>
    25.     /// The older destroyed state that was playing previous to the current state.
    26.     /// </summary>
    27.     public State PreviousState
    28.     {
    29.         get { return previousState; }
    30.     }
    31.  
    32.     private State currentState;
    33.  
    34.     /// <summary>
    35.     /// Currently updated state.
    36.     /// </summary>
    37.     public State CurrentState
    38.     {
    39.         get { return currentState; }
    40.     }
    41.  
    42.     private State nextState;
    43.  
    44.     /// <summary>
    45.     /// Next State.
    46.     /// This will be this machine current state next frame.
    47.     /// </summary>
    48.     public State NextState
    49.     {
    50.         get { return nextState; }
    51.     }
    52.  
    53.     private bool forced = false;
    54.  
    55.     public StateMachine(StateMachine parent)
    56.     {
    57.         this.parent = parent;
    58.     }
    59.  
    60.     public StateMachine(MonoBehaviour parent)
    61.     {
    62.         this.parent = parent;
    63.     }
    64.  
    65.     public void Update()
    66.     {
    67.         if (nextState != null)
    68.         {
    69.             if (currentState != null)
    70.             {
    71.                 previousState = currentState;
    72.                 previousState.Destructor();
    73.             }
    74.  
    75.             currentState = nextState;
    76.             currentState.Constructor();
    77.  
    78.             nextState = null;
    79.  
    80.             forced = false;
    81.         }
    82.  
    83.         if (currentState != null)
    84.             currentState.Update();
    85.     }
    86.  
    87.     /// <summary>
    88.     /// Switch the state on the next frame.
    89.     /// If null, will remove a previous request.
    90.     /// </summary>
    91.     public void SwitchState(State nextState)
    92.     {
    93.         if (forced)
    94.             return;
    95.  
    96.         if (nextState == null || (nextState != null  this.nextState != null  nextState.Priority < this.nextState.Priority))
    97.             return;
    98.  
    99.         this.nextState = nextState;
    100.     }
    101.  
    102.     /// <summary>
    103.     /// Forces the state to change, bypassing priorities.
    104.     /// Any other following request will be ignored. Use with care.
    105.     /// </summary>
    106.     public void ForceSwitchState(State nextState)
    107.     {
    108.         if (forced)
    109.             return;
    110.  
    111.         this.nextState = nextState;
    112.         forced = true;
    113.     }
    114.  
    115.     /// <summary>
    116.     /// Return true is the current or next state is of that type.
    117.     /// </summary>
    118.     public bool IsInState(Type type)
    119.     {
    120.         return currentState.GetType() == type || (nextState != null  nextState.GetType() == type);
    121.     }
    122. }
    123.  
    And the base state class;

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. /// <summary>
    6. /// Derive each state of your machine from this.
    7. /// </summary>
    8. public abstract class State
    9. {
    10.     private StateMachine machine;
    11.  
    12.     /// <summary>
    13.     /// Owner of this state, even if this state is not currently referenced by the machine.
    14.     /// </summary>
    15.     public StateMachine Machine
    16.     {
    17.         get { return machine; }
    18.     }
    19.  
    20.     protected int priority = 1;
    21.  
    22.     /// <summary>
    23.     /// When two states try to gain control of a machine, the highest priority win.
    24.     /// </summary>
    25.     public int Priority
    26.     {
    27.         get { return priority; }
    28.     }
    29.  
    30.     public State(StateMachine machine)
    31.     {
    32.         this.machine = machine;
    33.     }
    34.  
    35.     /// <summary>
    36.     /// Called when a state is becoming active.
    37.     /// </summary>
    38.     public virtual void Constructor() { }
    39.  
    40.     /// <summary>
    41.     /// Called when a state is no more active.
    42.     /// </summary>
    43.     public virtual void Destructor() { }
    44.  
    45.     /// <summary>
    46.     /// Called each frame to update the state internal logic.
    47.     /// </summary>
    48.     public virtual void Update() { }
    49. }
    50.  
     
  6. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    I agree to not liking this pattern very much. I'd much rather skip the switch statement completely. Also, using an enum is already limiting how many different states you can have.

    Code (csharp):
    1.  
    2. public class Agent : MonoBehaviour
    3. {
    4.     StateMachine stateMachine;
    5.  
    6.     void Update()
    7.     {
    8.         stateMachine.CurrentState.Execute();
    9.     }
    10. }
    11.  
    Code (csharp):
    1.  
    2. public class StateMachine
    3. {
    4.     public IState CurrentState { get; private set; }
    5.  
    6.     public void ChangeState(IState newState)
    7.     {
    8.         if (CurrentState != null) CurrentState.Exit();
    9.         CurrentState = newState;
    10.         CurrentState.Enter();
    11.     }
    12. }
    13.  
    Code (csharp):
    1.  
    2. public interface IState<T>
    3. {
    4.     public T Owner { get; private set; }
    5.  
    6.     public void Enter();
    7.  
    8.     public void Execute();
    9.  
    10.     public void Exit();
    11. }
    12.  
    13. public class IdleState : IState<Agent>
    14. {
    15.     public IdleState(Agent owner)
    16.     {
    17.         this.Owner = owner;
    18.     }
    19.  
    20.     public void Enter() { }
    21.  
    22.     public void Execute() { }
    23.  
    24.     public void Exit() { }
    25. }
    26.  
     
  7. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    I like your approach LightStriker, my only criticism (that of personal preference) would be with the naming of your methods "Constructor" and "Destructor" for two reasons:
    1. The naming is confusing since C# has its own constructor and finalizer/destructor mechanism. So imo the terminology clashes.
    2. It is not consistent with Unity's naming "OnEnable" and "OnDisable".
    In the context of a Unity project I would probably go with "OnEnable" and "OnDisable" and would probably introduce two additional methods "OnEnter" and "OnExit".

    That's just my 2 cents anyhow ;)
     
  8. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Well, I didn't use Unity's naming for the specific reason to not be confusing with their own method. After all, "OnEnable" is called when an object is loaded, while a state "initiator" is called just before the state become active - once, or again if you reuse states. It would probably be more similar to "Awake" or "Start".

    I guess "OnEnter" and "OnExit" would make more sense. I like KelsoMRK "Enter, Exit, Execute"... EEE

    Also, it doesn't clash that much with C# constructor/destructor, because they both uses the class' name.

    Code (csharp):
    1.  
    2. public class Car
    3. {
    4.     // Constructor
    5.     public Car() { }
    6.  
    7.     // Destructor
    8.     ~Car() { }
    9. }
    10.  
    But I can understand the confusion. And I admit that naming in my state machine come from a much older C (not C++) engine structure. Old habits die hard.
     
  9. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    Unity executes OnEnable and OnDisable each time an object is activated/deactivated.

    No, of course it doesn't cause a syntax error, but it does clash with the C# terminology. The "constructor" of a class is still a different concept which shouldn't be confused...
     
  10. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    @KelsoMRK - That is a very nice and clean implementation!