Giving Enemies the Power of Sight!

Enemies are more challenging when they have some intelligence. A great start to building enemy AI is to grant enemies the power of sight!

Sight is really a test that there is clear space between the enemy’s eyes and the player. With that knowledge it’s simple to work out a solution.

You can download the sample project at the bottom of this article to see it in action (and to get all the juicy code).

Defining Vision

To simulate vision we need to distil it down to fundamentals. Vision has:

  • a range (you can’t read newspaper print from a kilometre away)
  • a direction (you can’t see something directly behind you when you are facing forward)
  • line of sight (you can’t see through solid objects).

For a 2D game you can represent sight like so:

The elements of enemy vision
  • The green circle represents the maximum distance the enemy can see.
  • The dotted green lines represent the enemy’s field of view – it can only see things within that ‘cone’.
  • The blue line represents the enemy’s line of sight to the player (to test if anything blocks that view).

Coding Vision

Now that we have defined what the enemy can see (i.e. anything within their range, in the direction they are facing, and not blocked by other objects) we can work on the code.

I chose to break down this problem into three steps, where each step relies on the previous one:

IF the player is within the enemy's vision range
    IF the enemy is facing the player's direction
        IF nothing is in between the player and the enemy
            THEN the player can be seen!
  • If the player is not within range there is no need to check anything else (e.g. it doesn’t matter if you’re looking right at a newspaper if it’s too far away to read).
  • If the enemy is not looking in the player’s direction it doesn’t matter how close the player is (e.g. if he’s behind the enemy the enemy can’t see him.
  • Finally, if the player is behind a wall the enemy can’t see him even if the player is within range and in the enemy’s field of view.

Now to code those steps one-by-one.

Is the Player within the Enemy’s Vision Range?

This is easy. Simply give the enemy a very large circle (or sphere in 3D space) trigger collider, and if the player is within this collider he is within visual range. Typically this circle/sphere would be many times larger than the enemy itself, giving it vision across a large part of the screen:

The enemy’s visual range is determined by a large circle trigger collider

In the example linked at the bottom of this article I have a bool variable isPlayerInSightRange, which is set to true in the enemy’s OnTriggerStay2D() method, and set to false in the corresponding OnTriggerExit2D() method. The enemy script always knows if the player is (theoretically) within visual range.

I use a special collision layer for my vision collider so that it doesn’t get triggered by all the items in the level, since we only care about whether the enemy can see the player or not. This collision layer only collides with the player (set via the collision matrix in the physics settings).

Is the Enemy Facing the Player’s Direction?

I have set an angle of 65 degrees in my example code. This will ensure that the enemy can only see the player if the player is within 65 degrees of the centre of the enemy’s line of sight (which I have set as a transform directly in front – to the left in screen space – of the enemy).

The line of sight transform is just a way to draw an imaginary line facing directly forward from the enemy’s eyes. We then draw another imaginary line from the enemy’s eyes to the player, creating an angle like so:

Measuring the angle of the player from the enemy’s forward direction to see if he’s in the field of view

If that angle is < 65 degrees we consider the player to be within the enemy’s line of sight. The angle is calculated via Vector2.Angle, which takes the two vectors as parameters and returns the angle between them:

Vector2 directionToPlayer = player.position - transform.position; // represents the direction from the enemy to the player
Vector2 lineOfSight = lineOfSightEnd.position - transform.position; // the centre of the enemy's field of view, the direction of looking directly ahead
// calculate the angle formed between the player's position and the centre of the enemy's line of sight
float angle = Vector2.Angle(directionToPlayer, lineOfSight);

Does anything block the enemy’s view of the player?

The final check sends a raycast (an imaginary line that detects colliders) from the enemy to the player and see if it hits anything else on its journey. If it hits something other than the player (e.g. a wall or a crate) then we determine that the enemy’s line of sight to the player is blocked and the player can’t be seen; otherwise the player is seen.

RaycastHit2D[] hits = Physics2D.RaycastAll(transform.position, player.position - transform.position, distanceToPlayer);

Physics2D.RaycastAll returns an array of all the colliders the ray touches on its travel from the enemy’s eyes to the player. If you look at the code in the sample project, we ignore any objects that are actually the enemy. If the ray hits anything else that is not the player we know that this object must be between the player and the enemy because the ray only goes as far as the player.

The third parameter of Physics2D.RaycastAll lets us specify how far to shoot the ray, and therefore the ray can’t detect items further away than distanceToPlayer in this example.

This technique works well, but has a potential flaw depending on the context. The ray is sent from the enemy’s eyes to the player’s location (which will typically be the centre of the player’s transform). This means the player’s centre must be visible/not blocked in order for the enemy to see him. If the player’s body is behind a crate, but his head is exposed, the enemy still can’t see him.

You could remedy this by using multiple rays to detect different points on the player’s body, then work out a system that suits your particular game (e.g. the enemy must be able to see two out of three points on the player’s body to consider the player visible).

Working with it

That’s how it works. Download the sample to see all of the code, and refer to the code comments and the theory in the article if you get lost. Of course questions and comments are welcome too.

In the example project I change the enemy’s sprite colour to red when he can see the player. You can see some debug lines in the Scene window as well, indicating where the enemy is looking. I recently did a post about using debug lines if you want a bit of background on that technique.

Here are a few ways you could adapt this method to suit your game:

  • Change the enemy’s field of view to make it more narrow or wider
  • Add multiple visible points to the player and/or use multiple rays from the enemy to create more nuanced vision
  • Remove the vision circle trigger collider and always consider the player in visual range (i.e. make the enemy’s visual range effectively infinite)
  • Ignore the field of view angle totally if you want enemies to see in all directions simultaneously (e.g. a turret)
  • Expand the check for obstacles to ignore transparent objects (e.g. if the player is behind a glass window the enemy can still see him).

6 thoughts on “Giving Enemies the Power of Sight!

  1. The audio files are not in downloaded assets. Please advise.

    Also are there any other files that are not in the EnemySample.zip? Do you have files in other places that I can download.

    If I can get the “power of sight to work” I will zip it to you at the latest Unity level.

      • There are audio requirements in the Code:

        public IEnumerator Taunt()
        {
        // Check the random chance of taunting.
        float tauntChance = Random.Range(0f, 100f);
        if(tauntChance > tauntProbability)
        {
        // Wait for tauntDelay number of seconds.
        yield return new WaitForSeconds(tauntDelay);

        // If there is no clip currently playing.
        if(!audio.isPlaying)
        {
        // Choose a random, but different taunt.
        tauntIndex = TauntRandom();

        // Play the new taunt.
        audio.clip = taunts[tauntIndex];
        audio.Play();
        }
        }

Leave a Comment