Unity 2018 Artificial Intelligence Cookbook - Second Edition

4.5 (4 reviews total)
By Jorge Palacios
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Behaviors - Intelligent Movement

About this book

Interactive and engaging games come with intelligent enemies, and this intellectual behavior is combined with a variety of techniques collectively referred to as Artificial Intelligence. Exploring Unity's API, or its built-in features, allows limitless possibilities when it comes to creating your game's worlds and characters. This cookbook covers both essential and niche techniques to help you take your AI programming to the next level.

To start with, you’ll quickly run through the essential building blocks of working with an agent, programming movement, and navigation in a game environment, followed by improving your agent's decision-making and coordination mechanisms – all through hands-on examples using easily customizable techniques. You’ll then discover how to emulate the vision and hearing capabilities of your agent for natural and humanlike AI behavior, and later improve the agents with the help of graphs. This book also covers the new navigational mesh with improved AI and pathfinding tools introduced in the Unity 2018 update. You’ll empower your AI with decision-making functions by programming simple board games, such as tic-tac-toe and checkers, and orchestrate agent coordination to get your AIs working together as one.

By the end of this book, you’ll have gained expertise in AI programming and developed creative and interactive games.

Publication date:
August 2018
Publisher
Packt
Pages
334
ISBN
9781788626170

 

Chapter 1. Behaviors - Intelligent Movement

In this chapter, we will develop AI algorithms for movement by covering the following recipes:

  • Creating the behaviors template
  • Pursuing and evading
  • Adjusting the agent for physics
  • Arriving and leaving
  • Facing objects
  • Wandering around
  • Following a path
  • Avoiding agents
  • Avoiding walls
  • Blending behaviors by weight
  • Blending behaviors by priority
  • Shooting a projectile
  • Predicting a projectile's landing spot
  • Targeting a projectile
  • Creating a jump system

 

 

 

Introduction


Unity has been one of the most popular game engines for quite a while now, and it's probably the de facto game development tool for indie developers, not only because of its business model, which has a low entry barrier, but also because of its robust project editor, year-by-year technological improvements, and, most importantly, its ease of use and an ever-growing community of developers around the globe.

Thanks to Unity's heavy lifting behind the scenes (rendering, physics, integration, and cross-platform deployment, to name but a few) it's possible for us to focus on creating the AI systems that will bring our games to life, creating great real-time experiences in the blink of an eye.

The goal of this book is to give you the tools to build great AI, create better enemies, polish that final boss, or even build your own customized AI engine.

In this chapter, we will start by exploring some of the most interesting movement algorithms based on the steering behavior principles developed by Craig Reynolds, along with work from Ian Millington. These recipes are the stepping stones for most of the AI used in advanced games and other algorithms that rely on movement, such as the family of path-finding algorithms.

 

Creating the behaviors template


Before creating our behaviors, we need to code the stepping stones that help us to create not only intelligent movement, but also help us to build a modular system to change and add these behaviors. We will create custom data types and base classes for most of the algorithms covered in this chapter.

Getting ready

Our first step is to remember the update functions' order of execution:

  • Update
  • LateUpdate

How to do it...

We need to create three classes, Steering, AgentBehaviour, and Agent:

  1. Steering serves as a custom data type for storing the movement and rotation of the agent:
using UnityEngine; 
public class Steering 
{ 
  public float angular; 
  public Vector3 linear; 
  public Steering () 
  { 
    angular = 0.0f; 
    linear = new Vector3(); 
  } 
} 
  1. AgentBehaviour is the template class for most of the behaviors covered in this chapter:
using UnityEngine; 
public class AgentBehaviour : MonoBehaviour 
{ 
  public GameObject target; 
  protected Agent agent; 
  public virtual void Awake () 
  { 
    agent = gameObject.GetComponent<Agent>(); 
  } 
  public virtual void Update () 
  { 
      agent.SetSteering(GetSteering()); 
  } 
  public virtual Steering GetSteering () 
  { 
    return new Steering(); 
  } 
} 
  1. Finally, Agent is the main component, and it makes use of behaviors in order to create intelligent movement. Create the file and its bare bones:
using UnityEngine; 
using System.Collections; 
public class Agent : MonoBehaviour 
{ 
    public float maxSpeed; 
    public float maxAccel; 
    public float orientation; 
    public float rotation; 
    public Vector3 velocity; 
    protected Steering steering; 
    void Start () 
    { 
        velocity = Vector3.zero; 
        steering = new Steering(); 
    } 
    public void SetSteering (Steering steering) 
    { 
        this.steering = steering; 
    } 
} 
  1. Next, we code the Update function, which handles the movement according to the current value:
public virtual void Update () 
{ 
    Vector3 displacement = velocity * Time.deltaTime; 
    orientation += rotation * Time.deltaTime; 
    // we need to limit the orientation values 
    // to be in the range (0 - 360) 
    if (orientation < 0.0f) 
        orientation += 360.0f; 
    else if (orientation > 360.0f) 
        orientation -= 360.0f; 
    transform.Translate(displacement, Space.World); 
    transform.rotation = new Quaternion(); 
    transform.Rotate(Vector3.up, orientation); 
} 
  1. Finally, we implement the LateUpdate function, which takes care of updating the steering for the next frame according to the current frame's calculations:
public virtual void LateUpdate () 
{ 
    velocity += steering.linear * Time.deltaTime; 
    rotation += steering.angular * Time.deltaTime; 
    if (velocity.magnitude > maxSpeed) 
    { 
        velocity.Normalize(); 
        velocity = velocity * maxSpeed; 
    } 
    if (steering.angular == 0.0f) 
    { 
        rotation = 0.0f; 
    } 
    if (steering.linear.sqrMagnitude == 0.0f) 
    { 
        velocity = Vector3.zero; 
    } 
    steering = new Steering(); 
} 

How it works...

The idea is to be able to delegate the movement's logic inside the GetSteering() function on the behaviors that we will later build, simplifying our agent's class to a main calculation based on those.

