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

Now we have a simple agent that follows patrol route and change colour when it sees the player object. To make this usable and interesting in the game, we need to extend to functionality and have something else than a cube.

Previous parts of tutorial

I downloaded Zombie model Pxltiger from the AssetStore, combined meshes in order to manipulate renderer of each parts easily.

Animation controller. Walk state is connected to walk animation. Idle state is an empty state. Speed is used in transition (boolean in this case would have worked)
Animation controller. Walk state is connected to walk animation. Idle state is an empty state. Speed is used in transition (boolean in this case would have worked)

To run animation we need to configure an Animation Controller fort the character and link the controller to Animator component.

For set-up, the model needs

  • NavMeshAgent
  • RidigBody
  • CapsuleCollider (or MeshCollider)
  • AudioSource

The guard also needs Perception component, but the component should be on head, so the location used to track seeing at head where the eyes are. I just first create an empty GameObject and attach perception component to it. Drag the Perception object under Bib01 Head and check that the object is in the right height and facing correctly.

Placement of Perception object/component.
Placement of Perception object/component.
Guard set-up. Line shows where the perception component is and where it is looking at.
Guard set-up. Line shows where the perception component is and where it is looking at.

I changed material of the Zombie and now shader is Standard (Specular Setup). The Albedo texture is grey metal, specular colour is grey. Then I draw textures for electricity charges and placed them to specular texture using the the original Zombie texture to place the charges. I also want to guards to be metallic looking when they patrol or do nothing and specular when a guard sees or searches the player object.

I have total seven different specular textures that will be used in texture animation. The animation is controlled by the following script.

BodyEmAnim.cs

using System.Collections;
using UnityEngine;

public class BodyEmAnim : MonoBehaviour {

  // textures used in animation
  public Texture2D[] textureAnimation;
  // interval between texture change
  public float animSpeed = 0.1f;

  public Renderer bodyRenderer;

  // keeping track which texture we are currently using
  private int i;

  void Start () {
    if (bodyRenderer) {
      InvokeRepeating("Animate", animSpeed, animSpeed);
    }
    else {
      Debug.LogError("BodyEmAnim.Start(): No renerer. Texture animation is not started.");
    }
    i = 0;
  }

  public void SetEmission(float e) {
    // sets emission level. When e is zero, no emission and one full emission
    Color finalColor = Color.white * Mathf.LinearToGammaSpace (e);
    bodyRenderer.material.SetColor ("_EmissionColor", finalColor);
  }

  private int RndTexture() {
    // randomising next texture, but so that the same texture
    // is not selected twice a row.
    while (true) {
      int r = Random.Range(0, textureAnimation.Length);
      if(r != i) {
        i = r;
        return i;
      }
    }
  } 

  private void Animate () {
    // setting new texture to material
    bodyRenderer.material.SetTexture("_EmissionMap", textureAnimation[RndTexture()]);
  }
}

EDIT. Note that the scrip manipulates material. If you have multiple objects in a scene using this scrip, all of them should have their own materials. Otherwise all instances will manipulate the same material and the instances will have exactly the same animation.

I want to the Guard to be able to attack. Instead using the Zombie attack animation and implementing melee, I create electricity ray that the guard shoots from its eyes. This attack can be drawn using LineRenderer.

AttackVisualizer.cs

using UnityEngine;
using System.Collections;

public class AttackVisualizer : MonoBehaviour {

  public float randomRange = 0.5f;
  public int lengthOfLineRenderer = 10;

  private Transform target;
  private LineRenderer lineRenderer;

  private GameObject parent; // we store the parent for debuging

  void Start () {
    lineRenderer = GetComponent();
    // setting how many "corners" the line will have
    lineRenderer.SetVertexCount(lengthOfLineRenderer);
    // disabling the render and this component, so by default the attack is not drawn.
    lineRenderer.enabled = false;
    enabled = false;
  }

  void Update () {
    // when renderer is active, we create random variation to 
    // line so that it line looks like flash

    // first we split the vector from attacker to target to lengthOfLineRender
    // pieces.
    Vector3 dir = (target.position - transform.parent.transform.position);
    dir = dir / (float)lengthOfLineRenderer;

   // then we add random variation to the end of each piece.
   for (int n = 0; n < lengthOfLineRenderer; n++) {
     Vector3 pos = transform.parent.transform.position + dir * n;
     Vector3 randomVariation = new Vector3(pos.x + Random.Range(-randomRange, randomRange),
     pos.y + Random.Range(-randomRange, randomRange),
     pos.z + Random.Range(-randomRange, randomRange));
     lineRenderer.SetPosition(n, randomVariation);
   }
  }

