Tutorial: 1st-person sneak in Unity 5, part 2

The first part of tutorial: /2015/05/07/tutorial-1st-person-sneak-in-unity-5-part-1/

The guards needs to be able to observe their surroundings so we need a perception system. I want to make agent not to see its back, but sensing if someone is really close and have some peripheral vision with limited range (cf figure below). Also, guard should not be able to see through obstacles.

Perception system schematics
Perception system schematics

To fulfil these requirements we need to use LineCast to check it there is unobscured view. LineCast is rather resource intensive so I do not want to run code in every frame, but certain interval. Checking in every frame is not needed as object do not move much in one frame.

But before going to perception system, I create new interface for all characters that should be seen by guard.

[EDIT: added CanUseEnergy() and CanUseHealth() to interface]

CharacterInterface.cs

using UnityEngine;
using System.Collections;

public interface CharacterInterface {
  bool IsInvisible ();
  void ReceiveDamage (int amount);
  void PowerUp (int _energy, int _health);
  void Save(string levelName);
  bool CanUseEnergy();
  bool CanUseHealth();
}

These all functions are for the future use. I will get back to them later. The main thing I need for the Perception class is the interface exits.

Physics.OverlapSphere() is used to catch all GameObjects in specified layers within the maximum perception range. After that line of sight is calculated.

[EDIT: Added GameObject gameObject to PerceptionData]

Perception.cs

using UnityEngine;
using System.Collections;

public struct PerceptionData {
  public CharacterInterface perceived;
  public float range;
  public GameObject gameObject;
 
  public PerceptionData(CharacterInterface p, float r, GameObject g) {
     perceived = p;
     range = r;
     gameObject = g;
   }
}

public class Perception : MonoBehaviour {

   public float frontRange = 20.0f;
   // if angle between forward and los is less than this, the target is at front
   public float frontAngle = 30.0f; 
   public float frontRange2 = 10.0f;
   public float frontAngle2 = 90.0f;
   public float closeRange = 2.0f;
   public float checkInterval = 0.4f;
   // all object in the given layers are perceived
   public LayerMask mask;

   private bool withinPerceptionRange;

   private ArrayList perceived;

   void Start () {
      withinPerceptionRange = false;
     // We check what is seen in every checkInterval seconds.
     InvokeRepeating("Perceive", checkInterval, checkInterval);
     enabled = false; 
     perceived = new ArrayList();
   }

   private void CheckLineOfSight(Collider c) {
      RaycastHit hit;
      float range = -1;
      bool inRange = false;
      // We Linecast from this agent to PC to see if there is a line of sight.
      if (Physics.Linecast(transform.position, c.transform.position, out hit)) {
        if (hit.transform == c.transform) {
          if (hit.distance <= closeRange) {
            inRange = true;
            range = hit.distance;
          }
          else if (hit.distance <= frontRange2) {
            if (Vector3.Angle(transform.forward, hit.transform.position - transform.position) <= frontAngle2) {
              inRange = true;
              range = hit.distance;
            }
          }
          else if (hit.distance <= frontRange) {
            if (Vector3.Angle(transform.forward, hit.transform.position - transform.position) <= frontAngle) {
              inRange = true;
              range = hit.distance;
            }
          }
        }
      }
      if(inRange) {
        CharacterInterface character = c.gameObject.GetComponent();
        if(character != null) {
          perceived.Add (new PerceptionData(character, range));
          withinPerceptionRange = true;
        }
        else {
          Debug.LogError("Perception.CheckLineOfSight(): " + c.name + " does not have CharacterInterface component!");
        }
      }
    } 

    private void Perceive () {
      // Lets determine does this agent see the PC
     
      // Clearing data from previous time
      perceived.Clear();
      withinPerceptionRange = false;
 
      // Get all objects that are within the maximum perception range (frontRange)
      // in the layers defined by mask. 
      Collider[] hitColliders = Physics.OverlapSphere(transform.position, frontRange, mask);
      for(int i=0; i

For calculating line of sight we need to know which direction the hit is CheckLineOfSight() function. Following images illustrates the vectors needed in calculation. Before that, however, we just check if object is not behind any object that hides it using Physics.Linecast(). If the object hit is some wall etc. the observer did not see the character.

We need to calculate angle between transform.forward and A.
We need to calculate angle between transform.forward and A.
Transform position and hit.transform.position are vectors that starts from origo and ends to where the objects are. A vector is then transform.hit.position-transform.position (other way around the direction of vector is opposite). Note that instead of angle we could use (A.normalized - tranform.forward).sqrMagniture to do the same. where instead of angles one set up the maximun sqrLengths of vector A. This sqrMagnitude approach is faster to calculate, but angles are more intuitive to set-up.
Transform position and hit.transform.position are vectors that starts from origo and ends to where the objects are. A vector is then transform.hit.position-transform.position (other way around the direction of vector is opposite).
Note that instead of angle we could use (A.normalized – tranform.forward).sqrMagniture to do the same. where instead of angles one set up the maximun sqrLengths of vector A. This sqrMagnitude approach is faster to calculate, but angles are more intuitive to set-up.

We get angle with the following code that tells which direction hit is relative to transform forward.

Vector3.Angle(transform.forward, hit.transform.position - transform.position)

After that we compare angles and distances to closeRange, frontAngle2 and frontRange2, and frontAnge and fromRange to see if the hit object should be seem.

For testing the perception, I add some functionality to SimpleAgent and make it change colour when it sees the Player object. For that we need

  • 1st person controller from Unity assets packages. Set up new layer called Character and put the controller to that layer.
  • Make SimpleAgent black and add some feature to point where its front is.
SimpleAgent with Perception component
SimpleAgent with Perception component

Next is time to add functionality to the SimpleAgent.cs

void Update() {
   if(perception.Seeing) {
      GetComponent().material.SetColor("_Color", Color.white);
   }
   else {
      GetComponent().material.SetColor("_Color", Color.black);
   }
}

Object changes colour to black if it is not seeing and white if it seeing. Instead doing GetComponent in every frame, the result of GetComponent should be cached. GetComponent(typeof(T)) and especially generic GetComponent() are slow functions (see http://chaoscultgames.com/2014/03/unity3d-mythbusting-performance/).

I following clip, I have target of SimpleAgent set to null so it does not demonstrate the perception.

Published by lankoski

Petri Lankoski, D.Arts, is a Associate Professor in Game Studies at the school of Communication, Media and IT at the Södertörn University, Sweden. His research focuses on game design, game characters, role-playing, and playing experience. Petri has been concentrating on single-player video games but researched also (multi-player) pnp and live-action role-playing games. This blog focuses on his research on games and related things.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: