Enemy AI: chasing a player without Navigation2D and finding the path A *

Creating a game in which enemies must chase a player? It all starts with a simple one - we make the enemy run to the player. But what happens if it is behind a tree, or around the corner of a wall? Well, now the enemy will look pretty stupid - he will run into an object, fingering in place. Not very good!

To solve this problem, you can use the Navigation2D or AStar nodes built into Godot ( here is the GDQuest tutorial on both nodes ). But in Helms of Fury, we used a different method that worked great for our game, and we want to share it in this tutorial. Here's what it looks like:


Getting to work


We assume that you create enemies as KinematicBody2D objects and use a state machine to control their states. Not sure what a state machine is? I like this article explaining state machines and how to use them. Here is another article about implementing simple state machines in Godot .

Let's start with a simple Chase state for a stupid enemy who just runs to his target and gets stuck somewhere along the way:

# ChaseState.gd

func _init(enemy, params):
  enemy.dir = (enemy.target.position - enemy.position).normalized()

func _physics_process(delta):
  var motion = enemy.dir * enemy.speed
  enemy.move_and_slide(motion)

Traces of smell


To improve the state, we will force the player to leave a trace from his previous positions when moving. Thanks to this, when the enemy does not see the player, he will check to see if any of his past positions can be seen, and then follow them to the player. Since this is similar to how a dog takes a footprint, we will call it a smell footprint.

For the smell trace to work, we need to add the Timer node to the player, enable it to automatically start and set wait_time (we used 0.1s), and then add the code so that the player leaves a smell when the countdown ends.

# Player.gd
extends KinematicBody2D

const scent_scene = preload("res://Player/Scent.tscn")

var scent_trail = []

func _ready():
  $ScentTimer.connect("timeout", self, "add_scent")

func add_scent():
  var scent      = scent_scene.instance()
  scent.player   = player
  scent.position = player.position

  Game.level.effects.add_child(scent)
  scent_trail.push_front(scent)

Then you need to add the abandoned Scent.tscn itself. This is a simple Node2D scene containing a Timer so that the odor expires.

# Player.gd
extends KinematicBody2D

const scent_scene = preload("res://Player/Scent.tscn")

var scent_trail = []

func _ready():
  $ScentTimer.connect("timeout", self, "add_scent")

func add_scent():
  var scent      = scent_scene.instance()
  scent.player   = player
  scent.position = player.position

  Game.level.effects.add_child(scent)
  scent_trail.push_front(scent)

If you want odors to be visible during debugging, you can add a ColorRect node to them, and then just hide it. Having done this, you will see that when running after the player there is a trace of smell.

Now we need to awaken the enemies of the internal bloodhounds so that they follow these new smells when they do not see the player. But for this we need to add RayCast2D nodes to the enemies, and set up Physics Layers in Godot so that the beam knows what it can do collisions with.

Physics Layers


To set up Physics Layers in Godot, you need to click on Project in the top menu and then on Project Settings, then go to the Layer Names section in the lower left corner, and then select 2D Physics.


Give them any suitable name. After that, go to the objects in the game and expand Collision in the Property Inspector sidebar, and then click Β·Β· to assign them. For objects, they must be assigned as Layers.


After assigning the Physics Layers to objects, you need to change the RayCast2D of the enemies so that it checks those layers through which the enemies cannot move (in our case, this is solid, object, crate, hole, gate_closed).

After setting up Physics Layers, the final step is to change the state of the Chase.

# ChaseState.gd

func _init(enemy, params):
  chase_target()

func chase_target():
  var look     = enemy.get_node("RayCast2D")
  look.cast_to = (enemy.target.position - enemy.position)
  look.force_raycast_update()

  # if we can see the target, chase it
  if !look.is_colliding():
    enemy.dir = look.cast_to.normalized()

  # or chase first scent we can see
  else:
    for scent in enemy.target.scent_trail:
      look.cast_to = (scent.position - enemy.position)
      look.force_raycast_update()

      if !look.is_colliding():
        enemy.dir = look.cast_to.normalized()
        break

func _physics_process(delta):
  var motion = enemy.dir * enemy.speed
  enemy.move_and_slide(motion)

Now, when the enemy enters the Chase state, he begins to try to re-cast the player, and if there is nothing in his way, he will follow him! If there is something on the way, then he goes to the traces of smell and tries to re-cast each of them until he finds one of them, and then - pursues him!


And now everything works, the enemies turned into bloodhounds. To improve the system, it is possible to implement a system of avoiding collisions between enemies, but this is a topic for a separate article.

All Articles