08.making-the-bat-chase-the-robot

Making the bat chase the robot

In this lesson, we’ll make our bat chase the robot instead of the mouse cursor.

It will also make the follow mechanic a little more flexible.

In the previous lesson, we hard-coded a distance of 500 pixels to follow the mouse.

In this lesson, we’ll use an Area2D instead.

When the robot enters the area, we’ll keep track of it in a variable. If the robot exits the area, we’ll reuse a trick from the tower defense series: set the target variable to null.

The logic for the bat is simple: if there is a target, the bat moves toward it. Otherwise, if the target is null, the bat moves up to the ceiling.

Detecting the Robot

Add an Area2D named AggroArea to the bat scene with a CollisionShape2D as a child.

Use a circular shape and set the radius. The circle’s radius determines when the bat detects the robot.

As in the DisappearingPlatform we made before, set the AggroArea’s collision mask to only detect the robot’s layer.

In the Bat script, we add the target variable, which we’ll use to store the robot and a reference to the Area2D we just made. Add these lines at the top:

extends KinematicBody2D

# Will store the robot when it enters the Area2D.
var target: KinematicBody2D

# The Area2D that detects the player.
onready var aggro_area := $AggroArea

Let’s look at the new logic in the _physics_process() function.

func _physics_process(delta: float) -> void:
    var direction := Vector2.UP
    if target:
        direction = to_local(target.global_position).normalized()

    var desired_velocity := max_speed * direction
    var steering_vector := desired_velocity - velocity
    velocity += steering_vector * drag_factor
    
    velocity = move_and_slide(velocity)

You may notice we actually need fewer lines of code than before. Only the direction calculations changed.

You’ll want to replace the first four lines in your existing _physics_process() function.

func _physics_process(delta: float) -> void:
    var direction := Vector2.UP
    if target:
        direction = to_local(target.global_position).normalized()
    # ...

If there’s a target, the direction changes to the target’s position.

The function to_local() is present in all nodes that extend Node2D, like our area.

We use to_local() to convert the robot’s global position into an offset from the bat.

The function to_local() returns a Vector2 value so we can normalize it and get a direction vector.

The rest of the _physics_process() function is unchanged!

Before, we used the mouse cursor’s position. Now, we track the robot.

But there’s still a missing piece in this puzzle. We need to set the target variable. For this, we’ll use signals.

Connecting the Signals

We’ll use two signals from the Area2D we made:

These emit every time the area detects a physics body is entering or exiting the collision shape.

Connect them in the _ready() function.

func _ready() -> void:
    aggro_area.connect("body_entered", self, "_on_player_entered")
    aggro_area.connect("body_exited", self, "_on_player_exited")

All that’s left is to create the functions.

func _on_player_entered(robot: KinematicBody2D) -> void:
    # Sets the target to the robot when it enters the Area2D.
    target = robot


func _on_player_exited(robot: KinematicBody2D) -> void:
    # When the robot exits the Area2D, the target is out of range.
    target = null

And we’re done! The bat will now chase the robot when it’s close!

But notice how the bat will detect the robot even if the robot is behind an obstacle.

You can try to hide below a platform place between the bat and the robot. The bat will still move toward the character.

In the next lesson, we’ll explore how we can have the robot hide from the bat to make avoiding it easier and more enjoyable.

The Code

Here’s the current code for the bat so far.

extends KinematicBody2D

# Will store the robot when it enters the Area2D.
var target: KinematicBody2D

# The Area2D that detects the player.
onready var aggro_area := $AggroArea

export var max_speed := 300.0
export var drag_factor := 0.1

var velocity := Vector2.ZERO


func _ready() -> void:
    aggro_area.connect("body_entered", self, "_on_player_entered")
    aggro_area.connect("body_exited", self, "_on_player_exited")

func _physics_process(delta: float) -> void:
    var direction := Vector2.UP
    if target:
        direction = to_local(target.global_position).normalized()

    var desired_velocity := max_speed * direction
    var steering_vector := desired_velocity - velocity
    velocity += steering_vector * drag_factor
    
    velocity = move_and_slide(velocity)

func _on_player_entered(robot: KinematicBody2D) -> void:
    # Sets the target to the robot when it enters the Area2D.
    target = robot


func _on_player_exited(robot: KinematicBody2D) -> void:
    # When the robot exits the Area2D, the target is out of range.
    target = null