DEV Community

Cover image for SignalBus?
Jeferson 'Shin' Leite Borges
Jeferson 'Shin' Leite Borges

Posted on

SignalBus?

Ok, I'll try to write this post in both portuguese and english.

English

Let's go!

What is a SignalBus and why all this noise in my head when I think about this pattern and more importantly, how to implement it in Godot?

So what's the concept on this one?

Imagine the situation, you made your UI all beautiful and wonderful, you have your level all built up, you even have points to spawn the character throughout the level. Everything is perfect, then you need to connect the player and his life points with the UI.

The initial thought comes to mind, "This is a job for signals!", so at your level you locate the player's node and then connect it to UI. Everything works, but now you have a code snippet somewhere (level, player, or UI) that you need to know everyone's details and organize.

So this code is entangled.

For small games, this is not a problem, and particularly, until well advanced in my development I didn't experience any problems, and the solution seemed to me even elegant.

But I've this itch in my head, this noise in my brain, this might be better, but how?

It was then that I heard the term SignalBus. Now we have a problem and we're going to solve the problem a little more elegantly, and then we're going to think about the implications of that.

In this case we have a Singleton, which for the engine is done just by creating a script (which we will call SignalBus.gd and add the list of auto-loads, that way the script will always be running and it can be called in any other script.

Let's then add something very interesting to our friend signal bus, for example:

#SignalBus.gd
extends Node2D

signal PlayerDamaged(total_hp, current_hp)
Enter fullscreen mode Exit fullscreen mode

Pretty short, right?
But what about our player friend?

#Player.gd
class_name Player extends KinematicBody2D
#...
func receive_damage(damage):
  #...
  SignalBus.emit("PlayerDamaged", max_hp, current_hp)
  #...
#...
Enter fullscreen mode Exit fullscreen mode

As you can see, the player class only knows about SignalBus, but it doesn't know the UI, it doesn't know the level, and honestly, it doesn't need to. The level also no longer needs to know the player, let alone the UI.

Now let's see the UI class,

#UI.gd
class_name UI extends Control
#...
func _ready(damage):
  #...
  SignalBus.connect("PlayerDamaged", self, "on_update_player_life")
  #...
func on_update_player_life(max, curr):
  # Update ui with actual values
#...
Enter fullscreen mode Exit fullscreen mode

Now we have a code that is only coupled to SignalBus, with that we can move things to other places and even put more things listening to the player signal.

Imagine that we now have an enemy that waits to hear the player take damage and then shoots? Or we need to put a new animation of shaking the screen when the player takes damage, all this can be done by the same signal that is controlled by our friend bus.

Português

Vamos lá,

O que é um SignalBus e porque todo esse barulho em minha cabeça quando penso nesse padrão e o mais importante, como implementar ele em Godot?

Qual a idea por trás?

Imagine a situação, você fez sua UI toda linda e maravilhosa, você tem seu nível todo construido, você tem até pontos para nascer o personagem ao longo da fase. Tudo é perfeito, aí você precisa conectar o jogador e seus pontos de vida com a UI.

Em seu pensamento inicial vem a mente, "Isso é um trabalho para sinais!", então na sua nível você localiza o do jogador e então conecta junto a UI. Tudo funciona, mas agora você tem um trecho de código em algum lugar (nível, jogador ou UI) que precisa saber dos detalhes de todo mundo e organizar.

Ou seja, acoplamento de código.

Para jogos pequenos, isso não é um problema, e particularmente, até bem avançado no meu desenvolvimento eu não senti problemas, e a solução me pareceu até mesmo elegante.

Mas eu tinha essa pulga atrás da orelha pensando, isso pode ser melhor, mas como?

Foi então que ouvi o termo SignalBus. Agora temos um problema e iremos a solução do problema um pouco mais elegante, e depois vamos pensar nas implicações disso.

Nesse caso temos um Singleton, que para a engine é feito apenas por criar um script (que vamos chamar de SignalBus.gd e adicionar a lista de auto-loads, dessa forma o script vai estar sempre em execução e se pode ser chamado em qualquer outro script.

Vamos então adicionar algo bem interessante no nosso amigo busão de sinais, segue o exemplo:

#SignalBus.gd
extends Node2D

signal PlayerDamaged(total_hp, current_hp)
Enter fullscreen mode Exit fullscreen mode

Bem curtinho, né?
Mas e o nosso amigo jogador?

#Player.gd
class_name Player extends KinematicBody2D
#...
func receive_damage(damage):
  #...
  SignalBus.emit("PlayerDamaged", max_hp, current_hp)
  #...
#...
Enter fullscreen mode Exit fullscreen mode

Como se pode ver, a classe do jogador conhece apenas do SignalBus, mas não conhece o UI, nem conhece o nível, e sinceramente, não precisa. O nível também não precisa mais conhecer o jogador, e muito menos o UI.

Agora vmaos a classe do UI,

#UI.gd
class_name UI extends Control
#...
func _ready(damage):
  #...
  SignalBus.connect("PlayerDamaged", self, "on_update_player_life")
  #...
func on_update_player_life(max, curr):
  # Update ui with actual values
#...
Enter fullscreen mode Exit fullscreen mode

Agora temos um código que está acoplado apenas no SignalBus, com isso podemos mover coisas para outros lugares e inclusive colocar mais coisas ouvindo o sinal do player.

Imagine que agora temos um inimigo que espera ouvir o jogador receber dano para então atirar? Ou precisamos colocar uma nova animação de tremer a tela quando o jogador receber dano, tudo isso pode ser feito pelo mesmo sinal que é controlado pelo nosso amigo bus.

Espero que tenha gostado desse trecho.

(()=>())()

Oldest comments (0)