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.
- Check whether Player is within detection area of Enemy.
- 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.