08.pressure-plate-doors

Pressure plate doors

In this lesson, we’ll create doors that block the player’s progression. Each door can be opened temporarily by walking on a linked pressure plate.

We’ll also draw a dynamic line to show which plate activates which door.

In the scoreboard series, we used signals to communicate between nodes.

This section will demonstrate how to code mechanics like levers, switches, and other interactive objects that trigger something else.

Usually, we prefer keeping objects as independent as possible. A classical approach is to use node paths: providing the pressure plate with a path to the door node. We’d have to provide the nodes manually with the path of the other node they need to communicate with.

In our case, since the two nodes are supposed to work together exclusively, a simpler approach is possible.

We will set up each door as a child of a pressure plate node. It makes it easy to open the door from the pressure plate’s script and draw a line connecting them.

Note: The door being a child of the pressure plate in the node tree is unrelated to its location in the scene! We’re still free to move the door around and place it wherever we want. The scene structure is only to find each door easily from plates.

Looking at the door and pressure plate scenes

We prepared several scenes to save you repetitive steps.

In the ObstacleCourse_Part2/obstacles/doors/ directory, you’ll find a horizontal door, a vertical door, and a pressure plate.

The two doors use a StaticBody2D node as their root to block the player.

On the other hand, the pressure plate uses an Area2D node to receive a signal when the player walks over it.

All scenes also come with two animations named “closed” and “open.” Feel free to play the animations in the editor to see how they look.

Planning Out

To implement this mechanic, we need to:

  1. Add a timer for doors to close back, and make the open duration customizable.
  2. Draw a visual connection line between a plate and its door.
  3. Trigger the opening of doors when the character walks on a plate.

Let’s do this!

Exposing the Timer’s settings

We don’t want the doors to stay open forever. After a few seconds, they should close again. But we want to change this duration for each door, depending on how far it is from its pressure plate.

To do that, we need a Timer. Also, we want a way to tweak the timer’s duration for each pressure plate instance. The problem is, when instancing a scene, we don’t have access to its children.

How can we modify a scene’s child when the child isn’t accessible in the editor?

Let’s find out.

Open the PressurePlate.tscn scene, and add a Timer node.

Then, attach a script to the root node.

Our first task is to make it easy to adjust the Timer.wait_time property from the Inspector when adding PressurePlate instances to the obstacle course.

The timer is hidden in the PressurePlate scene, but we want to adjust its duration from the outside.

We will use a proxy property called open_time that we can directly modify in the Inspector. To do that, we use the export keyword when defining a variable in our script.

# The duration the linked door stays open in seconds.
export(float, 0.0, 100.0) var open_time := 1.0

We give open_time a default value of 1.0 seconds.

The values in parentheses after the export keyword are optional. They tell Godot that our variable is a float and that we want a lower limit of 0.0 and an upper limit of 100.0 seconds in the Inspector.

After saving the script, you should see the Open Time property appear in the Inspector whenever you select the PressurePlate root node.

If the Open Time property does not appear, go to the Scene menu and select Reload Current Scene.

We now use the value of open_time to set the Timer.wait_time property.

onready var timer := $Timer

func _ready() -> void:
    timer.wait_time = open_time

We called the open_time variable a “proxy” because we don’t use it directly in our code, except to set another property on a child node.

In Godot, we often create proxy properties to tweak children’s values.

Visually connecting the pressure plate and door

We will now prepare the line that links a pressure plate to its door. This helps the player to understand the obstacle course and decide where to go.

Add a Line2D node to the PressurePlate scene and move it up the tree, so it draws behind the plate.

The Line2D node is perfect for drawing lines and curves in the game. It lets you control the line’s width, color, width profile, etc.

By default, it has a blue color. We can change it to a red tone to match the pressure plate’s beam. The red we’re using is ff1852.

In the Inspector, change the line color to the palette’s red tone.

It uses an array of Vector2 points and connects them visually.

In our case, there will be 3 points:

This setup will always work and give us a nice 90-degree angle.

We need to calculate the Line2D points at runtime to draw a correct connection between the pressure plate and the door.

Back in the PressurePlate’s script, we first get the reference to the Door and Line2D nodes.

onready var line := $Line2D
# We'll add a door as a child of each PressurePlate instance in the obstacle
# course.
onready var door := $Door

We don’t have a Door node in the PressurePlate scene, but we can still declare variables that point to a Door node. We need to make sure to have door children for each instance in the obstacle course.

To draw the line, we update its points array.

func _ready() -> void:
    # ...
    # We update the line's points to draw an L-shaped line between the pressure
    # plate and the door.
    line.points = [
        # The first point is centered on the pressure plate.
        Vector2.ZERO,
        # The second point is to the left or right of the pressure plate,
        # vertically aligned with the door.
        Vector2(door.position.x, 0.0),
        # The last point is centered on the door.
        door.position
    ]

If you create a scene with a PressurePlate instance and a DoorHorizontal instance as a child of it, you will see a line connecting the two.

When running the scene above, we get the following result.

The player-plate interaction

Lastly, we need to code the interaction between the player and the plate.

When the character’s KinematicBody2D enters the plate, we want to:

When the timer runs out, we want to:

When the player enters the plate, we need to trigger the AnimationPlayer nodes for both the door and the plate.

We start by getting both of those in the PressurePlate script.

onready var animation_player := $AnimationPlayer
onready var door_animation_player := $Door/AnimationPlayer

We then connect the PressurePlate’s body_entered and the Timer’s timeout signals:

func _ready() -> void:
    # ...
    connect("body_entered", self, "_on_Area2D_body_entered")
    timer.connect("timeout", self, "_on_Timer_timeout")

We can move on to implementing those two callback functions. Let’s start with _on_Area2D_body_entered().

