Hi everyone,
I just pushed the first version of a signalling framework to the signals branch in my fork at GitHub. I'll issue a new pull request to the base repository when the build system changes have been merged because the signals depend on those changes.
I already talked briefly about signals in
my post over in the scripting thread (see section "Reacting to third-party changes"). Here, I'll try to describe signals and their intended purpose in a little more detail.
What's a signal?A signal is a way to broadcast information to interested receivers. They are a form of the observer pattern. A typical usage is to send notification of state changes in an object. Say we want to react to position changes of a movable entity. We could poll the position regularly, like this:
- Code:
-
while (interestedInPosition) {
if (entity.position != lastPosition) {
// Do something
}
lastPosition = entity.position;
}
Polling works, but it's not very good, performance-wise. If the entity moves sparingly, we end up doing nothing but comparing the positions over long stretches of time. A better way would be for the entity to tell us about position changes. That's what signals can be used for:
- Code:
-
void onPositionChanged(double x, double y, double z) {
// Do something
}
// ...
entity.sig_positionChanged.connect(onPositionChanged);
Each time its position changes, the entity takes care to emit the sig_positionChanged signal. The signal, in turn, invokes the onPositionChanged function. A function that is connected to a signal is sometimes called a "slot". Note that signals and slots have a many to many relation. A signal can be connected to any number of slots and a slot can be connected to any number of signals. This furthers code reuse and modularity.
How to use signals in Thrive (C++)To use the signals found in the above linked branch, include the header "signals/signal.h". All signal related classes can be found in the "thrive::signals" namespace. The following examples will assume a "using thrive::signals" declaration. In header files, please don't use the using declaration but rather fully qualify the name.
To define a signal, you need to tell it about the data types it's supposed to broadcast:
- Code:
-
Signal<double, double, double> sig_positionChanged;
The above signal will broadcast three doubles. Note that if you want to broadcast more complex data, like structs, you should broadcast (ideally const) references to avoid copying:
- Code:
-
Signal<const Component&> sig_componentAdded;
You can connect anything to a signal that is convertable to an std::function<Args>, where "Args" is the signature of the signal. Lambdas and the return value of std::bind are always good:
- Code:
-
auto onPositionChanged = [](double x, double y, double z) {
std::cout << "New x position: " << x << std::endl;
};
sig_positionChanged.connect(onPositionChanged);
The above would print out a message anytime the signal is emitted. The connect method returns a shared pointer to a connection object that allows you to sever the connection:
- Code:
-
std::shared_ptr<Connection> connection = sig_positionChanged.connect(onPositionChanged);
// Do stuff
connection->disconnect();