Vision-based enemies

(LOL I promised myself I’ll call it a day and play Palworld for the night, but here we are…)

OK so, as the title suggests, for a while now I’ve been stuck with the old trigger-based enemy targeting system. You can sneak up from behind an enemy for example, and miraculously, he would know you’re behind him (although he can’t see you, and let’s say he also can’t hear you (for now…)), turn around, chase and try to attack you

I wanted to change that a bit and make it a vision-based system, so if and only if you’re in his field of view (later on I might add hearing footsteps if I ever get a ‘PlayerCrouchingState.cs’ state machine integrated), and you’re in his targeting sphere, and he’s considered aggressive (activated by a boolean), can he see and chase you. How can we go around that? (Doing that in ‘AIController.cs’ would’ve been fairly simple, but in state machines…? It raised my eyebrows)

My guess is, that code goes in ‘EnemyPatrolState.cs’ and/or ‘EnemyIdleState.cs’

The Vector from the viewer to the target is

Vector3 toTarget = target.transform.position - transform.position

The vector the character is facing is


So the angle between the characters look and the target is

Vector3.Angle(transform.forward, toTarget);

Then take the 1/2 angle of the character’s visual range… Here’s a clue: Most humans have about a 180 degree field of vision, so if that angle is less than 90 (half of 180), then the target can be seen.

Obstacles are a bit extra work, however. For that, in addition to the angle, you need a ray trace from the character’s eyes along the toTarget Vector. If the closet thing in the RaycastAll is the target, you can see him.

For performance sake, you always want to test distance first, then angle, and only then raycast.

we have disabled the ‘AIController.cs’ script for the enemies, so I’m guessing this adjustment goes somewhere in “Fighter.cs”…? (I have a function I once created to avoid hitting through obstacles known as ‘HasLineOfSight()’, which was later called in ‘Fighter.AttackBehaviour()’… is this a good place to place that code?)

It’s certainly not where it would be called from… You’re going to be wanting to check these things in the Tick() methods of most of your Enemy states…
You should already have a method in EnemyBaseState to check if the player is in Chasing Range…

fair enough, I’ll give this a go in the morning as I’m quite burned out today and keep you updated :slight_smile: (hopefully it’s as simple as it sounds)

on a serious note though, isn’t there something we should be deleting to stop the enemy from detecting us when we enter his sphere?

In the RPG course, the enemy detected the layer by checking range in AIController.cs. That should be removed at this point.

haven’t deleted the AIController.cs script, I just deactivated it (it has the XP Reward value, and deleting that scripts releases NREs for some reason, so I kept it and just deactivated it :stuck_out_tongue_winking_eye: )

Anyway, I’ll go give this a go now :slight_smile:

