Looking for a better way to connect in-game buttons/levers to doors

Godot Version

4.4.rc3

Question

I have this very simple game in which the green character has to get to the pink character. I’d like to make it so that the door opens when you push the box over the button, and to do that is fairly simple for this one room if i just check a variable of the button in the door and just open it when its a certain value…

…but what do i do with other doors and buttons? What if I want to connect 3 buttons to a door, or one button to 2 doors, or what if I want one button to close one door and open another?

I was imagining having a literal connector attached to each button/door via a draw line and making it so that when connectors were overlapping then those buttons and doors would interact with each other. Is there some way of doing something like this with a tool script or something?

Script for the Door

extends StaticBody2D
var state = “closed”
@onready var animated_sprite_2d = %AnimatedSprite2D
@onready var collision_shape_2d = $CollisionShape2D

func _physics_process(delta):
if state == “closed”:
animated_sprite_2d.play(“default”)
collision_shape_2d.disabled = false
elif state == “open”:
animated_sprite_2d.play(“open”)
collision_shape_2d.disabled = true

Script for the Button

extends Area2D

@onready var character_body_2d = %CharacterBody2D
var state = 0

func _on_body_entered(_body):
state = 1
print(state)

func _on_body_exited(_body):
state = 0
print(state)

1 Like

You could give the button a class_name and use an @exported dictionary in your doors that have the buttons and in which state it should be. For example:

Your button:

class_name GameButton extends Area2D

signal changed()

var is_pressed:bool = false

func _on_body_entered(_body):
    is_pressed = true
    changed.dispatch()

func _on_body_exited(_body):
    is_pressed = false
    changed.dispatch()

The door:

extends StaticBody2D


@export var open_with_buttons:Dictionary[GameButton, bool] = {}


func _ready() -> void:
    for button in open_with_buttons:
        button.changed.connect(_check_if_open)
    
    
func _check_if_open() -> void:
	var open = false
	for button in open_with_buttons:
		
		var should_be_pressed = open_with_buttons.get(button)
		var is_pressed = button.is_pressed
		
		if is_pressed and should_be_pressed:
            # so far, we can open the door. Check the other doors
			open = true
		elif is_pressed and not should_be_pressed:
            # whoops, the button is pressed but it shouldn't be
			open = false
			# The door can't be opened so no need to continue checking
			break

    state = "open" if open else "closed"
    

# rest of the script

This way, from the inspector you can fill that dictionary with which buttons and in which state they should be to open the door.

As for being able to visualize it in the editor, it’s possible by using @tool scripts. If you want more functionality then you’d need to create a plugin

For example, to visualize it you could modify the door script like:

@tool
extends StaticBody2D


@export var open_with_buttons:Dictionary[GameButton, bool] = {}:
	set(value):
		open_with_buttons = value
		queue_redraw()

		
# ...
    

func _draw() -> void:
	if not Engine.is_editor_hint():
		# Not running in the editor, don't draw the debug lines!
		return

	# Draw a line to each button
	for button in open_with_buttons:
		var should_be_pressed = open_with_buttons.get(button)
		var color = Color.GREEN if should_be_pressed else Color.RED
		draw_line(Vector2.ZERO, to_local(button.global_position), color, 2, true)

(the script is pretty bare-bones, you’ll need to add extra logic to update it when moving a button for example)

Result:

2 Likes

This seems like an amazing idea! Only issue I’m having is in the button script. It says:

Cannot find member “dispatch” in base “signal”

Is it possible dispatch is an outdated command or it’s been renamed? There’s no information on it in Godot Wiki as far as I can find.

Try emit() instead of dispatch :slightly_smiling_face:

1 Like

Using emit() gets rid of the error in the code but nothing seems to happen when I step over the button. print(changed.get_connections()) on the button shows that the door is connected, but stepping on or off the button does not change the state of the door.

This seems like an amazing idea! Only issue I’m having is in the button script. It says:

Cannot find member “dispatch” in base “signal”

Is it possible dispatch is an outdated command or it’s been renamed? There’s no information on it in Godot Wiki as far as I can find.

Yep, sorry, it’s emit() :sweat_smile: I mixed it up with another signal implementation on another language.

Using emit() gets rid of the error in the code but nothing seems to happen when I step over the button. print(changed.get_connections()) on the button shows that the door is connected, but stepping on or off the button does not change the state of the door.

Uhm, not sure. I didn’t really test the code so it may have some bugs. You could try debugging, add a breakpoint in the _check_if_open() function, step on it and, see if it gets triggered when stepping on a button.

1 Like

New code for the Door

extends StaticBody2D

var state = “closed”
@onready var animated_sprite_2d = %AnimatedSprite2D
@onready var collision_shape_2d = $CollisionShape2D
@export var open_with_buttons:Dictionary[GameButton, bool] = {}

func _ready() → void:
for button in open_with_buttons:
button.changed.connect(_check_if_open)

func _check_if_open() → void:
var open = false
for button in open_with_buttons:

	var should_be_pressed = open_with_buttons.get(button)
	var is_pressed = button.is_pressed
	
	if is_pressed and should_be_pressed:
		# so far, we can open the door. Check the other doors
		open = true
	elif is_pressed and not should_be_pressed:
		# whoops, the button is pressed but it shouldn't be
		open = false
		# The door can't be opened so no need to continue checking
		break

state = "open" if open else "closed"

func _physics_process(_delta):
if state == “closed”:
animated_sprite_2d.play(“default”)
collision_shape_2d.disabled = false
elif state == “open”:
animated_sprite_2d.play(“open”)
collision_shape_2d.disabled = true

New code for the button

class_name GameButton extends Area2D

signal changed()

var is_pressed:bool = false

func _on_body_entered(_body):
print(“OnButton”)
is_pressed = true
changed.emit()

func _on_body_exited(_body):
print(“OffButton”)
is_pressed = false
changed.emit()

I have made sure that all of my objects are on the correct collision layers. The button does print a message when I step on it, so I know that part is working. The signal.emit just seems to do nothing and I cannot figure out why.

Also I apologize for the formatting. I have no idea how it works.

Scratch that, I figured it out. I thought unchecking the box next to the button assignment would invert the signal, but it just disables it. It works, but how would I invert the state of the door if I needed to? Like, if I wanted the buttons connected to it to close the door?

This has worked smoothly for the most part, but I’d like to be able to have it so that when two or more buttons are pressed, then and only then will the door open. So far it will open as soon as i press down any one of the buttons

Maybe you found a way in the meantime, but: Instead of directly setting the door to be open in the _check_if_open() method, you could add a counter to count the number of open doors. If this is equal or greater than a min_doors_to_open, you set the open to true.

I did figure it out and while it may not be the best solution, I just changed the lines

if is_pressed and should_be_pressed:

elif is_pressed and not should_be_pressed:

to

if is_pressed and should_be_pressed or not is_pressed and not should_be_pressed:

elif is_pressed and not should_be_pressed or not is_pressed and should_be_pressed: