When you create a video game, you most probably place the player in the game world with other non-playable characters. They can be enemies or can be neutral. In the later situation, it is not much logic to implement, but a behavior of enemies is more complex. A typical case is to make them to detect the player, to chase (to follow) and to attack. In this post I would like to demonstrate you this “bare bone” logic, that includes simple code to define aforesaid aspects.

How does an enemy detect the player

The first element of the enemy behavior is an ability to detect the player. There are various techniques to do so, but I discovered that the most common approach is to use Area components to do so. Each Area element can detect other objects (bodies), that enter or leave it. A particular zone of detection can be defined with collision shapes or collision polygons. I found an interesting pattern – use a collision polygon and define with it a scope of vision of an enemy. Take a look on the screenshot below and you will see that, it is similar to a human scope of vision.

A basic idea is simple: when a body (player) enters this collision polygon scope, it will be detected. Godot uses a concept of signals – they are also called observers in reactive programming; you can find more here, we will not stop on them – and once the detection occurs, the Area will trigger a signal method _on_Area_body_entered(body). On this step we need to store a reference to a target, so an enemy can use it later (to chase or to attack). Similarly, once the player leaves this area, will be triggered a signal method _on_Area_body_exited(body) and the reference of the target should be removed.

This logic is implemented in the following code snippet:

func _on_Area_body_entered(body):
    if body is Player:
        target = body as Player


func _on_Area_body_exited(body):
    if body is Player:
        target = null

Please note, that I use class names in my code. An alternative way is to use groups, like that:

func _on_Area_body_entered(body):
    if body.is_in_group('Player'):
        target = body as Player


func _on_Area_body_exited(body):
    if body.is_in_group('Player'):
        target = null

To be honest, GDScript is not Java in terms of object-oriented design and has known limitations, especially when it comes to types. This topic is outside of the scope of this post, and I think it requires a separate research, however what I want to say you here is that it really does not matter which style would you prefer to use in your project – with class types or groups.

Chase the player

Once, the player is known, the enemy can start to chase the player and once it will be close enough (in a shooting range), an enemy will attack. But let go step by step and begin with a chasing behavior. From a technical point of view, the process of chasing consists of two stages (or parts):

  • An enemy orients in the player’s direction (or simply speaking, rotates to the player)
  • An enemy moves close to the player

For the first point it is very simple. As the enemy is a typical KinematicBody, we can use a built-in method look_at(). This function accepts as the first argument a Vector3 object, that defines a position of the target and rotates an enemy, that its -Z axis will point towards the target’s position. The second argument is Vector3.UP, that defines a rotation pivot for the target. As the method is built-in we don’t need to write a lot of code:

func look_at_player():
    look_at(target.global_transform.origin, Vector3.UP)

Once the player enters the collision area, and it will be detected, the enemy will rotate itself to look at the player.

A more complex task is to actually chase the player. By chasing I understand here movement towards the player. That is where we will use the target reference variable. In other words, we need to determine a direction to move (e.g. where player is) and move there using move_and_slide method. Let see how it looks like with code:

func follow_player(delta):
    var direction = (target.transform.origin - transform.origin).normalized()
    move_and_slide(direction * speed * delta, Vector3.UP)

Don’t forget to normalize the direction vector: from the technical point of view, a normalized vector is the one, that is scaled to unit length.

Shoot at the player

The last thing to implement in this post is how an enemy attacks the player. In my sandbox project I organized weapons as separate components, that can be used by both player and enemies. An example enemy holds an assault rifle that and shoots with an interval, which is defined by a timer. As with the collision area, we need to implement a signal for the timer, that will call the shoot function. For more realistic behavior, we need to check, that the rifle can fire (for example, it is not reloading in the moment). This is a part of a gun logic, so we will not stop on it here, however if you are curious, feel free to research the sandbox game.

The code that implements an attack behaviour is presented in the code snippet below:

func shoot_to_player():
    if rifle.is_possible_shoot:
        rifle.shoot()


func _on_ShootTimer_timeout():
    shoot_to_player()

The first function is a definition of shooting and it checks, that a rifle is ready to use, and if it is true, than call the shoot method. The second function is a timeout signal for the timer. The whole physics_process for an enemy (what is called each game loop iteration) is following:

func _physics_process(delta):
    if target:
        var space_state = space_state = get_world().direct_space_state
        var result = space_state.intersect_ray(global_transform.origin, target.global_transform.origin)
        var collider = result.collider
        if collider.is_in_group("Player"):
            look_at_player()
            follow_player(delta)
            if shoot_timer.is_stopped():
                shoot_timer.start()
        else:
            shoot_timer.stop()

Source code

Hope you enjoyed this post and it helped you. I encourage you to explore the whole sandbox project in this github repository. Feel free to fork it and to play around.

Conclusion

In this post we created a simple yet complete enemy behavior, which is able to complete essential operations, such as player detection, moving (chasing) towards the player and to attack (to use weapons). If you have any questions or comments, don’t be scared to contact me or to write a comment below. I am quite to new game development, so I admit that not everything in my solution is ideal, so if you think, that yours is better – I’m open to hear about it.