Skip to content

Commit a955dd6

Browse files
committed
Merge branch 'patch-3'
1 parent 5b86162 commit a955dd6

File tree

5 files changed

+194
-50
lines changed

5 files changed

+194
-50
lines changed
Lines changed: 11 additions & 0 deletions
Loading

Documentation/Signals.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* <a href="#signalbusinstaller">SignalBusInstaller</a>
1212
* <a href="#when-to-use-signals">When To Use Signals</a>
1313
* Advanced
14+
* <a href="#abstract-signals">Abstract Signals</a>
1415
* <a href="#use-with-subcontainers">Signals With Subcontainers</a>
1516
* <a href="#async-signals">Asynchronous Signals</a>
1617
* <a href="#settings">Signal Settings</a>
@@ -479,6 +480,152 @@ These are just rules of thumb, but useful to keep in mind when using signals. T
479480

480481
When event driven program is abused, it is possible to find yourself in "callback hell" where events are triggering other events etc. and which make the entire system impossible to understand. So signals in general should be used with caution. Personally I like to use signals for high level game-wide events and then use other forms of communication (unirx streams, c# events, direct method calls, interfaces) for most other things.
481482

483+
## <a id="abstract-signals"></a>Abstract Signals
484+
485+
One of the problems of the signals is that when you subscribe to their types
486+
you are coupling your concrete signal types to the subscribers
487+
488+
For example, Lets say I have a player and i want to save the game when i finish a level.
489+
Ok easy, I create ``SignalLevelCompleted`` and then I subscribe it to my ``SaveGameSystem``
490+
then I also want to save when i reach a checkpoint, again i create ``SignalCheckpointReached``
491+
and then I subscribe it to my ``SaveGameSystem``
492+
you are begining to get something like this...
493+
```csharp
494+
public class Example
495+
{
496+
SignalBus signalBus;
497+
public Example(Signalbus signalBus) => this.signalBus = signalBus;
498+
499+
public void CheckpointReached() => signalBus.Fire<SignalCheckpointReached>();
500+
501+
public void CompleteLevel() => signalBus.Fire<SignalLevelCompleted>();
502+
}
503+
504+
public class SaveGameSystem
505+
{
506+
public SaveGameSystem(SignalBus signalBus)
507+
{
508+
signalBus.Subscribe<SignalCheckpointReached>(x => SaveGame());
509+
signalBus.Subscribe<SignalLevelCompleted>(x => SaveGame());
510+
}
511+
512+
void SaveGame() { /*Saves the game*/ }
513+
}
514+
515+
//in your installer
516+
Container.DeclareSignal<SignalLevelCompleted>();
517+
Container.DeclareSignal<SignalCheckpointReached>();
518+
519+
//your signal types
520+
public struct SignalCheckpointReached{}
521+
public struct SignalLevelCompleted{}
522+
```
523+
524+
And then you realize you are coupling the types``signalLevelCompleted`` and ``SignalCheckpointReached``to ``SaveGameSystem``.
525+
``SaveGameSystem`` shouldn't know about those "non related with saving" events...
526+
527+
So let's give the power of interfaces to signals!
528+
So i have the ``SignalCheckpointReached`` and ``SignalLevelCompleted`` both implementing **``ISignalGameSaver``**
529+
and my ``SaveGameSystem`` just Subscribes to **``ISignalGameSaver``** for saving the game
530+
So when i fire any of those signals the ``SaveGameSystem`` saves the game.
531+
Then you have something like this...
532+
```csharp
533+
public class Example
534+
{
535+
SignalBus signalBus;
536+
public Example(Signalbus signalBus) => this.signalBus = signalBus;
537+
538+
public void CheckpointReached() => signalBus.AbstractFire<SignalCheckpointReached>();
539+
540+
public void CompleteLevel() => signalBus.AbstractFire<SignalLevelCompleted>();
541+
}
542+
543+
public class SaveGameSystem
544+
{
545+
public SaveGameSystem(SignalBus signalBus)
546+
{
547+
signalBus.Subscribe<ISignalGameSaver>(x => SaveGame());
548+
}
549+
550+
void SaveGame() { /*Saves the game*/ }
551+
}
552+
553+
//in your installer
554+
Container.DeclareSignalWithInterfaces<SignalLevelCompleted>();
555+
Container.DeclareSignalWithInterfaces<SignalCheckpointReached>();
556+
557+
//your signal types
558+
public struct SignalCheckpointReached : ISignalGameSaver{}
559+
public struct SignalLevelCompleted : ISignalGameSaver{}
560+
561+
public interface ISignalGameSaver{}
562+
```
563+
564+
Now your ``SaveGameSystem`` doesnt knows about CheckPoints nor Level events, and just reacts to signals that save the game.
565+
The main difference is in the Signal declaration and Firing
566+
- ``DeclareSignalWithInterfaces`` works like ``DeclareSignal`` but it declares the interfaces too.
567+
- ``AbstractFire`` is the same that ``Fire`` but it fires the interfacesjust if you have Declared the signal with interfaces
568+
otherwise it will throw an exception.
569+
570+
Ok, let's show even more power.
571+
Now i create another signal for the WorldDestroyed Achievement "SignalWorldDestroyed"
572+
But i also want my SoundSystem to play sounds when i reach a checkpoint and/or unlock an Achievement
573+
So the code could look like this.
574+
```csharp
575+
public class Example
576+
{
577+
SignalBus signalBus;
578+
public Example(Signalbus signalBus) => this.signalBus = signalBus;
579+
580+
public void CheckpointReached() => signalBus.AbstractFire<SignalCheckpointReached>();
581+
582+
public void DestroyWorld() => signalBus.AbstractFire<SignalWorldDestroyed>();
583+
}
584+
585+
public class SoundSystem
586+
{
587+
public SoundSystem(SignalBus signalBus)
588+
{
589+
signalBus.Subscribe<ISignalSoundPlayer>(x => PlaySound(x.soundId));
590+
}
591+
592+
void PlaySound(int soundId) { /*Plays the sound with the given id*/ }
593+
}
594+
595+
public class AchievementSystem
596+
{
597+
public AchievementSystem(SignalBus signalBus)
598+
{
599+
signalBus.Subscribe<ISignalAchievementUnlocker>(x => UnlockAchievement(x.achievementKey));
600+
}
601+
602+
void UnlockAchievement(string key) { /*Unlocks the achievement with the given key*/ }
603+
}
604+
605+
//in your installer
606+
Container.DeclareSignalWithInterfaces<SignalCheckpointReached>();
607+
Container.DeclareSignalWithInterfaces<SignalWorldDestroyed>();
608+
609+
//your signal types
610+
public struct SignalCheckpointReached : ISignalGameSaver, ISignalSoundPlayer
611+
{
612+
public int SoundId { get => 2} //or configured in a scriptable with constants instead of hardcoded
613+
}
614+
public struct SignalWorldDestroyed : ISignalAchievementUnlocker, ISignalSoundPlayer
615+
{
616+
public int SoundId { get => 4}
617+
public string AchievementKey { get => "WORLD_DESTROYED"}
618+
}
619+
620+
//Your signal interfaces
621+
public interface ISignalGameSaver{}
622+
public interface ISignalSoundPlayer{ int SoundId {get;}}
623+
public interface ISignalAchievementUnlocker{ string AchievementKey {get;}}
624+
```
625+
626+
It offers a lot of modularity and abstraction for signals,
627+
you fire a concrete signal telling what you did and give them functionality trough Interface implementations
628+
482629
## <a id="use-with-subcontainers"></a>Signals With Subcontainers
483630

484631
Signals are only visible at the container level where they are declared and below. For example, you might use Unity's multi-scene support and split up your game into a GUI scene and an Environment scene. In the GUI scene you might fire a signal indicating that the GUI popup overlay has been opened/closed, so that the Environment scene can pause/resume activity. One way of achieving this would be to declare a signal in a ProjectContext installer (or a shared <a href="../README.md#scene-parenting">scene parent</a>), then subscribe to it in the Environment scene, and then fire it from the GUI scene.

UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/Binders/SignalExtensions.cs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,20 @@ public static DeclareSignalIdRequireHandlerAsyncTickPriorityCopyBinder DeclareSi
3030
return container.DeclareSignal(typeof(TSignal));
3131
}
3232

33-
//If you declare a concrete signal type with their interfaces you can do abstract Firing to get their non concrete subscriptions
34-
public static DeclareSignalIdRequireHandlerAsyncTickPriorityCopyBinder DeclareSignalWithInterfaces<TSignal>(this DiContainer container)
35-
{
36-
Type type = typeof(TSignal);
37-
38-
var declaration = container.DeclareSignal(type);
39-
40-
//Automatic interface declaration
41-
Type[] interfaces = type.GetInterfaces();
42-
int numOfInterfaces = interfaces.Length;
43-
for (int i = 0; i < numOfInterfaces; i++)
44-
{
45-
container.DeclareSignal(interfaces[i]);
46-
}
47-
48-
return declaration;
33+
public static DeclareSignalIdRequireHandlerAsyncTickPriorityCopyBinder DeclareSignalWithInterfaces<TSignal>(this DiContainer container)
34+
{
35+
Type type = typeof(TSignal);
36+
37+
var declaration = container.DeclareSignal(type);
38+
39+
Type[] interfaces = type.GetInterfaces();
40+
int numOfInterfaces = interfaces.Length;
41+
for (int i = 0; i < numOfInterfaces; i++)
42+
{
43+
container.DeclareSignal(interfaces[i]);
44+
}
45+
46+
return declaration;
4947
}
5048

5149
public static BindSignalIdToBinder<TSignal> BindSignal<TSignal>(this DiContainer container)

UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/SignalDeclaration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public IObservable<object> Stream
4141
}
4242
#endif
4343