Besides, we are guaranteed to be able to set the agent's steering value before it is used thanks to Unity script and function execution orders.

There's more...

This is a component-based approach, which means that we must remember to always have an Agent script attached to GameObject for the behaviors to work as expected.

See also

For further information on Unity's game loop and the execution order of functions and scripts, please refer to the official documentation available online at these links:

 

Pursuing and evading


Pursuing and evading are great behaviors to start with because they rely on the most basic behaviors and extend their functionality by predicting the target's next step.

Getting ready

We need a couple of basic behaviors called Seek and Flee; place them right after the Agent class in the scripts' execution order.

The following is the code for the Seek behavior:

using UnityEngine; 
using System.Collections; 
public class Seek : AgentBehaviour 
{ 
    public override Steering GetSteering() 
    { 
        Steering steering = new Steering(); 
        steering.linear = target.transform.position - transform.position; 
        steering.linear.Normalize(); 
        steering.linear = steering.linear * agent.maxAccel; 
        return steering; 
    } 
} 

Also, we need to implement the Flee behavior:

using UnityEngine; 
using System.Collections; 
public class Flee : AgentBehaviour 
{ 
    public override Steering GetSteering() 
    { 
        Steering steering = new Steering(); 
        steering.linear = transform.position - target.transform.position; 
        steering.linear.Normalize(); 
        steering.linear = steering.linear * agent.maxAccel; 
        return steering; 
    } 
}

 

 

How to do it...

Pursue and Evade are essentially the same algorithm, but differ in terms of the base class they derive from:

  1. Create the Pursue class, derived from Seek, and add the attributes for the prediction:
using UnityEngine; 
using System.Collections; 
 
public class Pursue : Seek 
{ 
    public float maxPrediction; 
    private GameObject targetAux; 
    private Agent targetAgent; 
} 
  1. Implement the Awake function in order to set up everything according to the real target:
public override void Awake() { base.Awake(); targetAgent = target.GetComponent<Agent>(); targetAux = target; target = new GameObject(); } 
  1. Implement the OnDestroy function for properly handling the internal object:
void OnDestroy () 
{ 
    Destroy(targetAux); 
} 
  1. Implement the GetSteering function:
public override Steering GetSteering() 
{ 
    Vector3 direction = targetAux.transform.position - transform.position; 
    float distance = direction.magnitude; 
    float speed = agent.velocity.magnitude; 
    float prediction; 
    if (speed <= distance / maxPrediction) 
        prediction = maxPrediction; 
    else 
        prediction = distance / speed; 
    target.transform.position = targetAux.transform.position; 
    target.transform.position += targetAgent.velocity * prediction; 
    return base.GetSteering(); 
} 
  1. To create the Evade behavior, the procedure is just the same, but it takes into account the fact that Flee is the parent class:
public class Evade : Flee 
{ 
    // everything stays the same 
} 

How it works...

These behaviors rely on Seek and Flee and take into consideration the target's velocity in order to predict where it will go next, and they aim at that position using an internal extra object.

 

Adjusting the agent for physics


We learned how to implement simple behaviors for our agents. However, we also need to take into consideration that our games will probably need the help of the physics engine in Unity. In that case, we need to take into consideration whether our agents have the RigidBody component attached to them, and adjust our implementation accordingly.

Getting ready

Our first step is to remember the execution order of event functions, but now we also consider FixedUpdate, because we're handling behaviors on top of the physics engine:

  • FixedUpdate
  • Update
  • LateUpdate

How to do it...

This recipe entails adding changes to our Agent class.

  1. Go to the Agent class.
  2. Add a member variable for storing the rigid body component's reference:
private Rigidbody aRigidBody; 
  1. Get the reference to the rigid body component in the Start function:
aRigidBody = GetComponent<Rigidbody>(); 
  1. Implement a function for transforming an orientation value to a vector:
public Vector3 OriToVec(float orientation) 
{ 
  Vector3 vector = Vector3.zero; 
  vector.x = Mathf.Sin(orientation * Mathf.Deg2Rad) * 1.0f; 
  vector.z = Mathf.Cos(orientation * Mathf.Deg2Rad) * 1.0f; 
  return vector.normalized; 
} 
  1. Add the following two lines at the beginning of the Update function:
