AI Walker Script

21 Oct 2010
Posted by xeophin

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).

using UnityEngine;
using System.Collections;
 
/// <summary>
/// A simple AI walking script. Follows a target.
/// </summary>
public class AIWalker : MonoBehaviour
{
 
    internal Vector3 target;
    private CharacterController controller;
    private int counter = 0;
    private Vector3 avoidNormal;
    private Vector3 semiStaticDirection;
 
    private RaycastHit hit;
 
    /// <summary>
    /// The delegate that will execute the current state of the FSM.
    /// </summary>
    private delegate void FState ();
    private FState stateMethod;
 
    /// <summary>
    /// Returns the vector of the current forward motion of the controller.
    /// </summary>
    /// <value>
    /// Returns a vector.
    /// </value>
    private Vector3 forward {
        get { return transform.TransformDirection (Vector3.forward); }
    }
 
    /// <summary>
    /// Helper function to get a right or left vector.
    /// </summary>
    /// <value>
    /// Returns left of right (Vector3).
    /// </value>
    private Vector3 randomLeftOrRight {
        get { return UnityEngine.Random.value > 0.5 ? Vector3.right : Vector3.left; }
    }
 
    /// <summary>
    /// Calculates the direction to the target object. Uses only the XZ plane.
    /// </summary>
    /// <value>
    /// The line of sight.
    /// </value>
    private Vector3 LineOfSight {
        get { return new Vector3 (target.x, transform.position.y, target.z); }
    }
 
    /// <summary>
    /// Gets the overall target.
    /// </summary>
    /// <value>
    /// The static target.
    /// </value>
    private Vector3 staticTarget {
        get { return GameObject.FindGameObjectWithTag ("Target").transform.position; }
    }
 
    #region UnityEngine methods
 
    /// <summary>
    /// Sets up the controller and puts the object into the new mode.
    /// </summary>
    void Start ()
    {
        controller = GetComponent<CharacterController> ();
        transform.tag = "CrowdObject";
        stateMethod = new FState (EnterWalk);
    }
 
    /// <summary>
    /// Executes in every frame once. Only calls the current state method.
    /// </summary>
    void FixedUpdate ()
    {
        stateMethod ();
    }
 
    #endregion
 
    #region Walk State
 
    /// <summary>
    /// Enter walk state.
    /// </summary>
    private void EnterWalk ()
    {
        target = staticTarget;
        transform.LookAt (LineOfSight);
        Debug.DrawLine (transform.position, target, Color.yellow);
        stateMethod = new FState (Walk);
        stateMethod ();
    }
 
    /// <summary>
    /// Normal, unobstructed walk towards the target.
    /// </summary>
    private void Walk ()
    {
        Debug.DrawLine (transform.position, target, Color.green);
 
        if (IsUnobstructed (staticTarget)) {
 
            target = staticTarget;
 
                        /* Currently not very perfect way to check whether there is
             * something on the side or not – otherwise we will just slide
             * along the colliders in the case that the final target is
             * unobstructed to the ray. */
bool blockedLeft = Physics.Raycast (transform.position, forward + transform.TransformDirection (Vector3.left), 1.5f);
            bool blockedRight = Physics.Raycast (transform.position, forward + transform.TransformDirection (Vector3.right), 1.5f);
 
            // Let's also check the sides to go sure
            if (blockedLeft && blockedRight) {
                transform.LookAt (LineOfSight);
            } else if (blockedLeft) {
                transform.RotateAroundLocal (Vector3.up, Mathf.PI * 0.1f);
            } else if (blockedRight) {
                transform.RotateAroundLocal (Vector3.up, -Mathf.PI * 0.1f);
            } else {
                transform.LookAt (LineOfSight);
            }
 
            // Move the controller
            controller.SimpleMove (forward * 2);
 
 
        } else {
            stateMethod = new FState (EnterWayfinding);
            ExitWalk ();
        }
 
        // Final check whether we move at all.
        DoesObjectMove (ExitWalk);
    }
 
    /// <summary>
    /// Exits the walk state.
    /// </summary>
    private void ExitWalk ()
    {
        print ("Exit Walk state.");
        counter = 0;
    }
 
    #endregion
 
    #region Wayfinding State
 
    /// <summary>
    /// Enters the wayfinding state.
    /// </summary>
    private void EnterWayfinding ()
    {
        print ("Enter Wayfinding state.");
 
        // Since we don't see the target anymore, we make a new one.
        target = SetNewTargetBasedOnReflection (LineOfSight);
        stateMethod = new FState (Wayfinding);
        stateMethod ();
    }
 
