09.creating-lines-of-sight-with-raycasts

Creating lines of sight with raycasts

In this lesson, we will take a break from altering the bat scene for a moment. Instead, we’ll look at a toy that focuses on the next mechanic we’re going to implement: making the bat “see” the robot.

We’ll be making a basic version of the bat, which looks in the direction the cursor is pointing. If we point at the robot, the bat can see it, and it looks angry. In a later lesson, we’ll combine this functionality with the movement we programmed earlier.

Looking at the hiding behavior on its own might help you understand how it works when integrated into the bat scene.

How to Make Objects See

To give a bat a line of sight, we will use a RayCast2D node.

It casts out a ray in a straight line and stops when it detects a physics object.

This works a little differently than in the real world. Rather than using light to see, it’s like a hand that reaches out and records the first thing it touches.

This is a slow visualization to help you see what the RayCast2D simulates. It casts a line to a point and records the closest physical object the line intersects.

In this example, it’s the robot, which makes the bat unhappy.

Let’s see this RayCast2D node in action.

The BatLook Toy Folder

Open the SideScroller/BatLookToy/ folder, where you’ll find some pre-made scenes to work on for this chapter.

Open the BatLookToyRoom.tscn scene. It has an instance of a simplified bat and the robot to move around.

If you press F6 to run the scene, nothing happens.

Adding a RayCast2D to the Bat

Open the BatLook.tscn scene. It has a Sprite as the root node. The LookDirection scene is a helper by GDQuest to help us visualize raycasts.

Add a RayCast2D to the scene.

Let’s inspect the RayCast2D node’s properties in the Inspector.

The Cast To property is the point the ray tries to reach relative to the raycast’s position.

We set it to Vector2(1000, 0) to make it face right by default. In code, we’ll rotate the raycast to follow the mouse cursor.

A vector pointing to the right corresponds to an angle of zero degrees in Godot, making it easy to rotate it from code.

Like other objects interacting with physics bodies, the raycast has a collision mask.

It can only intersect with physical objects on the mask we give it. In our case, we flag 1 and 2 because they’re the layers the walls and robot are on.

We also turn on the Enabled property to activate the raycast. Otherwise, by default, it is disabled and does not detect anything. You will often keep raycasts inactive in games to help with performance.

You should see the ray point to the right in the editor.

Updating the Raycast and Getting Collisions

Let’s move on to the script. When you open it, you’ll see this.

extends Sprite

var bat_textures := [
    preload("bat_aware.png"),
    preload("bat_hang.png")
]

onready var look_direction := $LookDirection

We need to do three things:

  1. Update the raycast to point at the cursor.
  2. Change the bat’s texture if the raycast hits the robot.
  3. Update the LookDirection scene, so we have a visual.

First, let’s update the raycast. Add a reference toward the top.

onready var raycast := $RayCast2D

Then, add the physics code.

func _physics_process(delta: float) -> void:
    # Rotate the raycast to look at the mouse cursor
    raycast.look_at(get_global_mouse_position())
    raycast.force_raycast_update()

Usually, a raycast checks for collisions and then reports them in the following physics frame.

The force_raycast_update() function immediately checks for a collision instead of waiting for the next physics frame.

This is important for an accurate reading because we want to check for a collision in this frame.

After the update, we check for a collision and either set the bat texture to angry if it hits a KinematicBody2D, or neutral otherwise.

func _physics_process(delta: float) -> void:
    # ...
    # We check if the ray collides with a KinematicBody2D. As the only
    # KinematicBody2D we can hit is the player, we update the bat's texture to
    # the mean-looking sprite.
    #
    # The is keyword checks for the type of the collider we get with
    # get_collider()
    if raycast.get_collider() is KinematicBody2D:
        texture = bat_textures[0]
    else:
        texture = bat_textures[1]

In a larger game, we might check which KinematicBody2D we hit. It may be another enemy rather than the player.

But in this scene, the only other KinematicBody2D is the player, so we don’t need extra checks.

All that’s left is to update the LookDirection instance. Since it draws a vector, we set its vector to the collision point.

func _physics_process(delta: float) -> void:
    # ...
    look_direction.vector = to_local(raycast.get_collision_point())

The is Keyword

You’ve already learned about types in code. They’re names we use to restrict which values are compatible with variables or functions. As we’ve seen, types can be anything from Vector2, float, and so on.

Sometimes, it’s useful to check the type of a variable or parameter using the is keyword. We can also use it to check what kind of node an object is.

We used it here because we know the robot is a KinematicBody2D to save some additional coding.

And there we go! A first look at raycasts. If you run the BatLookToyRoomVideo.tscn scene, you’ll be able to see the raycast in action.

In the next chapter, we’ll use this new knowledge of raycasts to improve our flying bat.

The Code

Here’s the full code for the BatLook.gd script.

extends Sprite

var bat_textures := [
    preload("bat_aware.png"),
    preload("bat_hang.png")
]

onready var look_direction := $LookDirection
onready var raycast := $RayCast2D

func _physics_process(delta: float) -> void:
    # Rotate the raycast to look at the mouse cursor
    raycast.look_at(get_global_mouse_position())
    raycast.force_raycast_update()
    # We check if the ray collides with a KinematicBody2D. As the only
    # KinematicBody2D we can hit is the player, we update the bat's texture to
    # the mean-looking sprite.
    #
    # The is keyword checks for the type of the collider we get with
    # get_collider()
    if raycast.get_collider() is KinematicBody2D:
        texture = bat_textures[0]
    else:
        texture = bat_textures[1]
    # Visualizes the RayCast2D.
    look_direction.vector = to_local(raycast.get_collision_point())