A small clay guard stands on warm paper next to a looping pencil path, with a pencil and a clay-orange bead on an oak desk.
|

AI NPC Behavior Systems in Unity

Your guard walks its route perfectly and ignores you even when you stand right in front of it, so here is how to find out which check is failing.

This is a debugging-only follow-up to post 179, which builds the patrol, notice, chase, and give-up guard in Unity and Godot; here you leave that machine alone and find out why the notice transition never fires.

You will not add states or redesign anything. You will add a few temporary lines, read them, fix one thing, and delete the lines.

The guard from post 179 decides it has “seen” you with three checks: distance, view angle, and a ray from its eyes that must hit you and not a wall. Patrol only leaves when all three pass. So either a check always fails, the result is thrown away, or the code is not running where you think.

Step 1: Prove the code runs at all

Before you suspect the math, prove the function is being called. Add one temporary line at the top of the sight check.

In Unity, inside CanSee():

Debug.Log($"{name} CanSee called");

In Godot, inside can_see():

print(name, " can_see called")

Press play. If the console stays empty, the problem is not sight. Check these:

  • The script is attached to the guard you are watching, not to a prefab you never placed or a copy hidden somewhere in the scene.
  • The component or node is enabled.

If the console floods with lines, the function runs. Delete that line and move on.

Step 2: Print each check, not just the result

A single true or false tells you nothing about why. Split the result so you can see which gate closes.

In Unity, temporarily log the three values just before the early returns in CanSee():

Debug.Log($"dist {to.magnitude:F1} / {sightRange}  angle {Vector3.Angle(transform.forward, to):F0} / {sightAngle * 0.5f:F0}");

In Godot, the same idea inside can_see():

print("dist ", snapped(to.length(), 0.1), " / ", sight_range,
      "  angle ", snapped(rad_to_deg((-global_transform.basis.z).angle_to(to)), 1.0), " / ", sight_angle * 0.5)

Now walk your player straight up to the guard’s face and watch the numbers.

Step 3: Distance

If the distance number never drops below the range, look at what you are measuring between.

  • The player reference points at the wrong thing. If the player field holds a spawn marker, a camera rig, or an empty parent that stayed at the origin, the distance will not change as you move. Watch the number while you walk. If it stays still, your reference is stale.
  • The range is set in the Inspector, not the script. The default in the code is only a starting value. Once a value is saved on the guard in the scene, that saved value wins. Select the guard and read the field directly.

Step 4: View angle

If the distance passes and the angle never drops under the limit, the guard is probably looking somewhere other than where its body points.

  • Unity forward is +Z. If your guard model was built facing another way, transform.forward points out of its back or its side. Select the guard, switch the move tool to local space, and look at which way the blue arrow points.
  • Godot forward is -Z. Post 179 calls this out, and it is still the most common miss. A model that faces +Z will have the player “behind” it while you stand at its nose.
  • The angle value is the full cone. The code halves it before comparing. If you typed in a number thinking it was the half angle, the cone is narrower than you meant.

Stand behind the guard. If the angle reading gets small, its forward is backward.

Step 5: Line of sight

If distance and angle both pass but the guard still ignores you, the ray is the problem. Add one more temporary print that names what the ray actually hit.

In Unity, after the raycast:

if (Physics.Raycast(eye, to.normalized, out RaycastHit h, sightRange))
    Debug.Log("ray hit " + h.transform.name);
Debug.DrawRay(eye, to.normalized * sightRange, Color.red);

Debug.DrawRay draws a line in the Scene view so you can see where the ray goes.

In Godot:

if not hit.is_empty():
    print("ray hit ", hit.collider.name)

The name you get back usually explains everything.

  • It hits part of the guard. A weapon, a shield, or a child collider sticks out in front of the eye point. Exclude it, or move it to a layer the ray ignores. In Godot, post 179 already excludes the guard’s own body, but not its child bodies.
  • It hits a trigger volume. A large trigger around the guard or the player, such as a hearing radius or a pickup zone, can catch the ray first. Whether rays stop on triggers depends on your physics query settings, so check them in your engine’s physics settings.
  • It hits nothing. The player has no collider, or the player is on a layer the ray skips. In Unity, objects on the Ignore Raycast layer are skipped by a default raycast. In Godot, check that the player’s collision layer is inside the ray query’s mask.
  • The eye point is in the wrong place. The code lifts the eye a fixed height above the guard’s origin. If your guard’s origin sits at its waist, the eye ends up above its head. If the player is short or crouched, aim at the player’s chest instead of its feet.

Step 6: The detector is on a different object than the player check expects

This one deserves its own step, because the ray can hit the player and the check can still fail.

The Unity code compares hit.transform == player. The Godot code compares hit.collider == player. Those comparisons only pass if the object holding the collider is the exact object you assigned as the player.

Often the collider sits on a child named Body or Hitbox while the player field points at the parent. The ray hits the child, and the comparison says no.

Pick one fix and stick to it:

  • Point the player field at the object that actually holds the collider.
  • Or, in Unity, compare against the hit’s root or its attached rigidbody instead of the transform itself.
  • Or move the collider up onto the object you assigned.

In Godot, post 179 already warns you to point the export at the player’s physics body. Check that it still does after any scene reorganization.

Step 7: The check passes, but the guard stays in Patrol

If every gate passes and the guard still walks its route, the state is not changing, or something changes it straight back.

Add a temporary label that shows the current state above the guard’s head. In Unity, a world-space text object parented to the guard works. In Godot, a Label3D child that you update each frame with the state name works. Watch it while you stand in view.

  • It never changes. Confirm the patrol branch actually calls Enter(State.Notice) or enter(State.NOTICE) when the check passes. A stray break or an elif in the wrong order can skip it.
  • It flickers to Notice and back. Something resets it, such as a spawn script or a second copy of the guard script on the same object.
  • It reaches Notice and stops. The notice timer is not counting down. Check that the timer is set on entering Notice and that the notice branch runs every frame.

Step 8: Remove every debug line

Once it works, delete the logs, the drawn rays, and the state label. Per-frame logs slow the editor, and a forgotten label will ship. Mark each temporary line with the same comment, such as // DEBUG-194 in C# or # DEBUG-194 in GDScript, and search for that tag before you commit.

Fix one thing, test, and only then look at the next. If you change three values at once and the guard starts noticing you, you will not know which one fixed it.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *