Home

Character Detection

2023-01-29

Introduction

So I've been making a 3D top-down shooter game recently, and wanted to implement Player detection on an Enemy character. So to implement it I went with the approach used by Sebastian Lague in one of his videos. I had watched this video when I was learning Unity back in 2019, and still to this date love the way it works. So I went with the same method for my game, but this time in Godot. Since I find this approach pretty cool I wanted to write a walkthrough of it in Godot, and here we go!

Prerequisite

  • Basics of Godot
  • Basics of Programming

To keep this blog short I will be omitting things unrelated to the actual topic. So you can get to know the approach and its implementation in less time. For any feedback or suggestions, do update me in the comment section down below.

Example

Before jumping into the walkthrough, let's look at the end result.

Let's briefly go through the core logic behind making Enemy detect the Player.

  1. Check whether Player is within detection area of Enemy.
  2. If it is, then fire raycast towards Player. The raycast will detect only world objects.
    • So if collided with a wall, player wouldn't be detected.
    • Otherwise, if not collided with anything, the player is detected.

Exercise

Project Files

Optionally, you can download the project files from over here. Here most of the essential things like Player movement, Enemy, the world, etc. are all setup. So you only need to focus on implementing Player detection for Enemy.

Once downloaded you can find two folders, 01 Exercise and 02 Solution. The Exercise folder is the one you have to import in Godot. Whereas, the Solution folder contains the completed logic which you can later refer to.

Steps

1. Check if Player is within Detection Area

Open theĀ Enemy.tscnĀ scene. You'll see that all essential nodes and logic are already setup. We'll be working with the Detection script found on Detection node.

Let's start by defining the main function that will perform detection.

func is_detected() -> bool:
    return false

We'll be populating it a little later. Before that let's define some more functions.

First we check whether Player is within the Enemy's view distance.

var _view_distance: float = 15.0

func _is_player_inside_view_distance() -> bool:
    var spotlight_pos = $SpotLight.global_transform.origin
    var player_pos: Vector3 = Global.player.global_transform.origin
    var distance_to_player: float = spotlight_pos.distance_to(player_pos)

    return distance_to_player < _view_distance

Here, we check the distance between Enemy's spotlight and the Player. I've used spotlight for visualizing the detection area of Enemy. Since both Spotlight and Enemy are in same position other than their differing y-coordinate, either of their positions can be used for getting distance.

If distance is less thanĀ _view_distance, then Player is within the range.

But that's not enough. We also need to know whether Player falls into the view angle of Enemy.

onready var _view_angle: float = deg2rad($"SpotLight".spot_angle)

func _is_player_inside_view_angle() -> bool:
    var spotlight_pos = $SpotLight.global_transform.origin
    var player_pos: Vector3 = Global.player.global_transform.origin    
    var dir_towards_player: Vector3 = spotlight_pos.direction_to(player_pos)

    var forward: Vector3 = -$SpotLight.global_transform.basis.z
    var angle = forward.angle_to(dir_towards_player)

    return angle < _view_angle

Here we find the angle between -

  • Spotlight's forward vector (Enemy's forward can also be used).
  • Vector that points towards the Player from Spotlight/Enemy's position (normalized).

If this angle is less thanĀ _view_angleĀ (Spotlight's angle), then it's confirmed that Player is within the Enemy's detection area.

Finally, wrap these two functions together into one.

func _is_player_inside_detection_area() -> bool:
    return _is_player_inside_view_distance() and _is_player_inside_view_angle()

And that's it, we can now know whether Player is within detection area or not.

2. Raycast to Detect Player

Now let's define a function for creating raycast.

onready var _collision_layer: int = Global.get_layers_bitmask([3]) # Collision layer - "World"

func _create_ray(origin: Vector3, target: Vector3) -> Dictionary:
    var space_state = get_world().direct_space_state
    var result = space_state.intersect_ray(origin, target, [], _collision_layer)
    return result

Here a ray is created from origin to the target, where it only detects objects in the World layer. As I've made 3rd layer to be "World", that's why I'm using the value of 3.

Now let us head back toĀ is_detected()Ā function and make it complete.

func is_detected() -> bool:
    if _is_player_inside_detection_area():
        var origin: Vector3 = $SpotLight.global_transform.origin
        var target: Vector3 = Global.player.global_transform.origin
        var result: Dictionary = _create_ray(origin, target)
            
        if result.empty():
            $AnimationPlayer.play("Red")
            return true
            
    $AnimationPlayer.play("White")
    return false

Let's see how it works.

  • First, we check whether Player is within the detection area.
  • If it is, we fire a raycast towards it.
  • If raycast isn't collided with any object, then that means Player is detected, and we turn spotlight's color red.
  • Otherwise, if raycast is collided along the way, then that means Player is not detected, so we keep the spotlight's color white.

3. Update Enemy.gd

With detection logic all complete, we can use it in our Enemy script. So let's add reference to detection node in a variable and update the code inĀ _process(delta)Ā function.

onready var detection = $Components/Detection

func _process(delta: float) -> void:
    if detection.is_detected():
        animation_player.play("Focus", -1.0, 5.0)
        look_around.process_look_at_player()
    else:
        animation_player.play("Focus", -1.0, 1.0)
        look_around.process_look_around(delta)
        
    _apply_rotation()

So here the code should be easy to understand. If player gets detected, we increase Enemy'sĀ FocusĀ animation speed and make it point towards the Player. Otherwise, if not detected thenĀ FocusĀ is played at normal speed and the Enemy looks around as before.

Conclusion

Here we go, that's all we have to do to implement detection system for Enemy or any other object. So I hope you got the idea behind using this approach and how to implement it with the above example.

For any feedbacks or suggestions, do share your thoughts in the comment section down below. Thank You! šŸ˜Š

Assets

Here's the list of assets shared in the blog -

References

This video is the source of my knowledge for implementing detection system. I recommend checking out the whole series if possible, as its concepts can be used in any game engine.

Player Detection in second half of the videoĀ by Sebastian Lague.

License

The following blog is licensed underĀ CC BY 4.0Ā Arpit Srivastava.