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.

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.


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.

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.

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.