  public void SetTarget (Transform myTarget) {
    target = myTarget;
    if (target) {
      lineRenderer.enabled = true;
      enabled = true;
    }
    else {
      lineRenderer.enabled = false;
      enabled = false;
    }
  }

  public void SetParent (GameObject myParent) {
    // storing the parent object if that info is needed for debuging
    parent = myParent;
  }
}

We need an empty GameObject that should contain

  • AttackVisualizer
  • LineRenderer

Following image shows AttackVisualiser object setup.

AttackVisualiser GameObject setup
AttackVisualiser GameObject setup

Next step is to implement Guard AI. Image below illustrate how the guard should behave in certain conditions. Even there is rather limited amount of states, the state machine starts to be rather complicated. A behaviour tree is probably more suitable with anything more complicated that this.

Guard AI state machine
Guard AI state machine

Now we need to implement that state machine and functionality that is controlled by guard AI. (I edited PerceptionData and  added a public GameObject gameObject to PerceptionData interface and storing seen GameObject to gameObject. The class is update in tutorial part 2)

Guard.cs

using UnityEngine;
using System.Collections;

public class Guard : MonoBehaviour, WaypointInteface {

	public Waypoint target;
	
	public float speed = 5.0f;
	public float rotationSpeed = 0.1f;
	
	public float searchingTime = 10.0f; // searching after loosing sight of PC
	
	// Attack specification
	public int damagePerSecond = 5;
	public float attackRange;

	public AudioClip attackSound; 

        // for caching components
	private Animator anim;
	private CharacterController controller;	
	private Perception perception;
	private AttackVisualizer attackVisualizer;
	private AudioSource audioSource;
	private BodyEmAnim bodyAnim;
        private NavMeshAgent agent;

        // for Turn() function
	private Quaternion _lookRotation;
	private Vector3 _direction;

        // defining states for Guard state machine
	public enum GuardStates {PATROLLING, IDLE, SEARCHING, CHASING, ATTACKING};

        // the current guard state
	private GuardStates state;

        private PerceptionData pd; data of seen and tracked GameObject
	private float searchingStateStart; // for keeping track when the guard lost sight of the PC
	private Vector3 lastSeen; // keeping track where the PC is last seen

        // keeping track if NavMeshAgent is in control of the guard movements or this scrip
	private bool agentControlling; 
        
	void Start () {
                // Caching component pointers 
		anim = GetComponent ();;
		perception = GetComponentInChildren();
		perception.SetParent (this.gameObject);
		attackVisualizer = GetComponentInChildren();
		audioSource = GetComponent();
		agent = GetComponent();
                bodyAnim = GetComponent();
                
                // initialising the state maching
		if(target) {
			agent.destination = target.transform.position;
			if(target.nextTarget != null) {
				agent.autoBraking = false;
			}
			state = GuardStates.PATROLLING;	
			agentControlling = true;
		}
		else {
			state = GuardStates.IDLE;
		}
	}
	
	
	// Update is called once per frame
	void Update () {
	
                // keeping track if the guard is moving or not to be able
                // display correct animation
		bool moving = false;
	
                // storing previous state; we need previous state later
		GuardStates prevState = state;
		
                // check if the guard sees PC
		if(perception.Seeing) {
                        // ignore PC if PC is invisible
			if(!GameAgents.GetCharacterInterface().IsInvisible()) {
				lastSeen = GameAgents.GetPlayer().transform.position;
				bodyAnim.SetEmission(1);
				state = GuardStates.CHASING;
                                // the guard might have seen multiple things
                                // now we just pick the first one seen 
				pd = (PerceptionData)perception.PerceptionData[0];
				// determining the state. 
				if(pd.range <= attackRange) { 				   state = GuardStates.ATTACKING;  				}  			}	 		                 }  		else {  			if(state == GuardStates.ATTACKING || state == GuardStates.CHASING) {  				Debug.Log ("Guard.Update(): " + name + " lost sigth. SEARCHING.");  				state = GuardStates.SEARCHING;  				searchingStateStart = Time.time;  				bodyAnim.SetEmission(0.5f);  			}  		}  		bool stateChanged = false;  		if(state != prevState) {  			stateChanged = true;  			Debug.Log ("Guard.Update(): " + name + " " + prevState.ToString () + " -> " + state.ToString());
			
		}
		
                // now handling things that needs to happen in the specific states
		switch (state) {
		
		case GuardStates.PATROLLING:
			// Follow the patrol route defined by waypoints
			bodyAnim.SetEmission(0);
			if(target) {
				agent.destination = target.transform.position;
				if(agentControlling == false) {
					if(target.nextTarget != null) {
						agent.autoBraking = false;
					}
					agent.Resume();
					agentControlling = true;
				}
				moving = true;
			}
			else {
				state = GuardStates.IDLE;
			}
			break;
		case GuardStates.ATTACKING:
			agent.Stop ();
			agentControlling = false;
			if(!IsInvoking()) {
				Debug.Log ("Guard.Update(): Initializing ATTACK");
				Attack();
				InvokeRepeating("Attack", 1.0f, 1.0f);
			}
                        // I do not want the guard go toward the player when it is attacking
                        // but I want it to keep facing the PC while attaking
                        // for that I need to handle turning by myself
			if (Turn (GameAgents.GetPlayer())) {
				moving = true;
			}
			break;
		case GuardStates.CHASING:
			agent.destination = lastSeen;
			if(agentControlling == false) {
				Debug.Log ("Guard.Update(): " + name + " CHASING, resuming agent control");
				agent.autoBraking = true;
				agent.Resume();
				agentControlling = true;
			}
			moving = true;
			break;
		case GuardStates.IDLE:
			agent.Stop();
			agentControlling = false;
			bodyAnim.SetEmission(0);
			break;
		case GuardStates.SEARCHING:
			moving = true;
			agent.destination = lastSeen;
			if(agentControlling == false) {
				Debug.Log ("Guard.Update(): " + name + " SEARCHING, going towards last seen position");
				agentControlling = true;
				agent.Resume();
			}
			if (!agent.pathPending)
			{
				if (agent.remainingDistance <= agent.stoppingDistance)
				{
					if (!agent.hasPath || agent.velocity.sqrMagnitude < 0.1f) 					{  						Debug.Log ("Guard.Update():" + name + " reached the last seen position.");  						anim.SetFloat("speed", 0);  						agent.Stop();  						agentControlling = false;  					}  				}  			}  			if(Time.time - searchingStateStart > searchingTime) {
				state = GuardStates.PATROLLING;
				Debug.Log ("Guard.Update(): " + name + " stopped SEARCHING at " + Time.time + "( started = " + searchingStateStart +  ") State = " + state.ToString());
			} 
			break;
		}
		
		if(state != GuardStates.ATTACKING) {
			if( IsInvoking()) {
				CancelInvoke();	
				audioSource.Stop();
				attackVisualizer.SetTarget(null);
			}
		}
		
                // lets notify animation system so it can play the correct animation
		if(moving) {
			anim.SetFloat("speed", 1);
		}
		else {
			anim.SetFloat("speed", 0);
		}
	}
	