public virtual void Update () 
{ 
  if (aRigidBody == null) 
    return; 
  // ... previous code 
  1. Define the FixedUpdate function:
public virtual void FixedUpdate() 
{ 
  if (aRigidBody == null) 
    return; 
  // next step 
} 
  1. Continue implementing the FixedUpdate function:
Vector3 displacement = velocity * Time.deltaTime; 
orientation += rotation * Time.deltaTime; 
if (orientation < 0.0f) 
  orientation += 360.0f; 
else if (orientation > 360.0f) 
  orientation -= 360.0f; 
// The ForceMode will depend on what you want to achieve 
// We are using VelocityChange for illustration purposes 
aRigidBody.AddForce(displacement, ForceMode.VelocityChange); 
Vector3 orientationVector = OriToVec(orientation); 
aRigidBody.rotation = Quaternion.LookRotation(orientationVector, Vector3.up); 

How it works...

We added a member variable for storing the reference to a possible rigid body component, and also implemented FixedUpdate, similar to Update, but taking into consideration that we need to apply force to the rigid body instead of translating the object ourselves, because we're working on top of Unity's physics engine.

Finally, we created a simple validation at the beginning of each function so they're called only when it applies.

See also

For further information on the execution order of event functions, please refer to official Unity documentation:

 

Arriving and leaving


Similar to Seek and Flee, the idea behind these algorithms is to apply the same principles and extend the functionality to a point where the agent stops automatically after a condition is met, either being close to its destination (arrive), or far enough from a dangerous point (leave).

Getting ready

We need to create one file for the Arrive and Leave algorithms respectively, and remember to set their custom execution order.

How to do it...

They use the same approach, but in terms of implementation, the name of the member variables change, as well as some computations in the first half of the GetSteering function:

  1. First, implement the Arrive behavior with its member variables to define the radius for stopping (target) and slowing down:
using UnityEngine; 
using System.Collections; 
 
public class Arrive : AgentBehaviour 
{ 
    public float targetRadius; 
    public float slowRadius; 
    public float timeToTarget = 0.1f; 
} 
  1. Create the GetSteering function:
public override Steering GetSteering() 
{ 
    // code in next steps 
} 
  1. Define the first half of the GetSteering function, in which we compute the desired speed depending on the distance from the target according to the radii variables:
Steering steering = new Steering(); 
Vector3 direction = target.transform.position - transform.position; 
float distance = direction.magnitude; 
float targetSpeed; 
if (distance < targetRadius) 
    return steering; 
if (distance > slowRadius) 
    targetSpeed = agent.maxSpeed; 
else 
    targetSpeed = agent.maxSpeed * distance / slowRadius; 
  1. Define the second half of the GetSteering function, in which we set the steering value and clamp it according to the maximum speed:
Vector3 desiredVelocity = direction; 
desiredVelocity.Normalize(); 
desiredVelocity *= targetSpeed; 
steering.linear = desiredVelocity - agent.velocity; 
steering.linear /= timeToTarget; 
if (steering.linear.magnitude > agent.maxAccel) 
{ 
    steering.linear.Normalize(); 
    steering.linear *= agent.maxAccel; 
} 
return steering; 
  1. To implement Leave, change the name of the member variables:
using UnityEngine; 
using System.Collections; 
 
public class Leave : AgentBehaviour 
{ 
    public float escapeRadius; 
    public float dangerRadius; 
    public float timeToTarget = 0.1f; 
} 
  1. Define the first half of the GetSteering function:
Steering steering = new Steering(); 
Vector3 direction = transform.position - target.transform.position; 
float distance = direction.magnitude; 
if (distance > dangerRadius) 
    return steering; 
float reduce; 
if (distance < escapeRadius) 
    reduce = 0f; 
else 
    reduce = distance / dangerRadius * agent.maxSpeed; 
float targetSpeed = agent.maxSpeed - reduce; 
  1. And finally, the second half of GetSteering stays just the same.

How it works...

After calculating the direction to go in, the next calculations are based on two radii distances in order to know when to go full throttle, slow down, and stop; that's why we have several if statements. In the Arrive behavior, when the agent is too far, we aim for full throttle, progressively slow down when inside the proper radius, and finally stop when close enough to the target. The inverse method applies to Leave:

A visual reference for the Arrive and Leave behaviors

 

Facing objects


Real-world aiming, just like in combat simulators, works a little differently to the widely-used automatic aiming process in almost every game. Imagine that you need to implement an agent controlling a tank turret or a humanized sniper; that's when this recipe comes in handy.

Getting ready

We need to make some modifications to our AgentBehaviour class:

  1. Add new member values to limit some of the existing ones:
public float maxSpeed; 
public float maxAccel; 
public float maxRotation; 
public float maxAngularAccel;

 

 

 

 

  1. Add a function called MapToRange. This function helps in finding the actual direction of rotation after two orientation values are subtracted:
public float MapToRange (float rotation) { 
    rotation %= 360.0f; 
    if (Mathf.Abs(rotation) > 180.0f) { 
        if (rotation < 0.0f) 
            rotation += 360.0f; 
        else 
            rotation -= 360.0f; 
    } 
    return rotation; 
} 
  1. Also, we need to create a basic behavior called Align that is the stepping stone for the facing algorithm. It uses the same principle as Arrive, but only in terms of rotation:
using UnityEngine; 
using System.Collections; 
 
public class Align : AgentBehaviour 
{ 
    public float targetRadius; 
    public float slowRadius; 
    public float timeToTarget = 0.1f; 
 
    public override Steering GetSteering() 
    { 
        Steering steering = new Steering(); 
        float targetOrientation = target.GetComponent<Agent>().orientation; 
        float rotation = targetOrientation - agent.orientation; 
        rotation = MapToRange(rotation); 
        float rotationSize = Mathf.Abs(rotation); 
        if (rotationSize < targetRadius) 
            return steering; 
        float targetRotation; 
        if (rotationSize > slowRadius) 
            targetRotation = agent.maxRotation; 
        else 
            targetRotation = agent.maxRotation * rotationSize / slowRadius; 
        targetRotation *= rotation / rotationSize; 
        steering.angular = targetRotation - agent.rotation; 
        steering.angular /= timeToTarget; 
        float angularAccel = Mathf.Abs(steering.angular); 
        if (angularAccel > agent.maxAngularAccel) 
        { 
            steering.angular /= angularAccel; 
            steering.angular *= agent.maxAngularAccel; 
        } 
        return steering; 
    } 
} 

How to do it...

We can now proceed to implement our facing algorithm that derives from Align:

  1. Create the Face class along with a private auxiliary target member variable:
using UnityEngine; 
using System.Collections; 
 
public class Face : Align 
{ 
    protected GameObject targetAux; 
} 
  1. Override the Awake function to set up everything and swap references:
public override void Awake() 
{ 
    base.Awake(); 
    targetAux = target; 
    target = new GameObject(); 
    target.AddComponent<Agent>(); 
} 
  1. Also, implement the OnDestroy function to handle references and avoid memory issues:
void OnDestroy () 
{ 
    Destroy(target); 
}

 

 

 

 

 

 

  1. Finally, define the GetSteering function:
public override Steering GetSteering() 
{ 
    Vector3 direction = targetAux.transform.position - transform.position; 
    if (direction.magnitude > 0.0f) 
    { 
        float targetOrientation = Mathf.Atan2(direction.x, direction.z); 
        targetOrientation *= Mathf.Rad2Deg; 
        target.GetComponent<Agent>().orientation = targetOrientation; 
    } 
    return base.GetSteering(); 
}

How it works...

The algorithm computes the internal target orientation according to the vector between the agent and the real target. Then, it just delegates the work to its parent class.

 

Wandering around


This technique works like a charm for random crowd simulations, animals, and almost any kind of NPC that requires random movement when idle.

Getting ready

We need to add another function to our AgentBehaviour class called OriToVec, which converts an orientation value to a vector:

public Vector3 GetOriAsVec (float orientation) { 
    Vector3 vector  = Vector3.zero; 
    vector.x = Mathf.Sin(orientation * Mathf.Deg2Rad) * 1.0f; 
    vector.z = Mathf.Cos(orientation * Mathf.Deg2Rad) * 1.0f; 
    return vector.normalized; 
} 

How to do it...

We can regard it as a big three-step process in which we first manipulate the internal target position in a parameterized random way, face that position, and move accordingly:

  1. Create the Wander class deriving from Face:
using UnityEngine; 
using System.Collections; 
 
public class Wander : Face 
{ 
    public float offset; 
    public float radius; 
    public float rate; 
} 
  1. Define the Awake function in order to set up the internal target:
public override void Awake() 
{ 
    target = new GameObject(); 
    target.transform.position = transform.position; 
    base.Awake(); 
} 
  1. Define the GetSteering function:
public override Steering GetSteering() 
{ 
    Steering steering = new Steering(); 
    float wanderOrientation = Random.Range(-1.0f, 1.0f) * rate; 
    float targetOrientation = wanderOrientation + agent.orientation; 
    Vector3 orientationVec = OriToVec(agent.orientation); 
    Vector3 targetPosition = (offset * orientationVec) + transform.position; 
    targetPosition = targetPosition + (OriToVec(targetOrientation) * radius); 
    targetAux.transform.position = targetPosition; 
    steering = base.GetSteering(); 
    steering.linear = targetAux.transform.position - transform.position; 
    steering.linear.Normalize(); 
    steering.linear *= agent.maxAccel; 
    return steering; 
} 

How it works...

The behavior takes two radii in order into consideration to get a random position to go next, looks toward that random point, and converts the computed orientation into a direction vector in order to advance:

A visual description of the parameters for creating the Wander behavior

 

Following a path


There are times when we need scripted routes, and it's simply inconceivable to do it entirely by code. Imagine you're working on a stealth game. Would you code a route for every single guard? This technique will help you build a flexible path system for those situations.

Getting ready

We need to define a custom data type called PathSegment:

using UnityEngine; 
using System.Collections; 
 
public class PathSegment 
{ 
    public Vector3 a; 
    public Vector3 b; 
 
    public PathSegment () : this (Vector3.zero, Vector3.zero){} 
    public PathSegment (Vector3 a, Vector3 b) 
    { 
        this.a = a; 
        this.b = b; 
    } 
} 

How to do it...

This is a long recipe that could be regarded as a big two-step process. First, we build the Path class, which abstracts points in the path from their specific spatial representations, and then we build the PathFollower behavior that makes use of that abstraction in order to get actual spatial points to follow:

  1. Create the Path class, which consists of nodes and segments; only the nodes are public and are assigned manually:
using UnityEngine; 
using System.Collections; 
using System.Collections.Generic; 
 
public class Path : MonoBehaviour 
{ 
    public List<GameObject> nodes; 
    List<PathSegment> segments; 
} 
  1. Define the Start function to set the segments when the scene starts:
void Start() 
{ 
    segments = GetSegments(); 
} 
  1. Define the GetSegments function to build the segments from the nodes:
public List<PathSegment> GetSegments () 
{ 
    List<PathSegment> segments = new List<PathSegment>(); 
    int i; 
    for (i = 0; i < nodes.Count - 1; i++) 
    { 
        Vector3 src = nodes[i].transform.position; 
        Vector3 dst = nodes[i+1].transform.position; 
        PathSegment segment = new PathSegment(src, dst); 
        segments.Add(segment); 
    } 
    return segments; 
} 
  1. Define the first function for abstraction, which is called GetParam:
public float GetParam(Vector3 position, float lastParam) 
{ 
    // body 
} 
  1. We need to find out the segment the agent is closest to:
float param = 0f; 
PathSegment currentSegment = null; 
float tempParam = 0f; 
foreach (PathSegment ps in segments) 
{ 
    tempParam += Vector3.Distance(ps.a, ps.b); 
    if (lastParam <= tempParam) 
    { 
        currentSegment = ps; 
        break; 
    } 
} 
if (currentSegment == null) 
    return 0f; 
  1. Given the current position, we need to work out the direction to go to:
Vector3 currPos = position - currentSegment.a; 
Vector3 segmentDirection = currentSegment.b - currentSegment.a; 
segmentDirection.Normalize(); 
  1. Find the point in the segment using vector projection:
Vector3 pointInSegment = Vector3.Project(currPos, segmentDirection); 
  1. Finally, GetParam returns the next position to reach along the path:
param = tempParam - Vector3.Distance(currentSegment.a, currentSegment.b);
param += pointInSegment.magnitude; 
return param;
  1. Define the GetPosition function:
public Vector3 GetPosition(float param)  
{ 
    // body 
} 
  1. Given the current location along the path, we find the corresponding segment:
Vector3 position = Vector3.zero; 
PathSegment currentSegment = null; 
float tempParam = 0f; 
foreach (PathSegment ps in segments) 
{ 
    tempParam += Vector3.Distance(ps.a, ps.b); 
    if (param <= tempParam) 
    { 
        currentSegment = ps; 
        break; 
    } 
} 
if (currentSegment == null) 
    return Vector3.zero; 
  1. GetPosition converts the parameter as a spatial point and returns it:
Vector3 segmentDirection = currentSegment.b - currentSegment.a; 
segmentDirection.Normalize(); 
tempParam -= Vector3.Distance(currentSegment.a, currentSegment.b); 
tempParam = param - tempParam; 
position = currentSegment.a + segmentDirection * tempParam; 
return position; 
  1. Create the PathFollower behavior, which derives from Seek (remember to set the order of execution):
using UnityEngine; 
using System.Collections; 
 
public class PathFollower : Seek 
{ 
    public Path path; 
    public float pathOffset = 0.0f; 
    float currentParam; 
}

 

 

  1. Implement the Awake function to set the target:
public override void Awake() 
{ 
    base.Awake(); 
    target = new GameObject(); 
    currentParam = 0f; 
} 
  1. The final step is to define the GetSteering function that relies on the abstraction created by the Path class to set the target position and apply Seek:
public override Steering GetSteering() 
{ 
    currentParam = path.GetParam(transform.position, currentParam); 
    float targetParam = currentParam + pathOffset; 
    target.transform.position = path.GetPosition(targetParam); 
    return base.GetSteering(); 
} 

How it works...

We use the Path class in order to have a movement guideline. It is the cornerstone, because it relies on GetParam to map an offset point to follow in its internal guideline, and it also uses GetPosition to convert that referential point to a position in the three-dimensional space along the segments.

The path-following algorithm just makes use of the path's functions in order to get a new position, update the target, and apply the Seek behavior.

There's more...

It's important to take into account the order in which the nodes are linked in the inspector for the path to work as expected. A practical way to achieve this is to manually name the nodes with a reference number:

An example of a path set up in the Inspector window

Also, we could define the OnDrawGizmos function in order to have a better visual reference of the path:

void OnDrawGizmos () 
{ 
    Vector3 direction; 
    Color tmp = Gizmos.color; 
    Gizmos.color = Color.magenta;//example color 
    int i; 
    for (i = 0; i < nodes.Count - 1; i++) 
    { 
        Vector3 src = nodes[i].transform.position; 
        Vector3 dst = nodes[i+1].transform.position; 
        direction = dst - src; 
        Gizmos.DrawRay(src, direction); 
    } 
    Gizmos.color = tmp; 
} 
 

Avoiding agents


In crowd-simulation games, it would be unnatural to see agents behaving entirely like particles in a physics-based system. The goal of this recipe is to create an agent capable of mimicking our peer-evasion movement.

Getting ready

We need to create a tag called Agent and assign it to those game objects that we would like to avoid, and we also need to have the Agent script component attached to them:

Example of how the Inspector window of a dummy agent should appear

Take a look on the following:

  • Tag: Agent (created by us)
  • Agent component is attached (the one created by us)

How to do it...

This recipe will entail creating a new agent behavior:

  1. Create the AvoidAgent behavior, which is composed of a collision avoidance radius and the list of agents to avoid:
using UnityEngine; 
using System.Collections; 
using System.Collections.Generic; 
 
public class AvoidAgent : AgentBehaviour 
{ 
    public float collisionRadius = 0.4f; 
    GameObject[] targets; 
} 
  1. Implement the Start function in order to set the list of agents according to the tag we created earlier:
void Start () 
{ 
    targets = GameObject.FindGameObjectsWithTag("Agent"); 
} 
  1. Define the GetSteering function:
public override Steering GetSteering() 
{ 
    // body 
} 
  1. Add the following variables to the compute distances and velocities from agents that are nearby:
Steering steering = new Steering(); 
float shortestTime = Mathf.Infinity; 
GameObject firstTarget = null; 
float firstMinSeparation = 0.0f; 
float firstDistance = 0.0f; 
Vector3 firstRelativePos = Vector3.zero; 
Vector3 firstRelativeVel = Vector3.zero; 
  1. Find the closest agent that is prone to collision with the current one:
foreach (GameObject t in targets) 
{ 
    Vector3 relativePos; 
    Agent targetAgent = t.GetComponent<Agent>(); 
    relativePos = t.transform.position - transform.position; 
    Vector3 relativeVel = targetAgent.velocity - agent.velocity; 
    float relativeSpeed = relativeVel.magnitude; 
    float timeToCollision = Vector3.Dot(relativePos, relativeVel); 
    timeToCollision /= relativeSpeed * relativeSpeed * -1; 
    float distance = relativePos.magnitude; 
    float minSeparation = distance - relativeSpeed * timeToCollision; 
    if (minSeparation > 2 * collisionRadius) 
        continue; 
    if (timeToCollision > 0.0f && timeToCollision < shortestTime) 
    { 
        shortestTime = timeToCollision; 
        firstTarget = t; 
        firstMinSeparation = minSeparation; 
        firstRelativePos = relativePos; 
        firstRelativeVel = relativeVel; 
    } 
} 
  1. If there is one that is prone to collision, then move away:
if (firstTarget == null) 
    return steering; 
if (firstMinSeparation <= 0.0f || firstDistance < 2 * collisionRadius) 
    firstRelativePos = firstTarget.transform.position; 
else 
    firstRelativePos += firstRelativeVel * shortestTime; 
firstRelativePos.Normalize(); 
steering.linear = -firstRelativePos * agent.maxAccel; 
return steering; 

How it works...

Given a list of agents, we take into consideration which one is closest, and, if it is close enough, we make it so that the agent tries to escape from the expected route of that first one according to its current velocity, so that they don't collide.

There's more...

This behavior works well when combined with other behaviors using blending techniques (some are included in this chapter); otherwise, it's a starting point for your own collision avoidance algorithms.

 

Avoiding walls


In this recipe, we will implement a behavior that imitates our own ability to evade walls. That is, seeing what we have in front of us that could be considered as a wall or obstacle, and walk around it using a safety margin, trying to maintain our principal direction at the same time.

Getting ready

This technique uses the RaycastHit structure and the Raycast function from the physics engine, so it's recommended that you look at the documents for a refresher in case you're a little rusty on the subject.

How to do it...

Thanks to our previous hard work, this recipe is a short one:

  1. Create the AvoidWall behavior derived from Seek:
using UnityEngine; 
using System.Collections; 
 
public class AvoidWall : Seek 
{ 
    // body 
} 
  1. Include the member variables for defining the safety margin and the length of the ray to cast:
public float avoidDistance; 
public float lookAhead; 
  1. Define the Awake function to set up the target:
public override void Awake() 
{ 
    base.Awake(); 
    target = new GameObject(); 
} 
  1. Define the GetSteering function required for future steps:
public override Steering GetSteering() 
{ 
    // body 
}

 

 

  1. Declare and set the variable needed for ray casting:
Steering steering = new Steering(); 
Vector3 position = transform.position; 
Vector3 rayVector = agent.velocity.normalized * lookAhead; 
Vector3 direction = rayVector; 
RaycastHit hit; 
  1. Cast the ray and make the proper calculations if a wall is hit:
if (Physics.Raycast(position, direction, out hit, lookAhead)) 
{ 
    position = hit.point + hit.normal * avoidDistance; 
    target.transform.position = position; 
    steering = base.GetSteering(); 
} 
return steering; 

How it works...

We cast a ray in front of the agent, and when the ray collides with a wall, the target object is placed in a new position, with consideration given to its distance from the wall and the safety distance declared, delegating the steering calculations to the Seek behavior; this creates the illusion of the agent avoiding the wall.

There's more...

We could extend this behavior by adding more rays, such as whiskers, in order to achieve better accuracy. Also, it is usually paired with other movement behaviors, such as Pursue, using blending:

 

The original ray cast and possible extensions for more precise wall avoidance

See also

For further information on the RaycastHit structure and the Raycast function, please refer to the official documentation available online at these links:

 

Blending behaviors by weight


The blending techniques allow you to add behaviors and mix them without creating new scripts every time you need a new type of hybrid agent.

This is one of the most powerful techniques in this chapter, and it's probably the most widely-used behavior-blending approach because of its power and the low cost of implementation.

Getting ready

We must add a new member variable to our AgentBehaviour class called weight and preferably assign a default value. In this case, this is 1.0f. We should refactor the Update function to incorporate weight as a parameter for the Agent class's SetSteering function. All in all, the new AgentBehaviour class should look something like this:

public class AgentBehaviour : MonoBehaviour 
{ 
    public float weight = 1.0f; 
 
    // ... the rest of the class 
 
    public virtual void Update () 
    { 
        agent.SetSteering(GetSteering(), weight); 
   } 
}

 

 

How to do it...

We just need to change the SetSteering function's signature and definition:

public void SetSteering (Steering steering, float weight) 
{ 
    this.steering.linear += (weight * steering.linear); 
    this.steering.angular += (weight * steering.angular); 
} 

How it works...

The weights are used to amplify the steering behavior result, and they're added to the main steering structure.

There's more...

The weights don't necessarily need to add up to 1.0f. The weight parameter is a reference for defining the relevance that the steering behavior will have among other parameters.

See also

In this project, there is an example of avoiding walls, which is worked out using weighted blending.

 

Blending behaviors by priority


Sometimes, weighted blending is not enough because heavyweight behaviors dilute the contribution of the lightweights, but those behaviors need to do their part too. That's when priority-based blending comes into play, applying a cascading effect from high-priority to low-priority behaviors.

 

Getting ready

This approach is very similar to the one used in the previous recipe. We must add a new member variable to our AgentBehaviour class. We should also refactor the Update function to incorporate priority as a parameter to the Agent class's SetSteering function. The new AgentBehaviour class should look something like this:

public class AgentBehaviour : MonoBehaviour 
{ 
    public int priority = 1; 
    // ... everything else stays the same 
    public virtual void Update () 
    { 
        agent.SetSteering(GetSteering(), priority); 
    } 
} 

How to do it...

Now, we need to make some changes to the Agent class:

  1. Add a new namespace from the library:
using System.Collections.Generic; 
  1. Add the member variable for the minimum steering value to consider a group of behaviors:
public float priorityThreshold = 0.2f; 
  1. Add the member variable for holding the group of behavior results:
private Dictionary<int, List<Steering>> groups; 
  1. Initialize the variable in the Start function:
groups = new Dictionary<int, List<Steering>>(); 
  1. Modify the LateUpdate function so that the steering variable is set by calling GetPrioritySteering:
public virtual void LateUpdate () 
{ 
    //  funnelled steering through priorities 
    steering = GetPrioritySteering(); 
    groups.Clear(); 
    // ... the rest of the computations stay the same 
    steering = new Steering(); 
} 
  1. Modify the SetSteering function's signature and definition to store the steering values in their corresponding priority groups:
public void SetSteering (Steering steering, int priority) 
{ 
    if (!groups.ContainsKey(priority)) 
    { 
        groups.Add(priority, new List<Steering>()); 
    } 
    groups[priority].Add(steering); 
} 
  1. Finally, implement the GetPrioritySteering function to funnel the steering group:
private Steering GetPrioritySteering () 
{ 
    Steering steering = new Steering(); 
    float sqrThreshold = priorityThreshold * priorityThreshold; 
    foreach (List<Steering> group in groups.Values) 
    { 
        steering = new Steering(); 
        foreach (Steering singleSteering in group) 
        { 
            steering.linear += singleSteering.linear; 
            steering.angular += singleSteering.angular; 
        } 
        if (steering.linear.sqrMagnitude > sqrThreshold || 
                Mathf.Abs(steering.angular) > priorityThreshold) 
        { 
            return steering; 
        } 
    } 
    return steering; 
}

How it works...

By creating priority groups, we blend behaviors that are common to one another, and the first group, in which the steering value exceeds the threshold, is selected. Otherwise, steering from the lowest-priority group is chosen.

 

There's more...

We could extend this approach by mixing it with weighted blending, so we would have a more robust architecture, achieving extra precision in the way the behaviors impact on the agent at every priority level:

foreach (Steering singleSteering in group) 
{ 
    steering.linear += singleSteering.linear * weight; 
    steering.angular += singleSteering.angular * weight; 
} 

See also

There is an example of avoiding walls using priority-based blending in this project.

 

Shooting a projectile


This is the stepping stone for scenarios where we want to have control over gravity-reliant objects, such as balls and grenades, so we can then predict the projectile's landing spot or be able to effectively shoot a projectile at a given target.

Getting ready

This recipe differs a little bit as it doesn't rely on the base AgentBehaviour class.

 

How to do it...

  1. Create the Projectile class, along with its member variables, to handle the physics:
using UnityEngine; 
using System.Collections; 
 
public class Projectile : MonoBehaviour 
{ 
    private bool set = false; 
    private Vector3 firePos; 
    private Vector3 direction; 
    private float speed; 
    private float timeElapsed; 
} 
  1. Define the Update function:
void Update () 
{ 
    if (!set) 
        return; 
    timeElapsed += Time.deltaTime; 
    transform.position = firePos + direction * speed * timeElapsed; 
    transform.position += Physics.gravity * (timeElapsed * timeElapsed) / 2.0f; 
    // extra validation for cleaning the scene 
    if (transform.position.y < -1.0f) 
        Destroy(this.gameObject);// or set = false; and hide it 
} 
  1. Finally, implement the Set function in order to fire the game object (for example, calling it after it is instantiated in the scene):
public void Set (Vector3 firePos, Vector3 direction, float speed) 
{ 
    this.firePos = firePos; 
    this.direction = direction.normalized; 
    this.speed = speed; 
    transform.position = firePos; 
    set = true; 
} 

How it works...

This behavior uses high school physics in order to generate the parabolic movement.

There's more...

We could also take another approach: implementing public properties in the script or declaring member variables as public, and instead of calling the Set function, having the script disabled by default in the prefab and enabling it after all the properties have been set. That way, we could easily apply the object pool pattern.

See also

For further information on the object pool pattern, please refer to the following Wikipedia article and the official Unity technologies video tutorial available online:

 

Predicting a projectile's landing spot


After a projectile is shot by the player, agents (our AI) need to either avoid it or look for it. For example, the agents need to run from a grenade to be kept alive, or run towards a soccer ball to take control. In either case, it's important for the agents to predict the projectile's landing spot to make decisions.

In this recipe, we will learn how to calculate such landing spots.

Getting ready

Before we get into predicting the landing position, it's important to know the time left before it hits the ground (or reaches a certain position). Thus, instead of creating new behaviors, we need to update the Projectile class.

How to do it...

  1. First, we need to add the GetLandingTime function to compute the landing time:
public float GetLandingTime (float height = 0.0f) 
{ 
    Vector3 position = transform.position; 
    float time = 0.0f; 
    float valueInt = (direction.y * direction.y) * (speed * speed); 
    valueInt = valueInt - (Physics.gravity.y * 2 * (position.y - height)); 
    valueInt = Mathf.Sqrt(valueInt); 
    float valueAdd = (-direction.y) * speed; 
    float valueSub = (-direction.y) * speed; 
    valueAdd = (valueAdd + valueInt) / Physics.gravity.y; 
    valueSub = (valueSub - valueInt) / Physics.gravity.y; 
    if (float.IsNaN(valueAdd) && !float.IsNaN(valueSub)) 
        return valueSub; 
    else if (!float.IsNaN(valueAdd) && float.IsNaN(valueSub)) 
        return valueAdd; 
    else if (float.IsNaN(valueAdd) && float.IsNaN(valueSub)) 
        return -1.0f; 
    time = Mathf.Max(valueAdd, valueSub); 
    return time; 
} 
  1. Now, we add the GetLandingPos function to predict the landing spot:
public Vector3 GetLandingPos (float height = 0.0f) 
{ 
    Vector3 landingPos = Vector3.zero; 
    float time = GetLandingTime(); 
    if (time < 0.0f) 
        return landingPos; 
    landingPos.y = height; 
    landingPos.x = firePos.x + direction.x * speed * time; 
    landingPos.z = firePos.z + direction.z * speed * time; 
    return landingPos; 
}

How it works...

First, we are solving the equation from the previous recipe for a fixed height, and, given the projectile's current position and speed, we are able to get the time at which the projectile will reach the given height.

There's more...

Remember to take into account the NaN validation. It's placed that way because there may be one, two, or no solutions to the equation. Furthermore, when the landing time is less than zero, it means the projectile won't be able to reach the target height.

 

Targeting a projectile


Just as it's important to predict a projectile's landing point, it's also important to develop intelligent agents capable of aiming projectiles. It wouldn't be fun if our rugby-player agents weren't capable of passing the ball.

Getting ready

Just as in the previous recipe, we only need to expand the Projectile class.

How to do it...

Thanks to our previous hard work, this recipe is a real piece of cake:

  1. Create the GetFireDirection function:
public static Vector3 GetFireDirection (Vector3 startPos, Vector3 endPos, float speed) 
{ 
    // body 
}

 

  1. Solve the corresponding quadratic equation:
Vector3 direction = Vector3.zero; 
Vector3 delta = endPos - startPos; 
float a = Vector3.Dot(Physics.gravity, Physics.gravity); 
float b = -4 * (Vector3.Dot(Physics.gravity, delta) + speed * speed); 
float c = 4 * Vector3.Dot(delta, delta); 
if (4 * a * c > b * b) 
    return direction; 
float time0 = Mathf.Sqrt((-b + Mathf.Sqrt(b * b - 4 * a * c)) / (2*a)); 
float time1 = Mathf.Sqrt((-b - Mathf.Sqrt(b * b - 4 * a * c)) / (2*a)); 
  1. If shooting the projectile is feasible given the parameters, return a non-zero direction vector:
float time; 
if (time0 < 0.0f) 
{ 
    if (time1 < 0) 
        return direction; 
    time = time1; 
} 
else 
{ 
    if (time1 < 0) 
        time = time0; 
    else 
        time = Mathf.Min(time0, time1); 
} 
direction = 2 * delta - Physics.gravity * (time * time); 
direction = direction / (2 * speed * time); 
return direction; 

How it works...

Given a fixed speed, we solve the corresponding quadratic equation in order to obtain the desired direction (when at least a single one-time value is available), which doesn't need to be normalized because we already normalized the vector while setting up the projectile.

 

 

There's more...

Take account of the fact that we are returning a blank direction when the time is negative; it means that the speed is not sufficient. One way to overcome this is to define a function that tests different speeds and then shoot the projectile.

Another relevant improvement is to add an extra parameter of the type bool for those cases when we have two valid times (which means two possible arcs), and we need to shoot over an obstacle such as a wall:

if (isWall) 
    time = Mathf.Max(time0, time1); 
else 
    time = Mathf.Min(time0, time1); 
 

Creating a jump system


Imagine that we're developing a cool action game where the player is capable of escaping using cliffs and rooftops. In that case, the enemies need to be able to chase the player and be smart enough to discern whether to take the jump and gauge how to do it.

Getting ready

We need to create a basic matching-velocity algorithm and the notion of jump pads and landing pads in order to emulate velocity math so that we can reach them.

The following is the code for the VelocityMatch behavior:

using UnityEngine; 
using System.Collections; 
 
public class VelocityMatch : AgentBehaviour { 
     
    public float timeToTarget = 0.1f; 
 
    public override Steering GetSteering() 
    { 
        Steering steering = new Steering(); 
        steering.linear = target.GetComponent<Agent>().velocity - agent.velocity; 
        steering.linear /= timeToTarget; 
        if (steering.linear.magnitude > agent.maxAccel) 
            steering.linear = steering.linear.normalized * agent.maxAccel; 
 
        steering.angular = 0.0f; 
        return steering; 
    } 
} 

Also, it's important to create a data type called JumpPoint:

using UnityEngine; 
using System.Collections; 
 
public class JumpPoint  
{ 
    public Vector3 jumpLocation; 
    public Vector3 landingLocation; 
 
    //The change in position from jump to landing 
    public Vector3 deltaPosition; 
 
    public JumpPoint () : this (Vector3.zero, Vector3.zero) 
    { 
    } 
 
    public JumpPoint(Vector3 a, Vector3 b) 
    { 
        this.jumpLocation = a; 
        this.landingLocation = b; 
        this.deltaPosition = this.landingLocation - this.jumpLocation; 
    } 
} 

How to do it...

  1. Create the Jump script along with its member variables:
using UnityEngine; 
using System.Collections; 
 
public class Jump : VelocityMatch 
{ 
    public JumpPoint jumpPoint; 
    public float maxYVelocity; 
    public Vector3 gravity = new Vector3(0, -9.8f, 0); 
    bool canAchieve = false; 
}

 

 

  1. Implement the SetJumpPoint function:
public void SetJumpPoint(Transform jumpPad, Transform landingPad) 
{ 
    jumpPoint = new JumpPoint(jumpPad.position, landingPad.position); 
} 
  1. Add a function to calculate the target:
protected void CalculateTarget() 
{ 
    target = new GameObject(); 
    target.AddComponent<Agent>(); 
    target.transform.position = jumpPoint.jumpLocation; 
    //Calculate the first jump time 
    float sqrtTerm = Mathf.Sqrt(2f * gravity.y * jumpPoint.deltaPosition.y + maxYVelocity * agent.maxSpeed); 
    float time = (maxYVelocity - sqrtTerm) / gravity.y; 
    //Check if we can use it, otherwise try the other time 
    if (!CheckJumpTime(time)) 
    { 
        time = (maxYVelocity + sqrtTerm) / gravity.y; 
    } 
} 
  1. Implement the CheckJumpTime function, to decide whether it's worth taking the jump:
private bool CheckJumpTime(float time) 
{ 
    //Calculate the planar speed 
    float vx = jumpPoint.deltaPosition.x / time; 
    float vz = jumpPoint.deltaPosition.z / time; 
    float speedSq = vx * vx + vz * vz; 
    //Check it to see if we have a valid solution 
    if (speedSq < agent.maxSpeed * agent.maxSpeed) 
    { 
        target.GetComponent<Agent>().velocity = new Vector3(vx, 0f, vz); 
        canAchieve = true; 
        return true; 
    } 
    return false; 
}

 

 

  1. Finally, define the GetSteering function:
public override Steering GetSteering() 
{ 
    Steering steering = new Steering(); 
    if (target == null) 
    { 
        CalculateTarget(); 
    } 
    if (!canAchieve) 
    { 
        return steering; 
    } 
    //Check if we've hit the jump point 
    if (Mathf.Approximately((transform.position - target.transform.position).magnitude, 0f) && 
        Mathf.Approximately((agent.velocity - target.GetComponent<Agent>().velocity).magnitude, 0f)) 
    { 
        // call a jump method based on the Projectile behaviour 
        return steering; 
    } 
    return base.GetSteering(); 
} 

How it works...

The algorithm takes into account the agent's velocity and calculates whether it can reach the landing pad or not. If it judges that the agent can, it tries to match the vertical velocity while seeking the landing pad's position.

About the Author

  • Jorge Palacios

    Jorge Palacios is a software and game developer with a BS in computer science and eight years of professional experience. He's been developing games for the last five years in different roles, from tool developer to lead programmer. Mainly focused on artificial intelligence and gameplay programming, he is currently working with Unity and HTML5. He's also a game-programming instructor, speaker, and game-jam organizer.

    Browse publications by this author

Latest Reviews

(4 reviews total)
很優惠的價格,希望可以常常辦這樣的活動,不過有些檔案太大,不可以傳到kindle有點可惜
Los libros son muy buenos, pero hace falta un control de calidar para los cursos en video.
Just the right book at the correct time

Recommended For You

Unity Artificial Intelligence Programming - Fourth Edition

Learn and Implement game AI in Unity 2018 to build smart game environments and enemies with A*, Finite State Machines, Behavior Trees and NavMesh.

By Dr. Davide Aversa and 2 more
Hands-On Game Development Patterns with Unity 2019

Write maintainable, fault-tolerant, and cleaner game codes by understanding the standard development patterns and battle-tested practices.

By David Baron
Unity Game Optimization - Third Edition

Get up to speed with a series of performance-enhancing coding techniques and methods that will help you improve the performance of your Unity applications

By Dr. Davide Aversa and 1 more
Learning C# by Developing Games with Unity 2019 - Fourth Edition

Understand the fundamentals of C# programming and get started with coding from ground up in an engaging and practical manner

By Harrison Ferrone