LOL that was not an extremely easy function to create, but it was manageable… I know the function works, BUT… I don’t know where to properly assign it to get it to function correctly. Here’s what I came up with (I placed this function in ‘EnemyBaseState.cs’):

    protected bool CanSeePlayer()
        Vector3 distanceToPlayer = stateMachine.Player.transform.position - stateMachine.transform.position;
        float angleToPlayer = Vector3.Angle(stateMachine.transform.forward, distanceToPlayer);

        // Checking if the player is within the field of view of the angle
        if (angleToPlayer <= stateMachine.FieldOfViewAngle * 0.5f)
            // If we get in here, it means player is in angle range, so now check for obstacles:
            RaycastHit[] hits = Physics.RaycastAll(stateMachine.transform.position, distanceToPlayer, stateMachine.PlayerChasingRangedSquared);
            foreach (RaycastHit hit in hits)
                if (hit.transform == stateMachine.Player.transform) {
                    // this here means that the closest obstacle is the player, so return true (and start chasing the player)
                    return true;
        return false;

‘FieldOfViewAngle’, a float variable, was assigned by me in ‘EnemyStateMachine.cs’ to 180 degrees (so 180 * 0.5 = 90 degrees)

I tried assigning it in ‘EnemyPatrolState.cs’ and ‘EnemyIdleState.cs’, so now instead of just checking for ‘IsInRange()’ (when we do that over there), I’m now checking for both range AND ‘CanSeePlayer()’, but the enemy behaviour was a bit all out of whack… as if his forward direction and his body allignment don’t match, so he can see me from weird angles, but not other angles that you’d typically expect the enemy to be able to see you from… how can I fix this?

I would actually put it in EnemyBaseState so you only have the function in there once, and then call it from anywhere you are also calling IsInRange() (but only if IsInRange() is true.

Rather than saving a FieldOfViewAngle, I would save it as a 1/2 angle to save the extra computation.

In terms of forward vector vs animation, you could try assigning a transform like the Hips (or perhaps the head) and using it’s forward vector as a comparison.

so… it’s a nested if statement, right?

OK genuinely curious, is that just a name change, or something else…?

umm… how do we do that? (I’ll do some research about it, but yes I have a doubt about that :sweat_smile:)

any chance we can turn the vision angle into gizmos, so I can see what I’m dealing with?

Just means that if you want the field of view to be 180 degrees, set the variable to 90 degrees instead of doing floating point division every frame.

Put a [field: SerializeField] Transform on the StateMachine and drag either the hips or the head into it. Use that transform for the transform.forward part of the calculation

The enemy is still acting weird… the way he’s acting right now, is that he will look at the player, try slash him once and then turn around and walk away for a bit, then look at the player again, try slash him once and then turn around and walk away for a bit, and it’s a cycle pretty much now…

even after I changed the transform to use his hips in the mathematical line, as follows:

            float angleToPlayer = Vector3.Angle(stateMachine.EyesForwardDirection.forward, distanceToPlayer);

(“EyesForwardDirection” is my attempt to do so, and I failed to draw proper gizmos…)

all I did next was in the Idle and Patrolling state, I would check if “CanFacePlayer()” only INSIDE the “IsInChaseRange()” block, so the code has to go through both before getting there… What went wrong?

Honestly, I don’t know. I’ve had mixed results with line of sight and field of view. Start by taking out the line of sight (raytrace). Just test the angle.

Actually, one thing I remember about the raytrace… if you start from transform.position, you wind up hitting the ground if there’s any kind of slope. While the direction calculation is fine, the raytrace origin should be transform.position + Vector3.up.


Vector3 distanceToPlayer = stateMachine.Player.transform.position - stateMachine.transform.position;

This should be ‘direction’, and normalised

Vector3 directionToPlayer = (stateMachine.Player.transform.position - stateMachine.transform.position).normalized;


RaycastHit[] hits = Physics.RaycastAll(stateMachine.transform.position, distanceToPlayer, stateMachine.PlayerChasingRangedSquared);
foreach (RaycastHit hit in hits)

you probably want to sort the hits by distance, the way we did for the mouse raycast


foreach (RaycastHit hit in hits)
    if (hit.transform == stateMachine.Player.transform) {
        // this here means that the closest obstacle is the player, so return true (and start chasing the player)
        return true;

This does not mean that the closest obstacle is the player. It means that one of the things you hit is the player. The closest obstacle is the first thing you hit (after sorting the hits). If that is the player, then the closest obstacle is the player

is this the function we are talking about?

RaycastHit[] RaycastAllSorted() {

            // This function ensures that if two items are close in the game world in front of our camera, that
            // we don't accidentally pick the item behind what the camera sees in front of it first

            // Get all hits
            RaycastHit[] hits = Physics.SphereCastAll(GetMouseRay(), raycastRadius);

            // Sort by distance
            float[] distances = new float[hits.Length];

            // build array of distances
            for (int i = 0; i < hits.Length; i++) {

                distances[i] = hits[i].distance;


            // Sort the hits
            Array.Sort(distances, hits);

            // Return

            return hits;


I found it in ‘PlayerController.cs’

Yes. After that sort, the first hit (hits[0]) is the closest thing you hit. As Brian mentioned, it will likely be the terrain so you should lift the origin of the ray.
Also note that you can hide behind a traffic sign. The raycast will hit the sign and then your enemy won’t see the player
This guy is invisible


I’ll place that function in ‘EnemyBaseState.cs’, and use that instead of this ‘Physics.RaycastAllSorted()’, right…?

                RaycastHit[] hits = Physics.RaycastAll(stateMachine.transform.position, directionToPlayer, stateMachine.PlayerChasingRangedSquared);

hmm… would striking like 5 rays instead of one help with that? 3 vertical, and 2 horizontal rays on the sides of the middle vertical ray


welp, the ‘RaycastAllSorted()’ got the vision system to work (when combined with a few other things I added off-screen), but… now comes the natural issue that follows up:


The enemy literally won’t even bother to fight back if you attack him, unless you’re in his field of view

After a bit of thinking, I got an idea… AFTER YOU ATTACK HIM, or if he sees you, why not deactivate his vision system (so he starts mindlessly pursuing you again) until you exit his targeter radius?

That way when you’re out of sight and out of mind, he’ll only chase you again if he sees you, and because you have deactivated his vision system (and he saw you again), then he’ll mindlessly chase you, independent of where you are around his eyes (which is exactly what I want when he sees you)

But when you’re in his view, it’s not limited by just his vision

Soo… how can I approach this?

The whole idea is that once he sees you, the combat gets intense until either one of you dies, or you escape his targeting zone, and when you’re out of sight (dead, ran away or whatever), he won’t know your existence until you are both in his range AND in his field of view


and for some reason (this is a new bug), beyond the players’ death, the enemy won’t recognize the player as a target again… should I have hooked the player death to an event relevant to the vision system by any chance? This is a new one…


Finally, if we can create gizmos to see what the enemy sees, that would help in development tremendously :slight_smile:

We can take these step by step, no need to address them all at once

This is why I don’t trust Full Self Driving cars. :stuck_out_tongue:

Not really, you’d be getting into quite the performance hit. You could make all obstacles on a layer, the player on it’s own later, and use a layermask so only these things are hit, and put the sign on a different layer that won’t be hit by the raycast.

lol I didn’t think of this before… might actually do that, if I end up implementing this system :slight_smile: (can’t tell without fixing the follow-up bugs, but more likely than not I will keep it after polishing it)

EDIT: I think I can put this for now on the backshelf. I want to focus more on getting the combat right first… the fact that there are no breaks between attacks means the player has zero chance of winning a fight, and that’s just a nightmare waiting to happen :slight_smile: - this is my next problem

Privacy & Terms