Controlling object-to-object movements (seek, flee, follow at a distance)

Computer controlled characters (non-player characters) often need to seek and move towards another object (such as the player's character), or alternatively flee away from another character. The logic is the same for both seeking and fleeing, and is demonstrated in this recipe.

Getting ready

This recipe builds upon the player-controlled cube Unity project you created in the first recipe. So make a copy of that project, open it, and then follow the steps of this recipe.

How to do it...

To make an object seek, flee, or follow another object, perform the following steps:

  1. Create a sphere named Arrow, positioned at (2, 0.5, 2) and with scale (1, 1, 1).
  2. Create a second sphere, named Sphere-small, positioned at (2, 0.5, 2.5) and with scale (0.5, 0.5, 0.5).
  3. Make Sphere-small a child-object of your GameObject Arrow.
  4. Add a Rigidbody component from Physics to the GameObject Arrow, modifying the following properties:
    • Set the value of Mass to 0.3
    • Uncheck the Use Gravity checkbox
    • Check (tick) the Freeze Position for the Y axis only
    How to do it...
  5. Add the following C# script class to your GameObject Arrow:
    // file: SeekTarget.cs
    using UnityEngine;
    using System.Collections;
    
    public class SeekTarget : MonoBehaviour {
      public GameObject playerGO;
      public const float MAX_MOVE_DISTANCE = 500.0f;
      
      private void FixedUpdate () {
        float moveDistance = MAX_MOVE_DISTANCE * Time.deltaTime;
        Vector3 source = transform.position;
        Vector3 target = playerGO.transform.position;
    
        Vector3 seekVelocity = Seek(source, target, moveDistance);
        seekVelocity = UsefulFunctions.ClampMagnitude(seekVelocity, moveDistance);
        rigidbody.AddForce( seekVelocity, ForceMode.VelocityChange );
        UsefulFunctions.DebugRay(transform.position, seekVelocity, Color.blue);
        UsefulFunctions.DebugRay(transform.position, rigidbody.velocity, Color.yellow);
      }
    
      private Vector3 Seek(Vector3 source, Vector3 target, float moveDistance){
        Vector3 directionToTarget = Vector3.Normalize( target - source );
        Vector3 velocityToTarget = moveDistance * directionToTarget;
        transform.LookAt( playerGO.transform );
    
        return velocityToTarget - rigidbody.velocity;
      }
    }
  6. Ensuring Arrow is selected, in the Inspector for the SeekTarget scripted component, drag Cube-player over the public variable playerTransform.

How it works...

We have added a Unity Rigidbody component from Physics to our Arrow object. This means we can ask Unity to apply a "force" to this object, and Unity will work out how to move the object based on its mass and any previous velocity the object already had.

Note

When working with physics and forces, we put our logic into the FixedUpdate() method, which is called at a fixed frame rate, immediately before Unity runs its physics simulation for the frame. Had such logic been located in the Update() method, several Update() messages, or none, might have been received since the last physics' simulation update, leading to uneven behavior.

Remember, once you have added a physics rigid body to an object you are influencing through code, that code must be placed into the FixedUpdate() method.

One danger when adding forces to already moving objects is that they start to move very fast, and this can lead to "overshooting" the target, and sometimes can even leave the chasing object forever orbiting around the target, never getting any closer to it. Therefore, we define a MAX_MOVE_DISTANCE constant, which will set a limit on the size of any force we will apply to Arrow. As usual, since the frame rate of games vary, we need to use the proportion of this speed maximum that is relative to the time since the last frame (Time.deltaTime).

Each time FixedUpdate() is called, the maximum move distance for this frame (moveDistance) is calculated first. Then the Seek() method is called passing the current positions of the source (Arrow) and target (Cube-player) objects. This method returns a Vector3 object that represents the force we wish to apply to Arrow to make it move towards the players cube. This force is limited to the size of moveDistance, and then applied to the Arrow object as a force to the rigid body component. Finally, the Arrow object is made to face towards its target, and then two debug rays are drawn, a blue one to show the seeking force we have just applied, and a yellow one to show the current direction/speed velocity that the Arrow object now has.

The core of this target-seeking behavior is to make use of the following two key pieces of information, which we do in the Seek() method:

  • The direction from the Arrow (source) object towards the player's cube (target) object
  • The magnitude (length) of the force vector we want to apply to our Arrow object

    Note

    A Vector3 object is the perfect data structure for forces, since they represent both direction and magnitude (the length of the vector).

    A normalized vector is simply a vector with a length (magnitude) of 1. This is very useful since we can then multiple a normalized vector by the speed (distance) we want an object to move. Sometimes we refer to a normalized vector as a unit vector, since it has a length of 1.

The Seek() method does the following:

  • It calculates the direction from the Arrow (source) object towards the players cube (target) object by subtracting the source position from the target position, and then normalizing the result.
  • This direction is multiplied by moveDistance to give the velocity vector we'd like the Arrow object to move.
  • If Arrow is already moving, then simply applying the force towards the target may "nudge" the Arrow object a little off its current direction towards the target. What we really want is to calculate the force to apply to make Arrow move towards the target taking into account the Arrow object's current movement. We do this by subtracting the Arrow object's current movement (rigidbody.velocity) from the force in the direction of the target. The result is a force that will redirect Arrow from its current direction towards the target.
    How it works...

There's more...

The following are some details you don't want to miss.

Using the simpler (and less sophisticated) MoveTowards() method

If the only movement behavior you wish to implement for an object is a simple seek behavior, Unity does provide a very straightforward method for this purpose. The use of rigid bodies and forces is needed for the later recipes in this chapter, which is why this simple method approach was not taken for this recipe. However, you could replace the code in this recipe for the SeekTarget class with the following, which uses the MoveTowards() Unity method for a simple seek behavior:

// file: MoveTowards.cs
using UnityEngine;
using System.Collections;

public class MoveTowards : MonoBehaviour {
  public Transform playerTransform;
  public float speed = 5.0F;

  private void Update () {
    transform.LookAt(playerTransform);
    float distance = speed * Time.deltaTime;
    Vector3 source = transform.position;
    Vector3 target = playerTransform.position;
    transform.position = Vector3.MoveTowards(source, target, distance);
  }
}

The MoveTowards() method takes three arguments, namely: the source position, the target position, and how far to move towards the target.

Improving arrival behavior through deceleration when near target

A nicer version of the seek behavior involves the seeking object to slow down as it gets closer to the target object. This can be achieved by replacing the Seek() method with a new Arrive() method. First, replace the statement in FixedUpdate(), so that the value of adjustVelocity is returned from the Arrive() method:

Vector3 adjustVelocity = Arrive(source, target);

Next, define a constant DECELERATION_FACTOR that we need:

const float DECELERATION_FACTOR = 0.6f;

Finally, implement the Arrive() method with the following code:

private Vector3 Arrive(Vector3 source, Vector3 target){float distanceToTarget = Vector3.Distance(source, target);
  Vector3 directionToTarget = Vector3.Normalize( target - source );
  float speed = distanceToTarget / DECELERATION_FACTOR;
  Vector3 velocityToTarget = speed * directionToTarget;
  
  return velocityToTarget - rigidbody.velocity;
}

The speed (magnitude) of the force to be applied to the Arrow object will vary depending on the remaining distance to the target (player's cube). This rate of deceleration can be tweaked by changing the value of the DECELERATION_FACTOR constant. As the distance to the target is reduced (that is, as the Arrow object gets closer), the size of the force to be applied to Arrow is smaller, so it slows down as it gets closer to the target. This results in a more natural (and satisfying) movement in target-seeking game objects.

The next screenshot shows the following two debug rays of the forces on the Arrow object at a point in time:

  • The longer ray shows the current movement velocity of the Arrow—upwards and to the right
  • The shorter ray shows the force to be applied to Arrow to make it move towards the player's cube, downwards to the right—which should result in the Arrow colliding with the target player cube object very soon
    Improving arrival behavior through deceleration when near target

Implementing flee target and follow at a distance

The opposite to arrive at is the flee behavior. This involves moving in the opposite direction to where the target is. First, change FixedUpdate() so that the value of adjustVelocity is set differently according to the distance from target (its default is 0; if within the value of FLEE_RADIUS, it will be a force to move away from the target, if outside the value of SEEK_RADIUS, it will be a force to move towards the target):

Vector3 adjustVelocity = Vector3.zero;

float distanceToTarget = Vector3.Distance(target, source);
if( distanceToTarget < FLEE_RADIUS )
  adjustVelocity += Flee(source, target);

if( distanceToTarget > SEEK_RADIUS )
  adjustVelocity += Arrive(source, target);

Next, define some constants we need:

const float DECELERATION_FACTOR = 0.6f;
public const float FLEE_RADIUS = 14f;
public const float SEEK_RADIUS = 6f;

As the value of FLEE_RADIUS is larger than that of SEEK_RADIUS, when the distance to the target is between 6 and 14, then both a flee and an arrive force will be calculated, resulting in a deceleration in this overlap zone. The result is a realistic following behavior, whereby the Arrow follows at approximately 10 units, accelerating and decelerating smoothly based on the player's cube movements.

Add the Arrive() method listed in the previous section, and also add a new method called Flee() as follows:

private Vector3 Flee(Vector3 source, Vector3 target){
  float distanceToTarget = Vector3.Distance(target, source);
  Vector3 directionAwayFromTarget = Vector3.Normalize(source - target);
  float speed = (FLEE_RADIUS - distanceToTarget) / DECELERATION_FACTOR;
  Vector3 velocityAwayFromTarget = speed * directionAwayFromTarget;
  return velocityAwayFromTarget;
}

The Flee() method works in the opposite way to Arrive(), so the speed (magnitude) of the velocity force vector will be larger when the target is closer. Therefore, the Arrow will move quickly away from a nearby target.

A follow-at-a-distance behavior can be achieved by making an object flee a nearby target, but seek to arrive at a far away target.

See also

  • The Controlling cube movement through player controls recipe
  • The Controlling object look-at behavior recipe
  • The Controlling object group movement through flocking recipe
  • The Following waypoints in a sequence recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.215.96