Programming Intelligence – Part I – Movement

If you haven’t read my introduction to this series I’d like to advise you to do so here.

In this first part of my artificial intelligence series, I’d like to cover the basics of our Agents’ movement.

Base Classes:

We should start by creating some base classes as we are trying to achieve a system that will allow us to modularly add new behaviors to our AI to make it more “intelligent”.

We will start out by creating three relatively simple classes (Steering, Agent, Behavior).

Steering.cs:

Steering is a base class that will hold all of our movement values for us. By that, I am speaking of the linear movement and the angular movement (rotation). Now, why would we need a class for that? As mentioned before we are aiming for a system that is really modular and we want to cover different steering behaviors. Having this class will make our lives a lot easier along the way as this will give us access to values that we are going to need to influence differently depending on the kind of steering behavior we are currently executing.

This class is basically only holding a value for each linear and angular movement, a constructor and two booleans that will help us determine if there is linear or angular movement at all.

using UnityEngine;

public class Steering  
{
  float angularMovement;
  Vector3 linearMovement;

  public float AngularMovement 
  {
    get{ return angularMovement;} 
    set{ angularMovement = value;}
  }

  public Vector3 LinearMovement
  {
    get{ return linearMovement;} 
    set{ linearMovement = value;}
  }

  public Steering()
  {
    angularMovement = .0f;
    linearMovement = new Vector3();
  }

  public bool IsMovingAngular()
  {
    return angularMovement != .0f;
  }

  public bool IsMovingLinear()
  {
    return linearMovement.sqrMagnitude != .0f;
  }	
}

 

Agent.cs:

Second of all, we should create a class that will be the component for our Agents. Agent.cs will have to be attached to all of our Agents.
For now, since we are only handling movement, you’ll see that we only have movement settings declared at the top of our class. This will change over the course of this series.

As you’ll see in the snippet below, we are utilizing FixedUpdate to take care of our movement updates, but we also seem to calculate movement in LateUpdate. Now, that might seem a little strange to you but there is a reason for that. Basically, everything that is happening in FixedUpdate is regarding our current frame whilst the LateUpdate() function calculates the next frame for us.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Agent : MonoBehaviour {

  [Header("Movement Settings:")]
  [SerializeField] float maximumSpeed;
  [SerializeField] float maximumAcceleration;
  [SerializeField] float orientation;
  [SerializeField] float rotation;
  [SerializeField] Vector3 velocity;

  protected Steering steering;

  Vector3 currentDisplacement;

  private void Start() 
  {
    velocity = Vector3.zero;
    steering = new Steering();
  }


  public virtual void FixedUpdate() 
  {
    currentDisplacement = velocity * Time.deltaTime;
    orientation += rotation * Time.deltaTime;

    
    if(orientation < .0f)
    {
      orientation += 360.0f;
    } 
    else if (orientation > 360.0f)
    {
      orientation -= 360.0f;
    }

    transform.Translate(currentDisplacement, Space.World);
    transform.rotation = new Quaternion();
    transform.Rotate(Vector3.up, orientation);
  }

  public virtual void LateUpdate()
  {
    velocity += steering.LinearMovement * Time.deltaTime;
    rotation += steering.AngularMovement * Time.deltaTime;

    if(velocity.magnitude > maximumSpeed)
    {
      velocity.Normalize();
      velocity = velocity * maximumSpeed;
    }

    if(!steering.IsMovingAngular())
    {
      rotation = .0f;
    }

    if(!steering.IsMovingLinear())
    {
      velocity = Vector3.zero;
    }

    steering = new Steering();
  }

  public void SetSteering(Steering _steering)
  {
    steering = _steering;
  }
}

 

Behavior.cs:

Lastly, we should implement a base class to build our behaviors upon. In our case, this is going to be our Behavior.cs.
This class is really simple – all there is to it is a GameObject target, as well as our Agent and an Update function that sets the new steering with every frame.

using UnityEngine;

public class Behavior : MonoBehaviour {

  GameObject target;

  public GameObject Target 
  {
    get{ return target;}
    set{ target = value;}
  }

  protected Agent agent;

  public virtual void Awake()
  {
    agent = GetComponent<Agent>();
  }

  public virtual void Update()
  {
    agent.SetSteering(GetSteering());
  }

  public virtual Steering GetSteering()
  {
    return new Steering();
  }
}

 

Movement:

With the base classes out of the way, we can now start to focus on the actual movement but to do that we need the bases of our movement. These are the Seek and the Flee behaviors. We are creating these behaviors to base our movement on them.

This GIF shows you the simple flee algorithm in action. As you can see – the white balls are moving in the opposite direction of the red ball.

Seek.cs:

We are going to start with the Seek behavior Рwhich is basically just going to contain the overriden GetSteering function. Seek is going to be our base behavior for every movement towards a target.

Let’s dive into the code – to do that we have to create a new C#-Script in our scripts folder called Seek.cs.
As I mentioned earlier Seek is a Behavior and has, therefore, to be inherited from our Behavior class.

