Home Creators Posts Import Register Favorites Logout

Content

In CE:ROM, a lot of things happen.

Blood bleeds, devices overheat, and [REDACTED] [REDACTED]s.

With so many moving parts, how can we keep track of everything such that Brooke knows when to berate you for jamming glass deeper into the patient?

It's actually pretty easy. Lemme tell you really quick about Signals and a global (gasp!) way of using them.

Signals

CE:ROM leverages oh-so-lovely Signals. You can think of signals as messages or events. Objects can emit, or 'shout', signals. Objects can also connect to, or 'listen for', signals. Using this system, you can have objects react in response to other objects doing stuff.

A big advantage signals have over, say, calling a function, is that the object broadcasting a signal doesn't have to care about who's listening. It simply emits the signal and lets whomever's listening do whatever in response.

Also, any number of objects can listen for a particular signal. They simply need to 'subscribe' to the signal to start listening.

This is great since it decouples objects from each other - we don't form a big mess of interconnected objects all trying to point at other objects.

An example

You know how in some games, if you get out of bounds, you might find yourself falling forever?

If something like that happened in CE:ROM, it would probably softlock the operation. Often times, the game waits for you to treat everything before it moves on, and obviously you can't really do that if a tumor moves off-screen, for example.

I made a reusable helper to catch this very circumstance, and it leverages a signal to work.

Pictured is Boundable, a component that I can attach to game objects. It emits a signal called on_boundaries_left when the object it's attached to leaves certain boundaries. (See how it's defined on line 3?)

It's very simple to actually emit a signal. We simply call .emit(), seen on line 6.

Let's take a look at a different object, one in which we want to react to the signal. Here, we connect to the signal we just talked about. We also define an anonymous function that will be called when the signal is emitted. In other words, the function will be called when this object goes out of bounds.

In this case, we'll just quietly delete the object in question. (Of course, we could do any number of things, such as teleporting back into bounds.) And that's it!

(Yes, I could have done just ...connect(queue_free). I'm being verbose for demonstrative purposes! Defining an anonymous function also makes it way easier if I decide I wanna do more than a single thing. So thank you, and quiet in the peanut gallery!)

Now, we could have done something like this instead and not have to deal with signals at all. But there are already huge downsides:

We're assuming the immediate parent is the object we want to free. (With the first method, the Boundable node could be practically anywhere, we just need to know about it.)

We've locked the functionality to be queue_free. (With the first method, 'what happens' when the signal is emitted is defined by the object who's listening instead of the object who's broadcasting.)

We've obfuscated where this function is being called from. The object is being freed from somewhere, making it a lot harder to keep track of everything.

I hope you now see why signals can be super great! But, like with most things in life, there is a downside. Did you notice it?

We need to know about the object who's doing the broadcasting, as we need to connect to its signal.

In the example, we connected to $Boundable, which was simple as it is just a direct child. But what if we might not know where the thing we're listening to is?

It might sound like an edge case at first, but consider there's a lot of scripts in a game which could also be compartmentalized and put in all sorts of different places. Sounds like a massive pain to have to figure out 'how to get' to a certain object. Especially if said object is programmatically spawned in! How do we fix that?

The Signal Bus

So here's another example. In 1-1, there are several pieces of glass you need to remove. If you mess up enough times, a Directive will fail (and Brooke will berate you).

Yes, it's the same picture.

To get this to work, the game needs to know when a piece of glass was messed up. But we don't care which specific piece of glass was messed up, just that any glass was messed up. We also would like to avoid having to specifically subscribe to every piece of glass - that's a lot of work and we might accidentally forget to listen to some of them!

Directly connecting to each glass object sounds like a nightmare. But thankfully, we can use a global class to pass around our signals to everyone.

A global class (a singleton, or an autoload in Godot, don't worry about the specifics too much) can be referenced by anyone, and only one copy can exist at a time. It's a pretty powerful tool (and one that's exceedingly easy to abuse).

One really useful use of a global is to simply define signals there.

It's nothing complicated. This signal doesn't even do anything, nor does it pass any data along. The signal essentially just has a name. First step done.

Next, we emit the signal from somewhere. In this example, that 'somewhere' would be in code relevant to handling the glass ("Dragger").

Note how here, we have to specify Events.draggable_shoved.emit(). That's because our SignalBus object had to be given a unique name in Godot's project settings, which I decided to be Events. In other words, the Events object will refer to the single, global instance of our SignalBus, and we'll be able to access it from anywhere using "Events.".

Still with me? Just one more step left.

Over in a different object (a script controlling operation 1-1), we just have to listen for the draggable_shoved signal and define what we'll do. (Here, we're handling the "≤3 extraction mistakes" Directive.)

And that's it! We didn't have to hunt down specific pieces of glass, and it'll work no matter if we have 1 or 100 pieces. Super simple!

That's just one use case. CE:ROM currently has nearly 300 signals on a signal bus. That might sound like a lot, but they're actually used relatively sparingly, mostly in cases where trying to connect two specific objects together (meaning we'd have to know the node path from one place to the other) would be really annoying.

They're also named pretty sensibly, making it actually quite easy to keep track of.

You might have also noticed signals can pass parameters. It works as you'd expect. We can pass around practically anything we need.

Getting characters to say specific things in response to what you're doing is super easy using the signal bus. Just define a signal, have one thing emit it, and have the other listen. Done!

Used correctly, a global signal bus / event handler can make things a breeze.

Just don't abuse it by piling all of your signals on it. We wouldn't want your code to have too much spaghetti, would we?

Files

Comments

DeadPixels

Darn it its to bad i can’t read!

DeadPixels

I always found it cool when you know what you’re doing on a second play-through so you go a little out of sequence and Brooke is like ay i saw that shi. Or when i panic and pull out a completely unrelated tool and spark says don’t pull out the tablet you doofus. Lol! Love to see how that works here!