The only node capable of interacting with the pressure plate is the player, so we don’t need extra checks to ensure that the detected body is correct.

When the body_entered signal emits, we know that the player touched the plate. We play the open animations and start the timer to close the door when it runs out.

func _on_Area2D_body_entered(godot: Node) -> void:
    animation_player.play("open")
    door_animation_player.play("open")
    timer.start()

The _on_Timer_timout() function gets called when the timer times out. There, we play the closed animations on the door and the pressure plate.

func _on_Timer_timeout() -> void:
    animation_player.play("closed")
    door_animation_player.play("closed")

The animations enable and disable the door’s collision shape, allowing the player to pass only when the door is open.

With this, the mechanic is complete and we’re ready to test the pressure plate. Create a scene with an instance of the Godot character, a pressure plate, and a door as a child of the pressure plate.

When you walk on the plate, the door should open. It should then close after a few moments.

Changing the line’s color

Currently, when the door gets triggered, the line stays red. We will add a keyframe to change the color in each animation.

Open the PressurePlate scene, and click on the AnimationPlayer. Select the “closed” animation.

Then, select the Line2D node. Notice how keys appear next to properties in the inspector. Click on the key next to the color:

Godot will ask you if you want to create a “RESET” track. Leave the defaults, and press “Create”:

This creates a key in the “closed” animation.

Now select the “open” animation, and with the Line2D still selected, change the color to green. The green we’re using is 3dff6e.

Click the key icon again to insert a new track and key with the green color. This change will make the line turn green when opening the door.

If you feel fancy, you could animate the color from red to green instead of switching it immediately, but for our needs, even an abrupt change looks good enough.

Adding a warning when the door instance is missing

Games are complex beasts, so we often forget the requirements for our code to work. Even in small projects, it’s easy to make mistakes.

Godot has a built-in warning mechanism to warn us when a node isn’t set up correctly.

You already saw that with the Area2D node. When creating an Area2D, it initially displays a warning sign.

We can hover over the icon to get a help message or click to show it in a popup.

We can use the same warning system in our code!

To do so, we need two things:

  1. The tool keyword.
  2. The _get_configuration_warning() function.

Godot’s tool scripts

Any script with the tool keyword on a code line runs in the Godot editor.

This keyword allows us to create editor tools and plugins. For example, we use it in this course for the Practices dock.

For now, we’ll just use it for our node warning.

Add the following code to complete the PressurePlate script.

# By convention, we place the tool keyword at the top of the script, before the
# extends line.
tool
extends Area2D

# ...

# This function tells Godot to display a warning sign next to a node when it
# returns a message. If it returns an empty string, however, nothing happens.
func _get_configuration_warning() -> String:
    # We ensure that the pressure plate has a child node named "Door" and that
    # it is a StaticBody2D.
    var door_node := get_node_or_null("Door") as StaticBody2D
    if not door_node:
        return "This node needs a child node named Door, either an instance of DoorHorizontal or DoorVertical."
    return ""

In the function, we check if PressurePlate has a child named Door using the get_node_or_null() function. This function returns null if the node doesn’t exist.

The as keyword allows us only to keep the node if it is of a specific type. In this case, StaticBody2D. Otherwise, the node gets replaced with null.

Note: We don’t use the existing door variable directly because it updates once when we open the scene in the editor. But the _get_configuration_warning() will be called multiple times by Godot while we work on the scene, and we want to get the correct warning when someone adds or removes a Door node.

If we now use the PressurePlate scene without a Door child node, we’ll get the warning symbol and message in the popup.

Add a DoorHorizontal or DoorVertical instance as a child, and the warning disappears.

Time to practice!

Practice: Turning on the lights

Open the practice Turning on the lights.

We have a tiny level with a pressure plate.

Your task is to code the interaction between Godot and the Switch to turn on a light.

The code

Here is the complete code for PressurePlate.gd source file.

# By convention, we place the tool keyword at the top of the script, before the
# extends line.
tool
extends Area2D


# The duration the linked door stays open in seconds.
export(float, 0.0, 100.0) var open_time := 1.0

onready var timer := $Timer
onready var line := $Line2D
# We'll add a door as a child of each PressurePlate instance in the obstacle
# course.
onready var door := $Door
onready var animation_player := $AnimationPlayer
onready var door_animation_player := $Door/AnimationPlayer


func _ready() -> void:
    timer.wait_time = open_time
    # We update the line's points to draw an L-shaped line between the pressure
    # plate and the door.
    line.points = [
        # The first point is centered on the pressure plate.
        Vector2.ZERO,
        # The second point is to the left or right of the pressure plate,
        # vertically aligned with the door.
        Vector2(door.position.x, 0.0),
        # The last point is centered on the door.
        door.position
    ]
    connect("body_entered", self, "_on_Area2D_body_entered")
    timer.connect("timeout", self, "_on_Timer_timeout")


func _on_Area2D_body_entered(godot: Node) -> void:
    animation_player.play("open")
    door_animation_player.play("open")
    timer.start()


func _on_Timer_timeout() -> void:
    animation_player.play("closed")
    door_animation_player.play("closed")


# This function tells Godot to display a warning sign next to a node when it
# returns a message. If it returns an empty string, however, nothing happens.
func _get_configuration_warning() -> String:
    # We ensure that the pressure plate has a child node named "Door" and that
    # it is a StaticBody2D.
    var door_node := get_node_or_null("Door") as StaticBody2D
    if not door_node:
        return "This node needs a child node named Door, either an instance of DoorHorizontal or DoorVertical."
    return ""

In the next lesson, we’ll redesign the obstacle course level using the new mechanics we worked on: pickups and pressure doors.