	void WaypointInteface.SetTarget(Waypoint t) {
                // Reached the target waypoint. Setting new waypoint as target
                // if there is one
		target = t;
		if(target) {
			Debug.Log ("Guard.SetTarget(): next target = " + target.name);
			agent.destination = target.transform.position;
		}
		else {
			Debug.Log ("Guard.SetTarget(): next target = null");
		}
	}
	
	private void Attack() {
		Debug.Log("Enemy.Attack(): " + Time.time);
		audioSource.clip = attackSound;
		audioSource.loop = true;
		audioSource.Play();
		
		GameAgents.GetCharacterInterface().ReceiveDamage(damagePerSecond);	
		
		attackVisualizer.SetTarget(pd.gameObject.transform);
	}
	
	private bool Turn(GameObject t) {
		
		// for calculating facing, we need to make sure that target waypoint is at the same 
		// height than the guard
		Vector3 tmpPos = new Vector3 (t.transform.position.x, transform.position.y, t.transform.position.z);
		
		// direction where we shoul be dacing
		_direction = (tmpPos - transform.position).normalized;
		
		// If we are looking at the correct direction, stop turning
		// No need to calculate angle, as when vectors forward desired 
		// are the same (lenth is small), the object is facing the correct 
		// direction
		if ((transform.forward - _direction).sqrMagnitude < 0.1) {
			return false;
		}
		
		//create the rotation we need to be in to look at the target
		_lookRotation = Quaternion.LookRotation(_direction);
		
		//rotate us over time according to speed until we are in the required rotation
		transform.rotation = Quaternion.Slerp(transform.rotation, _lookRotation, rotationSpeed);
		
		return true;
		
	}
	void OnDrawGizmos() {
		// visualizing the planned path of NavMeshAgent for debuging
		// with complex path this might be cause lack
		try {
			if(agent.path.corners.Length < 2) //if the path has 1 or no corners, there is no need
				return;
		
			for(int i = 1; i < agent.path.corners.Length; i++){
				Gizmos.DrawLine(agent.path.corners[i-1], agent.path.corners[i]); //go through each corner and set that to the line renderer's position
			}
		}
		catch(System.Exception) {
		
		}
			
	}
	
}

Attack needs sound. Get one, for example, from http://www.freesound.org/ and attack that to the game object. Now we should be able to test the guard using the same patrol route used with the SimpleAgent.

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: