10.giving-the-bat-a-line-of-sight

Giving the bat a line of sight

Now we’ve explored the RayCast2D node, we can use it in our Bat scene.

After this lesson, our robot will be able to hide from the bat. As the bat flies around, the raycast will update in real-time.

We’ll alter the bat scene by:

Let’s get going!

Adding the Raycast

Add a RayCast2D to the bat scene.

We need to set the Cast To vector here.

We make it point to the right and set the value to match the radius of the AggroArea’s collision shape.

This will be how far the bat can “see.” The raycast can’t collide with anything beyond its range.

Be sure to include the robot’s collision layer in the Collision Mask, or the raycast won’t detect it.

Lastly, turn the ray’s Enabled property on.

Updating the Raycast

In our bat script, let’s first store a reference to the raycast so we can use it in the script. Add this to the top of the script:

onready var raycast := $RayCast2D

The bat should only move toward the robot if:

Once more, we need to rewrite how we calculate the bat’s movement direction.

Please delete the line in the if target: conditional block at the top of _physics_process(). We’re going to replace it with the following.

func _physics_process(delta: float) -> void:
    # You should already have the following two lines in your script.
    var direction := Vector2.UP
    if target:
        # Robot has entered the areas and is within range, so we check if the
        # raycast is unobstructed
        raycast.look_at(target.global_position)
        raycast.force_raycast_update()
        # If the collider is the robot, then we have a direct view to it.
        #
        # Otherwise, it means something is blocking the bat's line of sight.
        if raycast.get_collider() == target:
            direction = to_local(target.global_position).normalized()

    # ...

The first thing to do is update the raycast. Recall that target is valid if it entered the AggroArea.

So, we know when we have a target. In that case, we can safely call the look_at() function, which keeps rotating to point at the target.

As seen in the previous lesson, we call the force_raycast_update() function every frame to ensure collisions are accurate.

func _physics_process(delta: float) -> void:
    # ...
    if target:
        # Robot has entered the areas and is within range, so we check if the
        # raycast is unobstructed
        raycast.look_at(target.global_position)
        raycast.force_raycast_update()
    # ...

Then, if the target is the same as the collider, we know there’s no wall between the bat and the robot, so we move the bat toward it.

func _physics_process(delta: float) -> void:
    # ...
    if target:
        # ...
        if raycast.get_collider() == target:
            direction = to_local(target.global_position).normalized()
    # ...

And that’s it! If you run the scene, the player can sneak around as needed.

However, if you run the scene and play around a bit, you might notice strange behavior when the bat and robot collide.

It’s funny, but it’s not really what we expect from an enemy!

In the next lesson, we’ll deal with what happens when the bat touches the robot by adding a simple fail state.

The Code

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

extends KinematicBody2D

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

var velocity := Vector2.ZERO
var target

onready var raycast := $RayCast2D
onready var aggro_area := $AggroArea


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:
        # Robot has entered the areas and is within range, so we check if the
        # raycast is unobstructed
        raycast.look_at(target.global_position)
        raycast.force_raycast_update()
        # If the collider is the robot, then we have a direct view to it.
        #
        # Otherwise, it means something is blocking the bat's line of sight.
        if raycast.get_collider() == 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(player: KinematicBody2D) -> void:
    target = player


func _on_player_exited(player: KinematicBody2D) -> void:
    target = null