AI Walker Script

A simple walker script for Unity 3D in C# that tries to avoid obstacles. It has been used in the ProXedural project at art school, and it made the cows walk towards one of the players. To be honest, it does not really deserve the I in AI. But then again, no one expects cows to be very clever.

Maybe someone can use it as a base to produce something better (oh well, who am I kidding, this has already been done).

  1. using UnityEngine;
  2. using System.Collections;
  3.  
  4. /// <summary>
  5. /// A simple AI walking script. Follows a target.
  6. /// </summary>
  7. public class AIWalker : MonoBehaviour
  8. {
  9.  
  10.  internal Vector3 target;
  11.    private CharacterController controller;
  12.     private int counter = 0;
  13.    private Vector3 avoidNormal;
  14.    private Vector3 semiStaticDirection;
  15.  
  16.   private RaycastHit hit;
  17.  
  18.    /// <summary>
  19.     /// The delegate that will execute the current state of the FSM.
  20.    /// </summary>
  21.    private delegate void FState ();
  22.    private FState stateMethod;
  23.  
  24.    /// <summary>
  25.     /// Returns the vector of the current forward motion of the controller.
  26.     /// </summary>
  27.    /// <value>
  28.   /// Returns a vector.
  29.   /// </value>
  30.  private Vector3 forward {
  31.       get { return transform.TransformDirection (Vector3.forward); }
  32.  }
  33.  
  34.  /// <summary>
  35.     /// Helper function to get a right or left vector.
  36.  /// </summary>
  37.    /// <value>
  38.   /// Returns left of right (Vector3).
  39.    /// </value>
  40.  private Vector3 randomLeftOrRight {
  41.         get { return UnityEngine.Random.value > 0.5 ? Vector3.right : Vector3.left; }
  42.    }
  43.  
  44.  /// <summary>
  45.     /// Calculates the direction to the target object. Uses only the XZ plane.
  46.  /// </summary>
  47.    /// <value>
  48.   /// The line of sight.
  49.  /// </value>
  50.  private Vector3 LineOfSight {
  51.       get { return new Vector3 (target.x, transform.position.y, target.z); }
  52.  }
  53.  
  54.  /// <summary>
  55.     /// Gets the overall target.
  56.    /// </summary>
  57.    /// <value>
  58.   /// The static target.
  59.  /// </value>
  60.  private Vector3 staticTarget {
  61.      get { return GameObject.FindGameObjectWithTag ("Target").transform.position; }
  62.    }
  63.  
  64.  #region UnityEngine methods
  65.  
  66.    /// <summary>
  67.     /// Sets up the controller and puts the object into the new mode.
  68.   /// </summary>
  69.    void Start ()
  70.   {
  71.       controller = GetComponent<CharacterController> ();
  72.        transform.tag = "CrowdObject";
  73.        stateMethod = new FState (EnterWalk);
  74.   }
  75.  
  76.  /// <summary>
  77.     /// Executes in every frame once. Only calls the current state method.
  78.  /// </summary>
  79.    void FixedUpdate ()
  80.     {
  81.       stateMethod ();
  82.     }
  83.  
  84.  #endregion
  85.  
  86.     #region Walk State
  87.  
  88.     /// <summary>
  89.     /// Enter walk state.
  90.   /// </summary>
  91.    private void EnterWalk ()
  92.   {
  93.       target = staticTarget;
  94.      transform.LookAt (LineOfSight);
  95.         Debug.DrawLine (transform.position, target, Color.yellow);
  96.      stateMethod = new FState (Walk);
  97.        stateMethod ();
  98.     }
  99.  
  100.  /// <summary>
  101.     /// Normal, unobstructed walk towards the target.
  102.   /// </summary>
  103.    private void Walk ()
  104.    {
  105.       Debug.DrawLine (transform.position, target, Color.green);
  106.  
  107.        if (IsUnobstructed (staticTarget)) {
  108.  
  109.            target = staticTarget;
  110.  
  111.                        /* Currently not very perfect way to check whether there is
  112.              * something on the side or not – otherwise we will just slide
  113.           * along the colliders in the case that the final target is
  114.              * unobstructed to the ray. <em>/
  115. bool blockedLeft = Physics.Raycast (transform.position, forward + transform.TransformDirection (Vector3.left), 1.5f);
  116.            bool blockedRight = Physics.Raycast (transform.position, forward + transform.TransformDirection (Vector3.right), 1.5f);
  117.             
  118.            // Let's also check the sides to go sure
  119.           if (blockedLeft && blockedRight) {
  120.              transform.LookAt (LineOfSight);
  121.             } else if (blockedLeft) {
  122.               transform.RotateAroundLocal (Vector3.up, Mathf.PI * 0.1f);
  123.          } else if (blockedRight) {
  124.              transform.RotateAroundLocal (Vector3.up, -Mathf.PI * 0.1f);
  125.             } else {
  126.                transform.LookAt (LineOfSight);
  127.             }
  128.           
  129.            // Move the controller
  130.          controller.SimpleMove (forward * 2);
  131.            
  132.            
  133.        } else {
  134.            stateMethod = new FState (EnterWayfinding);
  135.             ExitWalk ();
  136.        }
  137.       
  138.        // Final check whether we move at all.
  139.      DoesObjectMove (ExitWalk);
  140.  }
  141.  
  142.  /// <summary>
  143.     /// Exits the walk state.
  144.   /// </summary>
  145.    private void ExitWalk ()
  146.    {
  147.       print ("Exit Walk state.");
  148.       counter = 0;
  149.    }
  150.  
  151.  #endregion
  152.  
  153.     #region Wayfinding State
  154.  
  155.   /// <summary>
  156.     /// Enters the wayfinding state.
  157.    /// </summary>
  158.    private void EnterWayfinding ()
  159.     {
  160.       print ("Enter Wayfinding state.");
  161.        
  162.        // Since we don't see the target anymore, we make a new one.
  163.       target = SetNewTargetBasedOnReflection (LineOfSight);
  164.       stateMethod = new FState (Wayfinding);
  165.      stateMethod ();
  166.     }
  167.  
  168.  private void Wayfinding ()
  169.  {
  170.       Debug.DrawLine (transform.position, target, Color.red);
  171.         
  172.        // If the new target is fine, we go there.
  173.      if (IsUnobstructed (target)) {
  174.          controller.SimpleMove (forward * 2);
  175.        } else if (hit.collider.CompareTag ("CrowdObject")) {
  176.             
  177.            /</em> If there is another Crowd Object walking around, we try to
  178.            * avoid it. <em>/
  179.  
  180.            Vector3 direction = randomLeftOrRight;
  181.          target = target + transform.TransformDirection (direction);
  182.             transform.LookAt (LineOfSight);
  183.         } else {
  184.            
  185.            /</em> We simply try to find yet another exit. <em>/
  186.  
  187.          target = SetNewTargetBasedOnReflection (forward);
  188.           transform.LookAt (LineOfSight);
  189.         }
  190.       
  191.        
  192.        
  193.        if (DoesObjectMove (ExitWayfinding)) {
  194.          
  195.            /</em> If the instance moves, we can check whether we can see
  196.            * the actual target again and return to the normal walk state. <em>/
  197.  
  198.             if (IsUnobstructed (staticTarget)) {
  199.                stateMethod = new FState (EnterWalk);
  200.               ExitWayfinding ();
  201.              return;
  202.                 
  203.            } else {
  204.                transform.LookAt (LineOfSight);
  205.                 target = target + forward * 2f;
  206.             }
  207.       }
  208.       
  209.        
  210.        
  211.    }
  212.  
  213.  /// <summary>
  214.     /// Exits the wayfinding state.
  215.     /// </summary>
  216.    private void ExitWayfinding ()
  217.  {
  218.       print ("Exit Wayfinding State.");
  219.         counter = 0;
  220.    }
  221.  
  222.  #endregion
  223.  
  224.     #region Avoiding State
  225.  
  226.     /// <summary>
  227.     /// Enters the avoiding state.
  228.  /// </summary>
  229.    private void EnterAvoiding ()
  230.   {
  231.       print ("Enter Avoiding State.");
  232.      
  233.        /</em> Since we only get in very specific circumstances into this state,
  234.         * the <c>avoidNormal</c> vector should be set. <em>/
  235.  
  236.         target = new Ray (transform.position, avoidNormal).GetPoint (2);
  237.        
  238.        transform.LookAt (LineOfSight);
  239.         
  240.        // In order to go to one direction, we fix that for the duration of this state.
  241.         semiStaticDirection = randomLeftOrRight;
  242.        
  243.        stateMethod = new FState (Avoiding);
  244.        stateMethod ();
  245.     }
  246.  
  247.  /// <summary>
  248.     /// The Avoiding state tries to back up a little until it sees the 
  249.     /// actual target again. It is supposed to help in cases when the 
  250.  /// instance is stuck in somewhere and needs to get out of it again.
  251.    /// </summary>
  252.    private void Avoiding ()
  253.    {
  254.       Debug.DrawLine (transform.position, target, Color.yellow);
  255.      if (IsUnobstructed (target)) {
  256.          
  257.            /</em> We back up, with a slight bias to one side (since we likely got
  258.           * into this situation by walking straight. <em>/
  259.  
  260.             controller.SimpleMove (forward * 2);
  261.            target = target + forward + transform.TransformDirection (semiStaticDirection) * 0.2f;
  262.      } else if (hit.collider.CompareTag ("CrowdObject")) {
  263.             target = target + transform.TransformDirection (randomLeftOrRight);
  264.             transform.LookAt (LineOfSight);
  265.         } else {
  266.            
  267.            /</em> The temporary target is obstructed, we try to turn in another 
  268.            * direction. <em>/
  269.  
  270.           target = target + transform.TransformDirection (semiStaticDirection) * 0.2f;
  271.            transform.LookAt (LineOfSight);
  272.             if (IsUnobstructed (target)) {
  273.              controller.SimpleMove (forward);
  274.                target = forward + transform.TransformDirection (semiStaticDirection) * 0.2f;
  275.           } else {
  276.                stateMethod = new FState (EnterWayfinding);
  277.                 ExitAvoiding ();
  278.                return;
  279.             }
  280.       }
  281.       
  282.        if (DoesObjectMove (ExitAvoiding)) {
  283.            
  284.            if (IsUnobstructed (staticTarget)) {
  285.                
  286.                /</em> In order to get really away from the obstacle, we wait
  287.                * for a little while before we return to the original target.
  288.               <em>/
  289.  
  290.                counter++;
  291.          } else {
  292.                transform.LookAt (LineOfSight);
  293.                 counter = 0;
  294.            }
  295.           
  296.            if (counter > 20) {
  297.              stateMethod = new FState (EnterWalk);
  298.               ExitAvoiding ();
  299.                return;
  300.             }
  301.       }
  302.   }
  303.  
  304.  /// <summary>
  305.     /// Exits the avoiding state.
  306.   /// </summary>
  307.    private void ExitAvoiding ()
  308.    {
  309.       print ("Exit Avoiding State.");
  310.       counter = 0;
  311.    }
  312.  
  313.  #endregion
  314.  
  315.     #region Helper Functions
  316.  
  317.   /// <summary>
  318.     /// Determines whether the specified target is obstructed by another 
  319.   /// collider.
  320.   /// </summary>
  321.    /// <returns>
  322.     /// <c>true</c> if the target is unobstructed; otherwise, <c>false</c>.
  323.     /// </returns>
  324.    /// <param name='tempTarget'>
  325.   /// The target to check whether it is obstructed or not.
  326.    /// </param>
  327.  private bool IsUnobstructed (Vector3 tempTarget)
  328.    {
  329.       Vector3 start = transform.position;
  330.         Ray ray = new Ray (start, tempTarget - start);
  331.      Debug.DrawRay (start, ray.direction);
  332.       if (Physics.Raycast (ray, out hit, 3f)) {
  333.           return false;
  334.       } else {
  335.            return true;
  336.        }
  337.   }
  338.  
  339.  /// <summary>
  340.     /// Sets the new target based on reflection. Only the XZ-Plane is considered. This
  341.  /// method is used to let the instance walk along the rim of the object (since the
  342.  /// resulting direction of the instance will run along parallel to the object's
  343.    /// hit face).
  344.  /// </summary>
  345.    /// <returns>
  346.     /// The new target based on reflection, with the same distance from the collider as
  347.     /// the object has right now.
  348.   /// </returns>
  349.    /// <param name='direction'>
  350.    /// The direction the original ray is directed at.
  351.  /// </param>
  352.  
  353.     private Vector3 SetNewTargetBasedOnReflection (Vector3 direction)
  354.   {
  355.       // Set up new vector3 to hold result.
  356.       Vector3 result;
  357.         
  358.        Ray reflection = new Ray (hit.point, new Vector3 (Vector3.Reflect (direction, hit.normal).x, 0, Vector3.Reflect (direction, hit.normal).z));
  359.        
  360.        result = reflection.GetPoint (hit.distance);
  361.        
  362.                /</em> For the rare case that the reflection is the normal of the face,
  363.          * we just take a random side to go along. <em>/
  364.       if (Vector3.Distance (transform.position, result) < 0.2f) {
  365.          result = transform.TransformDirection (randomLeftOrRight) * 2f;
  366.         }
  367.       
  368.                /</em> If we have to do that kinda thing too much in a row, we are likely
  369.        * stuck somewhere and can't find out anymore. Since the 
  370.       * DoesObjectMove() method is called in most states, we just set the
  371.         * counter to a negative value, so the avoid state will be called
  372.        * sooner or later. <em>/
  373.      counter--;
  374.      
  375.        return result;
  376.  }
  377.  
  378.  /// <summary>
  379.     /// Checks whether the charactercontroller moved over the last frames.
  380.  /// If not, the state will change to "Avoid" after 5 frames of stuckyness.
  381.    /// Additionally, an exit function can be specified, in order
  382.   ///  to close up the current state properly.
  383.    /// </summary>
  384.    /// <returns>
  385.     /// <c>true</c> if the object moves, else <c>false</c>, and the state
  386.   /// will change to "Avoid".
  387.   /// </returns>
  388.    /// <param name='exitFunction'>
  389.     /// The exit function to finish up the current state.
  390.   /// </param>
  391.  private bool DoesObjectMove (FState exitFunction)
  392.   {
  393.       bool result;
  394.        if (controller.velocity.magnitude <= 0.05f) {
  395.            counter--;
  396.          result = false;
  397.             if (counter == -1) {
  398.                BroadcastMessage ("OnStop");
  399.          }
  400.       } else {
  401.            result = true;
  402.          if (counter != 0) {
  403.                 BroadcastMessage ("OnWalk");
  404.          }
  405.       }
  406.       
  407.        if (counter < -5) {
  408.          stateMethod = new FState (EnterAvoiding);
  409.           
  410.            if (IsUnobstructed (target)) {
  411.              /</em> The instance is being blocked by something invisible to the
  412.               * ray. Therefore, we just go into the other direction. */              
  413.                print ("Unobstructed Avoid.");
  414.                avoidNormal = -forward;
  415.             } else {
  416.                print ("Obstructed Avoid.");
  417.              avoidNormal = new Vector3 (hit.normal.x, 0, hit.normal.z);
  418.          }
  419.  
  420.            // Execute the exit function.
  421.           exitFunction ();
  422.  
  423.        }
  424.       return result;
  425.  }
  426.  
  427.    #endregion
  428. }
Social Tags:
C++

Add new comment