Enemy AI:Beginners Field of View Script in terms of understanding

As mentioned in my first post, I am a rusty programmer, fairly new to Unity and I don't believe in copying code willy-nilly without understanding what the actual code doing and more importantly the method/logic.

I wasn't planning to do any tutorial like posts on this blog, but what I thought would be rather simple to achieve, actually took me a lot longer than expected. I am hoping this might save someone some time.

About:  

What I wanted to do was to create a very basic to understand Enemy field of vision script. which I could break at each step, look at the values before moving to the next step. I did plenty of google searching and came across a few problems;
  1. Some awfully out of date code/examples.
  2. Code examples not in C# 
  3. Maths that is not meant for us mere mortals. 
  4. Some threads posted to other threads which no longer existed or I ended up in a thread inception. 
  5. Some threads broke down into discussions about the benefits and advantages of various methods without coming to any conclusion. 
  6. Some just posted snippets of code, without any of the dependencies.
This example is only concerning itself with the logic. I did not put it together with any regards in terms of  performance - I am not qualified to answer that. 

What it does:

It gives the enemy a field of vision and a range of vision. It returns to the console the following results as a text string;

Player in the Field of View - We are within the enemies field of view 
Player not in Field of View -  We are not within the enemies field of view.

Player in the Field of View: Player in View Distance:Player Spotted - We are within the field of view and we are in within the enemies view distance and the enemy can see the Player.

Player in the Field of View: Player in View Distance:Player Not Spotted - We are within the field of view and we are in within the enemies view distance and the enemy can not see the Player.

Step One: Set UP

  1. Create a capsule and call it Player
  2. Create another capsule and call it Enemy
  3. Create a plane, under transform, set the x scale to 10 and the Z scale to 10 
  4. Create a cube, under transform, set the x scale to 1, the y scale to 3, and the z scale to 5
  5. Drag the main camera under the player object and position it above the player. 
  6. Arrange the objects roughly as in the picture below. (enemy is coloured white, player is colored green, the cube white and the plane black.  This was done manually be creating materials and dragging them onto their objects.). 


Step Two: Player SetUp

  1. Set the Tag to Player 
  2. Add a Rigidbody and a Capsule Collider 
  3. Under Rigidbody, untick gravity and under constraints freeze rotation x,y,z(to avoid any weird movements) 
  4. Add a script called PlayerMovement and add in the following code. 
 using UnityEngine;  
 public class PlayerMovement : MonoBehaviour  
 {  
      public float speed = 6f;      // The speed that the player will move at.  
      Vector3 movement;          // The vector to store the direction of the player's movement.  
      Rigidbody playerRigidbody;     // Reference to the player's rigidbody.  
      void Awake ()  
      {  
           // Set up references.  
           playerRigidbody = GetComponent <Rigidbody> ();  
      }  
      void FixedUpdate ()  
      {  
           // Store the input axes.  
           float h = Input.GetAxisRaw ("Horizontal");  
           float v = Input.GetAxisRaw ("Vertical");  
           // Move the player around the scene.  
           Move (h, v);  
      }  
      void Move (float h, float v)  
      {  
           // Set the movement vector based on the axis input.  
           movement.Set (h, 0f, v);  
           // Normalise the movement vector and make it proportional to the speed per second.  
           movement = movement.normalized * speed * Time.deltaTime;  
           // Move the player to it's current position plus the movement.  
           playerRigidbody.MovePosition (transform.position + movement);  
      }  
 }  

Step Three: World SetUp 


  1. Set up a new Layer Called ShootMeLayer
  2. Set the layer on the Player and Cube to ShootMeLayer


Step Four: Enemy SetUp 