    private void Wayfinding ()
    {
        Debug.DrawLine (transform.position, target, Color.red);
 
        // If the new target is fine, we go there.
        if (IsUnobstructed (target)) {
            controller.SimpleMove (forward * 2);
        } else if (hit.collider.CompareTag ("CrowdObject")) {
 
            /* If there is another Crowd Object walking around, we try to
             * avoid it. */
 
            Vector3 direction = randomLeftOrRight;
            target = target + transform.TransformDirection (direction);
            transform.LookAt (LineOfSight);
        } else {
 
            /* We simply try to find yet another exit. */
 
            target = SetNewTargetBasedOnReflection (forward);
            transform.LookAt (LineOfSight);
        }
 
 
 
        if (DoesObjectMove (ExitWayfinding)) {
 
            /* If the instance moves, we can check whether we can see
             * the actual target again and return to the normal walk state. */
 
            if (IsUnobstructed (staticTarget)) {
                stateMethod = new FState (EnterWalk);
                ExitWayfinding ();
                return;
 
            } else {
                transform.LookAt (LineOfSight);
                target = target + forward * 2f;
            }
        }
 
 
 
    }
 
    /// <summary>
    /// Exits the wayfinding state.
    /// </summary>
    private void ExitWayfinding ()
    {
        print ("Exit Wayfinding State.");
        counter = 0;
    }
 
    #endregion
 
    #region Avoiding State
 
    /// <summary>
    /// Enters the avoiding state.
    /// </summary>
    private void EnterAvoiding ()
    {
        print ("Enter Avoiding State.");
 
        /* Since we only get in very specific circumstances into this state,
         * the <c>avoidNormal</c> vector should be set. */
 
        target = new Ray (transform.position, avoidNormal).GetPoint (2);
 
        transform.LookAt (LineOfSight);
 
        // In order to go to one direction, we fix that for the duration of this state.
        semiStaticDirection = randomLeftOrRight;
 
        stateMethod = new FState (Avoiding);
        stateMethod ();
    }
 
    /// <summary>
    /// The Avoiding state tries to back up a little until it sees the 
    /// actual target again. It is supposed to help in cases when the 
    /// instance is stuck in somewhere and needs to get out of it again.
    /// </summary>
    private void Avoiding ()
    {
        Debug.DrawLine (transform.position, target, Color.yellow);
        if (IsUnobstructed (target)) {
 
            /* We back up, with a slight bias to one side (since we likely got
             * into this situation by walking straight. */
 
            controller.SimpleMove (forward * 2);
            target = target + forward + transform.TransformDirection (semiStaticDirection) * 0.2f;
        } else if (hit.collider.CompareTag ("CrowdObject")) {
            target = target + transform.TransformDirection (randomLeftOrRight);
            transform.LookAt (LineOfSight);
        } else {
 
            /* The temporary target is obstructed, we try to turn in another 
             * direction. */
 
            target = target + transform.TransformDirection (semiStaticDirection) * 0.2f;
            transform.LookAt (LineOfSight);
            if (IsUnobstructed (target)) {
                controller.SimpleMove (forward);
                target = forward + transform.TransformDirection (semiStaticDirection) * 0.2f;
            } else {
                stateMethod = new FState (EnterWayfinding);
                ExitAvoiding ();
                return;
            }
        }
 
        if (DoesObjectMove (ExitAvoiding)) {
 
            if (IsUnobstructed (staticTarget)) {
 
                /* In order to get really away from the obstacle, we wait
                 * for a little while before we return to the original target.
                 */
 
                counter++;
            } else {
                transform.LookAt (LineOfSight);
                counter = 0;
            }
 
            if (counter > 20) {
                stateMethod = new FState (EnterWalk);
                ExitAvoiding ();
                return;
            }
        }
    }
 
    /// <summary>
    /// Exits the avoiding state.
    /// </summary>
    private void ExitAvoiding ()
    {
        print ("Exit Avoiding State.");
        counter = 0;
    }
 
    #endregion
 
    #region Helper Functions
 
