I am struggling to understand custom signals

Godot Version

4.6.stable

Question

I have had an amazing time working in Godot so far, but despite multiple videos, reading over the documents over and over, and attempts at writing code for it, I cannot wrap my head around how custom signals work.

I understand the pieces of it:


#This creates a new signal
signal signal_name

#and I know something like this would send the signal out to other nodes
func _process()
    signal_name.emit()

But what I’m really struggling to figure out is how a different node with a completely different script can read this signal. I understand it involves using connect() in some way?

I know there’s the thing in the inspector to connect signals to other nodes but I’d rather avoid putting in code that breaks if a node happens to be deleted later.

As of right now I have a top-down game where you can push different types of boxes around. It’s pretty much just that as of right now.

What I want is when certain boxes are pushed, different things happen. Like, one that’s a bomb and explodes after being pushed, or one that crumbles to pieces after being pushed for a certain amount of time.

Basically, I want the player to send a signal to the box when the player is pushing it, which I feel like should be way simpler than I’m making it out to be.

Below is my current player script.

#The player script (in case that wasn't obvious)

extends CharacterBody2D

@export var speed = 64
@export var player_animated_sprite : AnimatedSprite2D
var outer_force := Vector2.ZERO
const PUSH_FORCE := 16
const MIN_PUSH_FORCE := 8

func _physics_process(_delta):
	var direction = Input.get_vector("left", "right", "up", "down")
	print(direction)
	velocity = (direction * speed) + outer_force
	if velocity.x > 0:
		player_animated_sprite.play("right")
	if velocity.x < 0:
		player_animated_sprite.play("left")
	if velocity.y > 0:
		player_animated_sprite.play("down")
	if velocity.y < 0:
		player_animated_sprite.play("up")
	move_and_slide()

#this is what detects and allows the player to push boxes
	for i in get_slide_collision_count():
		var c = get_slide_collision(i)
		if c.get_collider() is RigidBody2D and c.get_collider().is_in_group("box"):
			var push_force = (PUSH_FORCE * velocity.length() / speed)
			c.get_collider().apply_central_impulse(-c.get_normal() * push_force)
#I know c.get_collider() is the box node so I feel like I could send the signal here somehow

And this is my current script for the boxes. It feels like much more of a mess but I assume I would put my connect into some kind of function here, but how do I get the script to know the signal?

extends RigidBody2D

var previous_velocity : Vector2 = Vector2.ZERO
var snap_timer := 0.1
@export var sprite_box : AnimatedSprite2D
@export var sprite_collision : CollisionShape2D
enum Type { WOODEN, METAL, ICE, BOUNCE, STONE, BOMB }
@export var box_type : Type

func _ready():

#I know this is ugly and I will probably switch to making a box class for all the different types once I understand classes a bit better
	match box_type:
		Type.WOODEN:
			physics_material_override.bounce = 0
			physics_material_override.friction = 0.8
			physics_material_override.absorbent = true
			physics_material_override.rough = true
			mass = 2.0
			linear_damp = 16
			sprite_box.play("wooden")
		Type.METAL:
			physics_material_override.bounce = 0
			physics_material_override.friction = 1
			physics_material_override.absorbent = true
			physics_material_override.rough = true
			mass = 4.0
			linear_damp = 16
			sprite_box.play("metal")
		Type.ICE:
			physics_material_override.bounce = 0
			physics_material_override.friction = 0
			physics_material_override.absorbent = true
			physics_material_override.rough = false
			mass = 0.5
			linear_damp = 0.0
			sprite_box.play("ice")
		Type.BOUNCE:
			physics_material_override.bounce = 1
			physics_material_override.friction = 0
			physics_material_override.absorbent = false
			physics_material_override.rough = false
			mass = 1.0
			linear_damp = 0.05
			sprite_box.play("bubble")
		Type.STONE:
			physics_material_override.bounce = 0
			physics_material_override.friction = 1
			physics_material_override.absorbent = false
			physics_material_override.rough = true
			mass = 15.0
			linear_damp = 16
			sprite_box.play("stone")
		Type.BOMB:
			physics_material_override.bounce = 0
			physics_material_override.friction = 0.8
			physics_material_override.absorbent = true
			physics_material_override.rough = true
			mass = 1.0
			linear_damp = 16
			sprite_box.play("bomb")
			contact_monitor = true
			max_contacts_reported = 4

func _physics_process(delta):
	
#this is just meant to snap the block into a grid if it isn't moving for some time. I would happily take advice on how to clean it up if have any.
	if sleeping:
		global_position = snapped(global_position,Vector2(2,2))
		if snap_timer <= 0 and linear_velocity != Vector2.ZERO:
			linear_velocity = Vector2.ZERO
			snap_timer = 0.1
		else:
			snap_timer -= delta
	else:
		snap_timer = 0.1

#I think here I would have some kind of function which activates once the box is being pushed? But I don't quite understand where or how I should put connect() into it.


It needs a reference to the node that declared the signal and then just access the signal using the dot syntax: reference_to_node.signal.connect(_signal_handling_function)

1 Like

You probably do not want a signal. You already have a direct reference to the box so any function you wish to call can be done directly on c.get_collider(). Signals are better meant for something that announces to the world it has done an action and anyone listening may react, not where you have a player acting on something specific and only that specific box should react.

If your boxes are of the same type you can declare a class_name for them to inherit and expand on the “push reaction” function you want.

extends RigidBody2D
class_name PushableBox

var previous_velocity : Vector2 = Vector2.ZERO
var snap_timer := 0.1
# etc...

func push_reaction():
	pass # does nothing for now...

Then any box-specific code can override this function in an extended script (you may right-click on the box node and select “extend script”)

extends PusableBox

func push_reaction():
	print("I am going to explode!!!")

This also reduces your collision detection to one is condition

for i in get_slide_collision_count():
	var c := get_slide_collision(i)
	var collider := c.get_collider()
	if collider is PushableBox:
		var push_force = (PUSH_FORCE * velocity.length() / speed)
		collider.apply_central_impulse(-c.get_normal() * push_force)
		collider.push_reaction()
3 Likes

This is pretty much what I needed for this! I’m still not quite sure where I should and shouldn’t be using signals but the way you’ve done things here makes more sense.

I’m still a bit fuzzy on the whole subject but I expect signals are more for when there is not a direct connection (like collision) between the two objects such as a door and a button that opens it.

1 Like

Great example, the button can emit that it’s been pressed and any door(s) can connect to it, so could a light and boom you have a light-switch-button; that’s the power of signals

1 Like

I think I understand to some degree but as for where or how to get the reference to the other node or where to put this within code I’m not quite there yet but I’ll likely leave figuring this out for a future question as I’ve been given another solution for this problem.

If you have trouble finding out how to get a reference to some other node, global signals can be the answer. Make an autoloaded events.gd named as Events like this:

extends Node
signal settings_changed()

You can now send the signal from anywhere like this

Events.settings_changed.emit()

And you can connect to the signal from anywhere like this:

Events.settings_changed.connect(my_func)

If your signal sender can easily get a reference to the target, a direct function call is usually the correct choice. For example, a parent node can easily call a child node’s functions. So use functions.

But it is awkward for a child node to call the parent node’s functions. Since the parent can easily call the child’s functions, it can also easily connect a signal from the child node to itself, making it simple for the child node to make a “function call“ to the parent just by sending a signal.