Within the GetSteering function, you see that we are creating a new steering and then modifying the linear movement value. In this case, we are doing so by subtracting our own position from our targets position and then normalizing this Vector, which will result in the direction towards our target.

Lastly, we are just multiplying the direction by our agent’s maximum acceleration value and return this steering.

using UnityEngine;

public class Seek : Behavior 
{
  public override Steering GetSteering() 
  {
    Steering steering = new Steering();

    steering.LinearMovement = Target.transform.position - 
    transform.position;
    
    steering.LinearMovement.Normalize();
    steering.LinearMovement = steering.LinearMovement * 
    agent.MaximumAcceleration;

    return steering;
  }	
}

 

Flee.cs:

When looking at our Flee class you’ll quickly notice that it looks almost identical to our Seek class, the only difference here is that instead of subtracting our own position from the target’s position we are now subtracting the position of the target from our own. We are doing this as it will result ultimately in giving us the direction away from the target. As you might have already guessed, Flee is going to be our base behavior for every movement away from a target.

Let’s create a new Script called Flee.cs with the following code.

using UnityEngine;

public class Flee : Behavior 
{

  public override Steering GetSteering()
  {
    Steering steering = new Steering();

    steering.LinearMovement = transform.position - 
    Target.transform.position;

    steering.LinearMovement.Normalize();
    steering.LinearMovement = steering.LinearMovement * 
    agent.MaximumAcceleration;

    return steering;
  } 
}

 

Pursuing and Evading:

Since we took care of our Seek and Flee Behaviors, which are, as I mentioned before, the base classes for our movements – we can now go ahead and implement Pursue.cs, an algorithm that allows our agent to follow a target in order to try and catch it. Now, this is the important part of the pursue. A simple follow would just need to retrace the steps of its target while we are aiming for something that is able to catch up with its target.
We’ll achieve this result by predicting our targets position using its velocity.

This GIF shows the Pursue in action. As you can see pretty clearly the white ball isn’t retracing red’s path but rather assuming/predicting where red will go, this gives the algorithm much more realism.

Now, Evading is also quite a bit different than just the simple fleeing in the opposite direction. You’ll notice pretty quickly that Pursue.cs and Evade.cs look exactly the same except for that they implement the different base classes for the opposite directions.

This GIF shows the white ball presuming red’s direction and trying to escape him.

So you might have already figured out that Evade isn’t just a simple fleeing algorithm but it is more so meant to predict steps and therefore ultimately escape.

Since both of these are literally the same I’ll give you both snippets with only one explanation.

So, let’s talk code: If you take a look at the following snippets you’ll see that we set up a maxPredictions, an input target, and a target agent. In awake we’ll initialise these variables. Now, it might seem a little strange to you that we instantiate a new GameObject there, but I swear it’ll make sense in a second.

In our GetSteering() function we are getting the direction of our target like we did before. We are also calculating distance and speed. These values allow us to get a prediction of our target’s next position, which is the position we are setting the instantiated GameObject to. So basically we are using the new GameObject as a placeholder target to move towards/away from, depending on what behavior we are using.

There is also an OnDestroy function necessary in this class as we are instantiating the placeholder object, so we need to get rid of it at one point so we don’t trash our memory.

using UnityEngine;

public class Pursue : Seek
{
  float maxPrediction;

  GameObject inputTarget;
  Agent targetAgent;

  Vector3 direction;
  float distance;
  float speed;
  float prediction;

  public float MaxPrediction {get{ return maxPrediction;}}

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

    targetAgent = Target.GetComponent<Agent>();
    inputTarget = Target;
    Target = new GameObject();
  }

  void OnDestroy() 
  {
    Destroy(inputTarget);	
  }

  public override Steering GetSteering()
  {
    direction = inputTarget.transform.position - transform.position;
    distance = direction.magnitude;
    speed = agent.Velocity.magnitude;

    if(speed <= distance/maxPrediction)
    {
      prediction = maxPrediction;
    } 
    else 
    {
      prediction = distance/speed;
    }

    Target.transform.position = inputTarget.transform.position;
    Target.transform.position += targetAgent.Velocity * prediction;
    return base.GetSteering();
  } 
}

 

using UnityEngine;

public class Evade : Flee 
{
  float maxPrediction;

  GameObject inputTarget;
  Agent targetAgent;

  Vector3 direction;
  float distance;
  float speed;
  float prediction;

  public float MaxPrediction {get{ return maxPrediction;}}

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

    targetAgent = Target.GetComponent<Agent>();
    inputTarget = Target;
    Target = new GameObject();
  }

  void OnDestroy() 
  {
    Destroy(inputTarget);	
  }

  public override Steering GetSteering()
  {
    direction = inputTarget.transform.position - transform.position;
    distance = direction.magnitude;
    speed = agent.Velocity.magnitude;

    if(speed <= distance/maxPrediction)
    {
      prediction = maxPrediction;
    } 
    else 
    {
      prediction = distance/speed;
    }

    Target.transform.position = inputTarget.transform.position;
    Target.transform.position += targetAgent.Velocity * prediction;
    return base.GetSteering();
  } 
}

 

Arriving and Leaving:

Now, on the topic of realism, the next point should be that an agent’s movement shouldn’t be abrupt. It should feel as natural as possible. In order to achieve this we are going to implement algorithms for arriving and leaving next.

These two algorithms are again based on our seek and flee behaviors, but now we are trying to give our algorithm a parameter that basically is a stopping radius from our target. For arriving we are aiming for a result where our agent stops once he is within a given radius, so we can avoid seeing our agent just bumping into his target. Not only that but we are also going to slow down the agent’s speed the closer it gets to reaching its target.

This GIF shows how we are now slowing down once we enter our slowing radius. We stop as soon as we reach our target radius.

Arrive.cs:

For our arrive script we are going to start out with a few variables that’ll help us define the radius we consider close enough to our target, as well as a radius in which we are going to start slowing down our agent’s speed. Within the starting variables there is also a timeToTarget, which is basically a value in seconds we are going to use to determine a speed in which we want to reach our target once we entered our given radius.

Let’s talk about our GetSteering() function : Firstly we are calculating the desired speed we need to reach our target within our time to target once we enter the radius.

We then use this value we just calculated to clamp the steering value so it won’t overshoot our maximumSpeed setting.


using UnityEngine;

public class Arrive : Behavior 
{
    [SerializeField] float targetRadius;
    [SerializeField] float slowingRadius;
    [SerializeField] float timeToTarget;

    public float TargetRadius{ get{ return targetRadius;}}
    public float SlowingRadius{ get{ return slowingRadius;}}
    public float TimeToTarget{ get{ return timeToTarget;}}

    Vector3 desiredVelocity;
    Vector3 currentDirection;
    float currentDistance;
    float targetSpeed;

    public override Steering GetSteering()
    {
        Steering steering = new Steering();

        currentDirection = Target.transform.position - 
        transform.position;
        currentDistance = currentDirection.magnitude;

        if(currentDistance < targetRadius)
        {
            return steering;
        }

        if(currentDistance > slowingRadius)
        {
            targetSpeed = agent.MaximumSpeed;
        } 
        else
        {
            targetSpeed = agent.MaximumSpeed * 
            currentDistance/slowingRadius;
        }

        desiredVelocity = currentDirection;
        desiredVelocity.Normalize();

        desiredVelocity *= targetSpeed;

        steering.LinearMovement = desiredVelocity - 
        agent.Velocity;
        steering.LinearMovement /= timeToTarget;

        if(steering.LinearMovement.magnitude > 
        agent.MaximumAcceleration)
        {
            steering.LinearMovement.Normalize();
            steering.LinearMovement *= agent.MaximumAcceleration;
        }

        return steering;
    }   
}

 

Leave.cs:

For leaving it is basically the same algorithm again, but with the opposite direction, so we want our agent to stop once it is out of reach of something that is dangerous for example.

You’ll notice in the following code snippet that most of the things are the same as in Arrive. The only difference here is really the direction.

using UnityEngine;

public class Leave : Behavior 
{
    [SerializeField] float escapeRadius; 
    [SerializeField] float dangerRadius; 
    [SerializeField] float timeToTarget; 

    public float EscapeRadius{get {return escapeRadius;}} 
    public float DangerRadius{get {return dangerRadius;}} 
    public float TimeToTarget{get {return timeToTarget;}}

    Vector3 desiredVelocity; 
    Vector3 currentDirection;
    float currentDistance;
    float reduction;
    float targetSpeed;

    public override Steering GetSteering()
    {
        Steering steering = new Steering(); 
        
        currentDirection = transform.position - 
        Target.transform.position; 
        currentDistance = currentDirection.magnitude;

        if (currentDistance > dangerRadius) 
        {
            return steering;
        }
 
        if (currentDistance < escapeRadius) 
        {
            reduction = 0f; 
        }
        else 
        {
            reduction = currentDistance / 
            dangerRadius * agent.MaximumSpeed; 
        }
        
        targetSpeed = agent.MaximumSpeed - reduction; 

        desiredVelocity = currentDirection; 

        desiredVelocity.Normalize(); 
        desiredVelocity *= targetSpeed; 

        steering.LinearMovement = desiredVelocity - 
        agent.Velocity; 
        steering.LinearMovement /= timeToTarget; 

        if (steering.LinearMovement.magnitude > 
        agent.MaximumAcceleration) 
        { 
            steering.LinearMovement.Normalize(); 
            steering.LinearMovement *= agent.MaximumAcceleration; 
        } 

        return steering; 
    }  
}

 

Get the Repository on GitHub!

Up Next:

Stay tuned for the next part which will cover obstacle avoidance.
It is available here right now for Early Access Patrons and will be publically available from 18th September.

Please subscribe to my Twitter to get notified when the next part of this series is available.

Liked it? Take a second to support Kristin on Patreon!

1 comment

Leave a Reply

Your email address will not be published. Required fields are marked *