    /// <summary>
    /// Determines whether the specified target is obstructed by another 
    /// collider.
    /// </summary>
    /// <returns>
    /// <c>true</c> if the target is unobstructed; otherwise, <c>false</c>.
    /// </returns>
    /// <param name='tempTarget'>
    /// The target to check whether it is obstructed or not.
    /// </param>
    private bool IsUnobstructed (Vector3 tempTarget)
    {
        Vector3 start = transform.position;
        Ray ray = new Ray (start, tempTarget - start);
        Debug.DrawRay (start, ray.direction);
        if (Physics.Raycast (ray, out hit, 3f)) {
            return false;
        } else {
            return true;
        }
    }
 
    /// <summary>
    /// Sets the new target based on reflection. Only the XZ-Plane is considered. This
    /// method is used to let the instance walk along the rim of the object (since the
    /// resulting direction of the instance will run along parallel to the object's
    /// hit face).
    /// </summary>
    /// <returns>
    /// The new target based on reflection, with the same distance from the collider as
    /// the object has right now.
    /// </returns>
    /// <param name='direction'>
    /// The direction the original ray is directed at.
    /// </param>
 
    private Vector3 SetNewTargetBasedOnReflection (Vector3 direction)
    {
        // Set up new vector3 to hold result.
        Vector3 result;
 
        Ray reflection = new Ray (hit.point, new Vector3 (Vector3.Reflect (direction, hit.normal).x, 0, Vector3.Reflect (direction, hit.normal).z));
 
        result = reflection.GetPoint (hit.distance);
 
                /* For the rare case that the reflection is the normal of the face,
         * we just take a random side to go along. */
        if (Vector3.Distance (transform.position, result) < 0.2f) {
            result = transform.TransformDirection (randomLeftOrRight) * 2f;
        }
 
                /* If we have to do that kinda thing too much in a row, we are likely
         * stuck somewhere and can't find out anymore. Since the 
         * DoesObjectMove() method is called in most states, we just set the
         * counter to a negative value, so the avoid state will be called
         * sooner or later. */
        counter--;
 
        return result;
    }
 
    /// <summary>
    /// Checks whether the charactercontroller moved over the last frames.
    /// If not, the state will change to "Avoid" after 5 frames of stuckyness.
    /// Additionally, an exit function can be specified, in order
    ///  to close up the current state properly.
    /// </summary>
    /// <returns>
    /// <c>true</c> if the object moves, else <c>false</c>, and the state
    /// will change to "Avoid".
    /// </returns>
    /// <param name='exitFunction'>
    /// The exit function to finish up the current state.
    /// </param>
    private bool DoesObjectMove (FState exitFunction)
    {
        bool result;
        if (controller.velocity.magnitude <= 0.05f) {
            counter--;
            result = false;
            if (counter == -1) {
                BroadcastMessage ("OnStop");
            }
        } else {
            result = true;
            if (counter != 0) {
                BroadcastMessage ("OnWalk");
            }
        }
 
        if (counter < -5) {
            stateMethod = new FState (EnterAvoiding);
 
            if (IsUnobstructed (target)) {
                /* The instance is being blocked by something invisible to the
                 * ray. Therefore, we just go into the other direction. */               
                print ("Unobstructed Avoid.");
                avoidNormal = -forward;
            } else {
                print ("Obstructed Avoid.");
                avoidNormal = new Vector3 (hit.normal.x, 0, hit.normal.z);
            }
 
            // Execute the exit function.
            exitFunction ();
 
        }
        return result;
    }
 
    #endregion
}
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).using UnityEngine;using System.Collections;/// /// A simple AI walking script. Follows a target./// public class AIWalker : MonoBehaviour{ internal Vector3 target; private CharacterController controller; private int counter = 0; private Vector3 avoidNormal; private Vector3 semiStaticDirection; private RaycastHit hit; /// /// The delegate that will execute the current state of the FSM. /// private delegate void FState (); private FState stateMethod; /// /// Returns the vector of the current forward motion of the controller. /// /// /// Returns a vector. /// private Vector3 forward { get { return transform.TransformDirection (Vector3.forward); } } /// /// Helper function to get a right or left vector. /// /// /// Returns left of right (Vector3). /// private Vector3 randomLeftOrRight { get { return UnityEngine.Random.value > 0.5 ? Vector3.right : Vector3.left; } } /// /// Calculates the direction to the target object.

Post new comment

The content of this field is kept private and will not be shown publicly. If you have a Gravatar account associated with the e-mail address you provide, it will be used to display your avatar.
By submitting this form, you accept the Mollom privacy policy.


Navigation



Languages