1. Add a script and call it FieldOfVisionTest and add the following code. 

 using UnityEngine;  
 using System.Collections;  
 public class FieldOfVisionTest : MonoBehaviour {  
      /*  
       * *****************************************************************************  
       * Author:RSJS  
       * web:https://intotheeaglesnest.blogspot.co.uk/  
       * Description: Simple script to understand an enemy AI sight approach. This script  
       * gives the enemy a field of view and a view range. it returns the result as a string  
       * and displays it in the debug.console.   
       * Depend: For the raycast, the items the enemy can "See" should be on a layer called  
       * ShootMeLayer  
       * *****************************************************************************  
      */  
      public int FieldOfView = 120; //enemy field of view  
      public int ViewDistance = 10; //enemy view distance   
      private Transform playerTrans; // used for the player position   
      private Vector3 rayDirection; //used for the ray casting   
      private string ViewResult; //Used for storing result of the view text  
      int ShootableMask; // Mask for shooting things.   
      void Awake ()   
      {  
           playerTrans = GameObject.FindGameObjectWithTag ("Player").transform; // so the enemy knows where the player is  
           ShootableMask = LayerMask.GetMask ("ShootMeLayer"); // when ray casting only look for objects on this level  
      }  
      // Update is called once per frame  
      void FixedUpdate () {  
           ViewResult = ""; // Clear out our result each time  
           LookForEnemy (); // Call our look for the enemy script   
           onDrawGizmos (); // For debugging purpozes  
           Debug.Log(ViewResult); //print to the console the result of our look for enemy   
      }  
      void LookForEnemy ()  
      {  
           //Get direction from player to and enemy   
           rayDirection = playerTrans.position - transform.position;        
           // Test to see if the enemy is in the view range.  
           if ((Vector3.Angle (rayDirection, transform.forward)) <= (FieldOfView * 0.5f)) {  
                //Yes, player is in the view range  
                ViewResult = ViewResult + "Player In Field of View";  
                //Now, test with a raycast if the player is with the view distance  
                RaycastHit hit;  
                if (Physics.Raycast (transform.position, rayDirection, out hit, ViewDistance,ShootableMask))   
                {  
                     Debug.DrawLine (transform.position, playerTrans.position, Color.red); //For debugging   
                     if (hit.distance <=ViewDistance)  
                     {         
                          //Yes, player is in viewing distance   
                          ViewResult = ViewResult + ":Player In View Range";  
                          //Now test to see if what we are looking for is indeed the player  
                          if (hit.collider.tag== "Player") {  
                          //     Yes, Enemy is in sight   
                               ViewResult = ViewResult + ":Player Spotted";  
                          }  
                               else  
                                    {  
                               //No enemy is being blocked  
                               ViewResult = ViewResult + ":Player Not Spotted";  
                                    }  
                     }  
                }  
           } else {  
                //player not in view range  
                ViewResult = "Player Not In Field of View";  
           }  
      }  
      void onDrawGizmos()  
      {  
           //not my code, but gives an apx view of the field of sight. would love to know a better approach  
           if (playerTrans == null)  
                return;  
           Vector3 frontRayPoint = transform.position + (transform.forward * ViewDistance);  
           // apx persepctive   
           Vector3 LeftRayPoint = frontRayPoint;  
           LeftRayPoint.x += FieldOfView * 0.5f;  
           Vector3 RightRayPoint = frontRayPoint;  
           RightRayPoint.x -= FieldOfView * 0.5f;  
           Debug.DrawLine (transform.position, frontRayPoint, Color.green);  
           Debug.DrawLine (transform.position, LeftRayPoint, Color.green);  
           Debug.DrawLine (transform.position, RightRayPoint, Color.green);  
      }  
 }  

Running the Code 

Make sure you have the console visible, also note I have moved the wall in the below screenshot. I highly recommend moving the objects and player around when playing with the code to understand it.


In the above picture, we can see our player is hiding behind the wall, the console is showing (as it should )

Player in the Field of View: Player in View Distance:Player Not Spotted

Now to break down what is happening.

The first test we do, is whether the player is in the field of view. to do this, we use Vector3.Angle to work out the angle between the enemy and the player. We then test this to see if this value fits within the field of view we have assigned the enemy. Note, and this is what confused me at first, the angle Vector3 returns is not normalised and returns the smaller of the two angles, so it can never be higher than 180. To address this in our code, we divide our fieldofView value by two

<= (FieldOfView * 0.5f))

If this is still not making much sense, I suggest adding to the code

Debug.Log(Vector3.Angle (rayDirection, transform.forward));

and moving the player around to see the result.

The second test we do is see if the player is within our view range. To do this we use a Raycast  (the red line in the picture) to the distance of our player view and see if it hits the player. If it does, then we know the player is in view. Play around with the field distance value to see this in action.

The third test we do (and this is what was doing my head in!) is to test what we are actually looking at is the player. To do this, we return the tag of the collider object and if it is tje player, then we know we are looking at the player.

if (hit.collider.tag== "Player") {

Now, the reason I believe this was doing my head in, is down to the fact that the Raycast does not stop when it hits a collider it keeps on going. You need to be careful the order you do the above steps, but I have found doing them this way, gives a result that is traceable in terms of understanding.

To better demonstrate this, add a few more cubes around the playing area. Add them unique tags ie left wall, right wall  and add the following code

Debug.Log(hit.collider.tag);

And if you are feeling brave, move around the order of tests and see what results you get and see why it can get confusing! 

Notes & Conclusion 

I don't quite understand why at the moment, but when I have tried this on some character assets, it has not worked and the reason appears to be, that the Raycast  has been hitting the floor. (this is also the reason I believe I have had some problems with other tutorials out there) . To correct this, I found changing the y values on the transform.position and player positions has worked.

This is also not the approach I am using in my game, instead, I am using Physics.OverlapSphere  and an adaption of the code based on the 2D solution listed here  , the reason for this, when writing the actual code for my game, after going through all the above, using OverlapSphere comes across as a cleaner solution it also allows us to do a few other things I am planning. 

I hope though, that the above is of use to someone in terms of getting an understanding. 







Comments