Anton Tarasenko
3 years ago
2 changed files with 393 additions and 1 deletions
@ -0,0 +1,392 @@ |
|||||||
|
# Signals and slots system |
||||||
|
|
||||||
|
Acedia provides its own unique event system that is more powerful than regular |
||||||
|
`delegate`s and easier to use than listener-type objects like `GameRules`. |
||||||
|
It's best demonstrated with an example from AcediaFixes' feature |
||||||
|
that deals with friendly fire-related exploits. |
||||||
|
Said feature has to catch and handle `NetDamage()` event from `GameRules` and, |
||||||
|
with Acedia's signal/slot system, handler can be added with a single line: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
_.unreal.gameRules.OnNetDamage(self).connect = NetDamageHandler; |
||||||
|
``` |
||||||
|
|
||||||
|
`OnNetDamage()` is a *signal function* responsible for adding new handlers |
||||||
|
for the `NetDamage` event. |
||||||
|
Function `NetDamageHandler` has to have an appropriate signature |
||||||
|
(parameters and return value types) for the event and will be called each time |
||||||
|
`NetDamage` event occurs, starting from the next occurrence. |
||||||
|
|
||||||
|
Unlike raw `delegate`s, that can each only store one function, signal/slot |
||||||
|
system allows us to have however many handlers we need: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
// All of those will be called on `NetDamage` event |
||||||
|
_.unreal.gameRules.OnNetDamage(self).connect = NetDamageNext; |
||||||
|
_.unreal.gameRules.OnNetDamage(self).connect = NetDamageTry; |
||||||
|
_.unreal.gameRules.OnNetDamage(self).connect = NetDamageRevolution; |
||||||
|
_.unreal.gameRules.OnNetDamage(self).connect = NetDamageEvolutionR; |
||||||
|
``` |
||||||
|
|
||||||
|
`self` argument is necessary for the cleanup and refers to the object that is |
||||||
|
responsible for the added handler - if it gets deallocated, then all of |
||||||
|
the associated handlers will be automatically removed: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
// This assumes that `someObj` is a child class of `AcediaObject` |
||||||
|
_.unreal.gameRules.OnNetDamage(someObj).connect = NetDamageHandler; |
||||||
|
_.memory.Free(someObj); // After this line `NetDamageHandler()` won't be used |
||||||
|
``` |
||||||
|
|
||||||
|
Most of the time this parameter is going to be `self`, since normally each |
||||||
|
object adds its own functions as event handlers. |
||||||
|
|
||||||
|
Once you no longer need to handle an event, you can *disconnect* from it: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
_.unreal.gameRules.OnNetDamage(self).Disconnect(); |
||||||
|
// To disconnect some object `someObj`: |
||||||
|
_.unreal.gameRules.OnNetDamage(someObj).Disconnect(); |
||||||
|
``` |
||||||
|
|
||||||
|
This will remove all handlers associated with the object passed as |
||||||
|
an argument. |
||||||
|
|
||||||
|
> **NOTE:** |
||||||
|
> Even though handlers associated with deallocated object will be automatically |
||||||
|
> removed, it is still good practice to manually remove all handlers associated |
||||||
|
> with it inside a finalizer, since otherwise "dead" handlers will be stored |
||||||
|
> until related event is triggered. |
||||||
|
|
||||||
|
Removing individual handlers is also possible, but can be a bit cumbersome and |
||||||
|
is not recommended. |
||||||
|
|
||||||
|
> **NOTE:** |
||||||
|
> Our signals and slots take this moniker after [Qt](https://doc.qt.io)'s |
||||||
|
> events system; |
||||||
|
> however, what they are and how they work is different and shouldn't |
||||||
|
> be mixed up. |
||||||
|
|
||||||
|
## [Advanced] How signals and slots work |
||||||
|
|
||||||
|
### Delegates overview |
||||||
|
|
||||||
|
This system was created because of limitations of `delegate`s in UnrealEngine 2. |
||||||
|
One can declare `delegate` inside any class: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MyClass extends Object; |
||||||
|
|
||||||
|
delegate MyDelegate() |
||||||
|
{ |
||||||
|
// This code will be executed if no function is assigned to the delegate. |
||||||
|
Log("Empty message"); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
then assign a function to them and any time a delegate is called - an assigned |
||||||
|
function will be called instead: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
function handler() |
||||||
|
{ |
||||||
|
Log("Handler is called!"); |
||||||
|
} |
||||||
|
|
||||||
|
local MyClass obj; |
||||||
|
obj = new class'MyClass'; |
||||||
|
obj.MyDelegate(); // "Empty message" is logged |
||||||
|
obj.MyDelegate = handler; |
||||||
|
obj.MyDelegate(); // "Handler is called!" is logged |
||||||
|
obj.MyDelegate = none; // Reset delegate to its default state |
||||||
|
obj.MyDelegate(); // "Empty message" is logged |
||||||
|
``` |
||||||
|
|
||||||
|
However they have their limitations, main one being that you can neither assign |
||||||
|
several functions to a `delegate` nor can you create an array of `delegate`s to |
||||||
|
have several handlers for your events. |
||||||
|
|
||||||
|
### `Slot`s are boxed `delegate`s, `Signal`s are arrays of `Slot`s |
||||||
|
|
||||||
|
Acedia bypasses this limitation by essentially boxing `delegate`s. |
||||||
|
If you are unfamiliar with the concept of boxing, it is discussed |
||||||
|
[here](./objects.md). |
||||||
|
`Slot` is just an object that contains a single `delegate` (usually called |
||||||
|
`connect()`) with some extra code to support cleanup of no longer needed |
||||||
|
`Slot`s. |
||||||
|
Wrapping `delegate`s into `Slot`s allows us to store them in arrays represented |
||||||
|
by `Signal`s: each `Signal` is usually associated with some sort of an event and |
||||||
|
can refer (be connected to) several `Slot`s, therefore supporting several |
||||||
|
different handlers for its event. |
||||||
|
|
||||||
|
`Signal`s are usually declared as internal variables and are different from |
||||||
|
*signal function* like `_.unreal.gameRules.OnNetDamage()`. |
||||||
|
Connecting a handler to an event with line like |
||||||
|
`OnNetDamage(self).connect = NetDamage` |
||||||
|
actually results in performing following steps: |
||||||
|
|
||||||
|
1. Appropriate `Signal` object is found / accessed; |
||||||
|
2. New `Slot` object for that `Singal` is created and returned; |
||||||
|
3. `connect` delegate for returned `Slot` gets assigned with a handler function |
||||||
|
(`NetDamage` in above example). |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
// [GameRulesAPI.uc] |
||||||
|
public final function GameRules_OnNetDamage_Slot OnNetDamage( |
||||||
|
AcediaObject receiver) |
||||||
|
{ |
||||||
|
local Signal signal; |
||||||
|
local UnrealService service; |
||||||
|
// These two lines are implementation detail for `OnNetDamage()`, |
||||||
|
// you can store your `signal` object wherever you need. |
||||||
|
service = UnrealService(class'UnrealService'.static.Require()); |
||||||
|
signal = service.GetSignal(class'GameRules_OnNetDamage_Signal'); |
||||||
|
// This is the important line that creates new slot |
||||||
|
return GameRules_OnNetDamage_Slot(signal.NewSlot(receiver)); |
||||||
|
} |
||||||
|
|
||||||
|
// [FixFFHack.uc] |
||||||
|
// `connect` is simply a delegate defined inside |
||||||
|
// `GameRules_OnNetDamage_Slot` object |
||||||
|
_.unreal.gameRules.OnNetDamage(self).connect = NetDamage; |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
If returned signal is not assigned any function, then it will be automatically |
||||||
|
cleaned up at some later point. |
||||||
|
We cannot directly check whether a `delegate` was assigned some value, but |
||||||
|
`connect`'s default implementation calls special protected method `DummyCall()` |
||||||
|
that tells us that its slot is empty. |
||||||
|
|
||||||
|
### Disconnecting |
||||||
|
|
||||||
|
So how does `_.unreal.gameRules.OnNetDamage(someObj).Disconnect()` work? |
||||||
|
Same as above, `_.unreal.gameRules.OnNetDamage(someObj)` creates a new empty |
||||||
|
slot associated with `someObj`. |
||||||
|
This slot is aware of both `signal` its connected to and associated `someObj`. |
||||||
|
`Disconnect()` method makes our `slot` inform its `signal` that all `slot`s |
||||||
|
related to `someObj` (including itself) must be disconnected and deallocated. |
||||||
|
|
||||||
|
This means that disconnecting object's `slot`s from the `signal` with |
||||||
|
`Disconnect()`always involves creation of the new `slot` that will never be |
||||||
|
connected to any handler method. |
||||||
|
It is a roundabout way of doing things, but it provides a simple interface for |
||||||
|
the task that shouldn't be performed often enough to affect performance. |
||||||
|
|
||||||
|
## [Advanced] How to make your own `signal`s and `slot`s |
||||||
|
|
||||||
|
Providing support for your own signals and slots actually takes quite a bit more |
||||||
|
work that using them. |
||||||
|
Here we will consider main use cases. |
||||||
|
|
||||||
|
### Simple notification events |
||||||
|
|
||||||
|
If you need to add an event with handlers that don't take any parameters and |
||||||
|
don't return anything, then easiest way it to use `SimpleSignal` / `SingleSlot` |
||||||
|
classes: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MyEventClass extends AcediaObject; |
||||||
|
|
||||||
|
var private SimpleSignal onMyEventSignal; |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
onMyEventSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
_.memory.Free(onMyEventSignal); |
||||||
|
onMyEventSignal = none; |
||||||
|
} |
||||||
|
|
||||||
|
public function SimpleSlot OnMyEvent(AcediaObject receiver) |
||||||
|
{ |
||||||
|
return SimpleSlot(onMyEventSignal.NewSlot(receiver)); |
||||||
|
} |
||||||
|
|
||||||
|
// Suppose you want to emit the signal when this function is called... |
||||||
|
public function SimpleSlot FireOffMyEvent(AcediaObject receiver) |
||||||
|
{ |
||||||
|
// ...simply call this and all the slots will have their handlers called |
||||||
|
onMyEventSignal.Emit(); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Then you can use `OnMyEvent()` as a *signal function*: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
// To add handlers |
||||||
|
myEventClassInstance.OnMyEvent(self).connect = handler; |
||||||
|
// To remove handlers |
||||||
|
myEventClassInstance.OnMyEvent(self).Disconnect(); |
||||||
|
``` |
||||||
|
|
||||||
|
### Events with parameters |
||||||
|
|
||||||
|
Some of the events, like `OnNetDamage()` in our first examples, can take |
||||||
|
parameters. |
||||||
|
We cannot use `SimpleSignal` or `SimpleSlot` for them and have to define new |
||||||
|
classes that will wrap around `delegate` with an appropriate signature. |
||||||
|
|
||||||
|
You simply need to follow the template and define new classes like this: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MySignal extends Signal; |
||||||
|
|
||||||
|
public final function Emit(<PARAMETERS>) |
||||||
|
{ |
||||||
|
local Slot nextSlot; |
||||||
|
StartIterating(); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
while (nextSlot != none) |
||||||
|
{ |
||||||
|
MySlot(nextSlot).connect(<PARAMETERS>); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
} |
||||||
|
CleanEmptySlots(); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedSlotClass = class'MySlot' |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MySlot extends Slot; |
||||||
|
|
||||||
|
delegate connect(<PARAMETERS>) |
||||||
|
{ |
||||||
|
DummyCall(); // This allows Acedia to cleanup slots without set handlers |
||||||
|
} |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
super.Finalizer(); |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
where you can use any set of parameters instead of `<PARAMETERS>`. |
||||||
|
You can check out `Unreal_OnTick_Signal` and `Unreal_OnTick_Slot` as |
||||||
|
an example. |
||||||
|
|
||||||
|
### Events with return values |
||||||
|
|
||||||
|
Sometimes you want your handlers to respond in some way to the event. |
||||||
|
You can either allow them to modify input parameters (e.g. by declaring them as |
||||||
|
`out`) or allow them to have return value. |
||||||
|
`OnNetDamage()`, for example, is allowed to modify incoming damage by returning |
||||||
|
a new value. |
||||||
|
|
||||||
|
To add signals / slots that handle return value use following templates: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MySignal extends Signal; |
||||||
|
|
||||||
|
public final function <RETURN_TYPE> Emit(<PARAMETERS>) |
||||||
|
{ |
||||||
|
local <RETURN_TYPE> newValue; |
||||||
|
local Slot nextSlot; |
||||||
|
StartIterating(); |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
while (nextSlot != none) |
||||||
|
{ |
||||||
|
newValue = <SLOT_CLASS>(nextSlot).connect(<PARAMETERS>); |
||||||
|
// This check if necessary before using returned value |
||||||
|
if (!nextSlot.IsEmpty()) |
||||||
|
{ |
||||||
|
// Now handle `newValue` however you see fit |
||||||
|
} |
||||||
|
nextSlot = GetNextSlot(); |
||||||
|
} |
||||||
|
CleanEmptySlots(); |
||||||
|
// Return whatever you see fit after handling all the slots |
||||||
|
return <END_RETURN_VALUE>; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
relatedSlotClass = class'MySlot' |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
class MySlot extends Slot; |
||||||
|
|
||||||
|
delegate <RETURN_TYPE> connect(<PARAMETERS>) |
||||||
|
{ |
||||||
|
DummyCall(); |
||||||
|
// Return anything you want: |
||||||
|
// this value will be filtered inside corresponding `Signal` |
||||||
|
// if no handler is set to the associated slot |
||||||
|
return <???>; |
||||||
|
} |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
super.Finalizer(); |
||||||
|
connect = none; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
For working example you can check out `GameRules_OnNetDamage_Signal` and |
||||||
|
`GameRules_OnNetDamage_Slot` classes. |
||||||
|
|
||||||
|
## [Advanced] How to remove particular `slot` |
||||||
|
|
||||||
|
In our very first example we've seen that we can remove all `slot`s for |
||||||
|
`OnNetDamage()`, associated with `someObj` by calling |
||||||
|
`_.unreal.gameRules.OnNetDamage(someObj).Disconnect()`. |
||||||
|
But sometimes it might be necessary to remove only one slot. |
||||||
|
In that case you'll have to store that slot in a separate variable: |
||||||
|
|
||||||
|
```unrealscript |
||||||
|
// Each `signal` can have its own `slot` type |
||||||
|
var GameRules_OnNetDamage_Slot trackedSlot; |
||||||
|
var int trackedSlotLifeVersion; |
||||||
|
// ... |
||||||
|
// Record new `slot` in a variable, then set `connect` delegate |
||||||
|
trackedSlot = _.unreal.gameRules.OnNetDamage(self); |
||||||
|
trackedSlot.connect = handler; |
||||||
|
trackedSlotLifeVersion = trackedSlot.GetLifeVersion(); |
||||||
|
// ... |
||||||
|
// 1. You can then change `slot`'s handler |
||||||
|
if (trackedSlotLifeVersion == trackedSlot.GetLifeVersion()) { |
||||||
|
trackedSlot.connect = handler2; |
||||||
|
} |
||||||
|
// ... |
||||||
|
// 2. Or deallocate `slot` once it's no longer needed - |
||||||
|
// its handler won't be called again |
||||||
|
trackedSlot.FreeSelf(trackedSlotLifeVersion); |
||||||
|
trackedSlot = none; |
||||||
|
``` |
||||||
|
|
||||||
|
Here we record *life version* because it is `signal` and not us that is |
||||||
|
responsible for the deallocation of `slot`s. |
||||||
|
If we don't check life versions, we might use `slot` reallocated for a different |
||||||
|
purpose. |
||||||
|
This shouldn't happen unless you deallocate `trackedSlot` in some other way |
||||||
|
(e.g. with `_.unreal.gameRules.OnNetDamage(self).Disconnect()`), but its safer |
||||||
|
to do this check. |
||||||
|
Not accessing separate `slot`s is even safer. |
||||||
|
|
||||||
|
> **NOTE:** |
||||||
|
> `singal`s themselves also track `slot`'s life versions and will be able to |
||||||
|
> tell if you've deallocated them. |
Loading…
Reference in new issue