Browse Source

Document Acedia's event system

pull/8/head
Anton Tarasenko 3 years ago
parent
commit
e4794554ad
  1. 392
      docs/events.md
  2. 2
      docs/index.md

392
docs/events.md

@ -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.

2
docs/index.md

@ -51,4 +51,4 @@ about any topic of interest, but we strongly recommend that you first read up on
the fundamental topics:
[what is API](./api.md),
at least non-advanced topics of [Acedia's objects / actors](./objects.md)
and about signal / slot system.
and about [signal / slot system](./events.md).

Loading…
Cancel
Save