44+
public List<SignalSubscription> Subscriptions => _subscriptions;
45+
4446
public int TickPriority
4547
{
4648
get; private set;

UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Main/SignalBus.cs

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Zenject
1111
public class SignalBus : ILateDisposable
1212
{
1313
readonly SignalSubscription.Pool _subscriptionPool;
14-
readonly Dictionary<BindingId, SignalDeclaration> _localDeclarationMap;
14+
readonly Dictionary<BindingId, SignalDeclaration> _localDeclarationMap = new Dictionary<BindingId, SignalDeclaration>();
1515
readonly SignalBus _parentBus;
1616
readonly Dictionary<SignalSubscriptionId, SignalSubscription> _subscriptionMap = new Dictionary<SignalSubscriptionId, SignalSubscription>();
1717
readonly ZenjectSettings.SignalSettings _settings;
@@ -35,7 +35,14 @@ public SignalBus(
3535
_signalDeclarationFactory = signalDeclarationFactory;
3636
_container = container;
3737

38-
_localDeclarationMap = signalDeclarations.ToDictionary(x => x.BindingId, x => x);
38+
signalDeclarations.ForEach(x =>
39+
{
40+
if (!_localDeclarationMap.ContainsKey(x.BindingId))
41+
{
42+
_localDeclarationMap.Add(x.BindingId, x);
43+
}
44+
else _localDeclarationMap[x.BindingId].Subscriptions.AllocFreeAddRange(x.Subscriptions);
45+
});
3946
_parentBus = parentBus;
4047
}
4148

@@ -50,43 +57,22 @@ public int NumSubscribers
5057
}
5158

5259

53-
//AbstractFire Works like a normal Fire but it fires the interface types of the signal too
60+
//Fires Signals with their interfaces
5461
public void AbstractFire<TSignal>() where TSignal : new() => AbstractFire(new TSignal());
5562
public void AbstractFire<TSignal>(TSignal signal) => AbstractFireId(null, signal);
5663
public void AbstractFireId<TSignal>(object identifier, TSignal signal)
5764
{
5865
// Do this before creating the signal so that it throws if the signal was not declared
59-
Type tt = typeof(TSignal);
60-
var declaration = GetDeclaration(tt, identifier, true);
61-
declaration.Fire(signal);
62-
63-
//Everything is fired like a normal signal and then this method Fires the signal with the interface types
64-
//Its async because its faster and doesn't blocks the main thread when you fire signals
65-
//Tested with a loop of 1 million iteration and this is the faster way of getting the interfaces fast
66-
FireSignalGetDeclarationForInterfacesAsync(identifier, signal, tt);
67-
}
68-
69-
//Fire and forget methof for the task
70-
public async void FireSignalGetDeclarationForInterfacesAsync<TSignal>(object identifier, TSignal signal, Type type)
71-
{
72-
await Task.Run(() => FireSignalGetDeclarationForInterfacesTask(identifier, signal, type));
73-
}
74-
public async Task FireSignalGetDeclarationForInterfacesTask<TSignal>(object identifier, TSignal signal, Type type)
75-
{
76-
//The asynchronous iteration for reflection
77-
Type[] interfaces = type.GetInterfaces();
78-
int numOfInterfaces = interfaces.Length;
79-
for (int i = 0; i < numOfInterfaces; i++)
80-
{
81-
//To make this work you should also declare the signal's interfaces, but they are automatically declared
82-
//if you do "DeclareSignalWithInterfaces<TSignal>()" in the container
83-
//Go to SignalExtensions.cs for more info
84-
var declaration = GetDeclaration(interfaces[i], identifier, true);
85-
declaration.Fire(signal);
86-
}
87-
}
88-
66+
Type signalType = typeof(TSignal);
67+
InternalFire(signalType, signal, identifier, true);
8968

69+
Type[] interfaces = signalType.GetInterfaces();
70+
int numOfInterfaces = interfaces.Length;
71+
for (int i = 0; i < numOfInterfaces; i++)
72+
{
73+
InternalFire(interfaces[i], signal, identifier, true);
74+
}
75+
}
9076

9177
public void LateDispose()
9278
{
@@ -232,7 +218,7 @@ public IObservable<TSignal> GetStream<TSignal>()
232218

233219
public IObservable<object> GetStreamId(Type signalType, object identifier)
234220
{
235-
return GetDeclaration(signalType, identifier, true).Stream;
221+
return GetDeclaration(new BindingId(signalType, identifier)).Stream;
236222
}
237223

238224
public IObservable<object> GetStream(Type signalType)

0 commit comments

Comments
 (0)