diff --git a/Documentation/Signals.md b/Documentation/Signals.md index 98314c749..54654f360 100644 --- a/Documentation/Signals.md +++ b/Documentation/Signals.md @@ -11,6 +11,7 @@ * SignalBusInstaller * When To Use Signals * Advanced + * Abstract Signals * Signals With Subcontainers * Asynchronous Signals * Signal Settings @@ -479,6 +480,151 @@ These are just rules of thumb, but useful to keep in mind when using signals. T 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. +## Abstract Signals + +One of the problems of the signals is that when you subscribe to their types you are coupling your concrete signal types to the subscribers + +For example, Lets say I have a player and i want to save the game when i finish a level. +Ok easy, I create ``SignalLevelCompleted`` and then I subscribe it to my ``SaveGameSystem`` +then I also want to save when i reach a checkpoint, again i create ``SignalCheckpointReached`` +and then I subscribe it to my ``SaveGameSystem`` +you are begining to get something like this... +```csharp +public class Example +{ + SignalBus signalBus; + public Example(Signalbus signalBus) => this.signalBus = signalBus; + + public void CheckpointReached() => signalBus.Fire(); + + public void CompleteLevel() => signalBus.Fire(); +} + +public class SaveGameSystem +{ + public SaveGameSystem(SignalBus signalBus) + { + signalBus.Subscribe(x => SaveGame()); + signalBus.Subscribe(x => SaveGame()); + } + + void SaveGame() { /*Saves the game*/ } +} + +//in your installer +Container.DeclareSignal(); +Container.DeclareSignal(); + +//your signal types +public struct SignalCheckpointReached{} +public struct SignalLevelCompleted{} +``` + +And then you realize you are coupling the types``signalLevelCompleted`` and ``SignalCheckpointReached``to ``SaveGameSystem``. +``SaveGameSystem`` shouldn't know about those "non related with saving" events... + +So let's give the power of interfaces to signals! +So i have the ``SignalCheckpointReached`` and ``SignalLevelCompleted`` both implementing **``ISignalGameSaver``** +and my ``SaveGameSystem`` just Subscribes to **``ISignalGameSaver``** for saving the game +So when i fire any of those signals the ``SaveGameSystem`` saves the game. +Then you have something like this... +```csharp +public class Example +{ + SignalBus signalBus; + public Example(Signalbus signalBus) => this.signalBus = signalBus; + + public void CheckpointReached() => signalBus.AbstractFire(); + + public void CompleteLevel() => signalBus.AbstractFire(); +} + +public class SaveGameSystem +{ + public SaveGameSystem(SignalBus signalBus) + { + signalBus.Subscribe(x => SaveGame()); + } + + void SaveGame() { /*Saves the game*/ } +} + +//in your installer +Container.DeclareSignalWithInterfaces(); +Container.DeclareSignalWithInterfaces(); + +//your signal types +public struct SignalCheckpointReached : ISignalGameSaver{} +public struct SignalLevelCompleted : ISignalGameSaver{} + +public interface ISignalGameSaver{} +``` + +Now your ``SaveGameSystem`` doesnt knows about CheckPoints nor Level events, and just reacts to signals that save the game. +The main difference is in the Signal declaration and Firing + - ``DeclareSignalWithInterfaces`` works like ``DeclareSignal`` but it declares the interfaces too. + - ``AbstractFire`` is the same that ``Fire`` but it fires the interfacesjust if you have Declared the signal with interfaces + otherwise it will throw an exception. + +Ok, let's show even more power. +Now i create another signal for the WorldDestroyed Achievement "SignalWorldDestroyed" +But i also want my SoundSystem to play sounds when i reach a checkpoint and/or unlock an Achievement +So the code could look like this. +```csharp +public class Example +{ + SignalBus signalBus; + public Example(Signalbus signalBus) => this.signalBus = signalBus; + + public void CheckpointReached() => signalBus.AbstractFire(); + + public void DestroyWorld() => signalBus.AbstractFire(); +} + +public class SoundSystem +{ + public SoundSystem(SignalBus signalBus) + { + signalBus.Subscribe(x => PlaySound(x.soundId)); + } + + void PlaySound(int soundId) { /*Plays the sound with the given id*/ } +} + +public class AchievementSystem +{ + public AchievementSystem(SignalBus signalBus) + { + signalBus.Subscribe(x => UnlockAchievement(x.achievementKey)); + } + + void UnlockAchievement(string key) { /*Unlocks the achievement with the given key*/ } +} + +//in your installer +Container.DeclareSignalWithInterfaces(); +Container.DeclareSignalWithInterfaces(); + +//your signal types +public struct SignalCheckpointReached : ISignalGameSaver, ISignalSoundPlayer +{ + public int SoundId { get => 2} //or configured in a scriptable with constants instead of hardcoded +} +public struct SignalWorldDestroyed : ISignalAchievementUnlocker, ISignalSoundPlayer +{ + public int SoundId { get => 4} + public string AchievementKey { get => "WORLD_DESTROYED"} +} + +//Your signal interfaces +public interface ISignalGameSaver{} +public interface ISignalSoundPlayer{ int SoundId {get;}} +public interface ISignalAchievementUnlocker{ string AchievementKey {get;}} +``` + +It offers a lot of modularity and abstraction for signals, +you fire a concrete signal telling what you did and give them functionality trough Interface implementations + ## Signals With Subcontainers 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 scene parent), then subscribe to it in the Environment scene, and then fire it from the GUI scene. diff --git a/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/Binders/SignalExtensions.cs b/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/Binders/SignalExtensions.cs index c8fb19cb0..cd93bb32f 100755 --- a/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/Binders/SignalExtensions.cs +++ b/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/Binders/SignalExtensions.cs @@ -30,6 +30,22 @@ public static DeclareSignalIdRequireHandlerAsyncTickPriorityCopyBinder DeclareSi return container.DeclareSignal(typeof(TSignal)); } + public static DeclareSignalIdRequireHandlerAsyncTickPriorityCopyBinder DeclareSignalWithInterfaces(this DiContainer container) + { + Type type = typeof(TSignal); + + var declaration = container.DeclareSignal(type); + + Type[] interfaces = type.GetInterfaces(); + int numOfInterfaces = interfaces.Length; + for (int i = 0; i < numOfInterfaces; i++) + { + container.DeclareSignal(interfaces[i]); + } + + return declaration; + } + public static BindSignalIdToBinder BindSignal(this DiContainer container) { var signalBindInfo = new SignalBindingBindInfo(typeof(TSignal)); diff --git a/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/SignalDeclaration.cs b/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/SignalDeclaration.cs index ff801a0db..b651be6e8 100755 --- a/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/SignalDeclaration.cs +++ b/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Internal/SignalDeclaration.cs @@ -41,6 +41,8 @@ public IObservable Stream } #endif + public List Subscriptions => _subscriptions; + public int TickPriority { get; private set; diff --git a/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Main/SignalBus.cs b/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Main/SignalBus.cs index cf9b13d20..5457aa643 100755 --- a/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Main/SignalBus.cs +++ b/UnityProject/Assets/Plugins/Zenject/OptionalExtras/Signals/Main/SignalBus.cs @@ -11,7 +11,7 @@ namespace Zenject public class SignalBus : ILateDisposable { readonly SignalSubscription.Pool _subscriptionPool; - readonly Dictionary _localDeclarationMap; + readonly Dictionary _localDeclarationMap = new Dictionary(); readonly SignalBus _parentBus; readonly Dictionary _subscriptionMap = new Dictionary(); readonly ZenjectSettings.SignalSettings _settings; @@ -35,7 +35,14 @@ public SignalBus( _signalDeclarationFactory = signalDeclarationFactory; _container = container; - _localDeclarationMap = signalDeclarations.ToDictionary(x => x.BindingId, x => x); + signalDeclarations.ForEach(x => + { + if (!_localDeclarationMap.ContainsKey(x.BindingId)) + { + _localDeclarationMap.Add(x.BindingId, x); + } + else _localDeclarationMap[x.BindingId].Subscriptions.AllocFreeAddRange(x.Subscriptions); + }); _parentBus = parentBus; } @@ -49,6 +56,24 @@ public int NumSubscribers get { return _subscriptionMap.Count; } } + + //Fires Signals with their interfaces + public void AbstractFire() where TSignal : new() => AbstractFire(new TSignal()); + public void AbstractFire(TSignal signal) => AbstractFireId(null, signal); + public void AbstractFireId(object identifier, TSignal signal) + { + // Do this before creating the signal so that it throws if the signal was not declared + Type signalType = typeof(TSignal); + InternalFire(signalType, signal, identifier, true); + + Type[] interfaces = signalType.GetInterfaces(); + int numOfInterfaces = interfaces.Length; + for (int i = 0; i < numOfInterfaces; i++) + { + InternalFire(interfaces[i], signal, identifier, true); + } + } + public void LateDispose() { if (_settings.RequireStrictUnsubscribe) @@ -76,10 +101,7 @@ public void LateDispose() public void FireId(object identifier, TSignal signal) { - // Do this before creating the signal so that it throws if the signal was not declared - var declaration = GetDeclaration(typeof(TSignal), identifier, true); - - declaration.Fire(signal); + InternalFire(typeof(TSignal), signal, identifier, true); } public void Fire(TSignal signal) @@ -89,11 +111,7 @@ public void Fire(TSignal signal) public void FireId(object identifier) { - // Do this before creating the signal so that it throws if the signal was not declared - var declaration = GetDeclaration(typeof(TSignal), identifier, true); - - declaration.Fire( - (TSignal)Activator.CreateInstance(typeof(TSignal))); + InternalFire(typeof(TSignal), null, identifier, true); } public void Fire() @@ -103,7 +121,7 @@ public void Fire() public void FireId(object identifier, object signal) { - GetDeclaration(signal.GetType(), identifier, true).Fire(signal); + InternalFire(signal.GetType(), signal, identifier, true); } public void Fire(object signal) @@ -128,16 +146,13 @@ public bool IsSignalDeclared(Type signalType) public bool IsSignalDeclared(Type signalType, object identifier) { - return GetDeclaration(signalType, identifier, false) != null; + var signalId = new BindingId(signalType, identifier); + return GetDeclaration(signalId) != null; } public void TryFireId(object identifier, TSignal signal) { - var declaration = GetDeclaration(typeof(TSignal), identifier, false); - if (declaration != null) - { - declaration.Fire(signal); - } + InternalFire(typeof(TSignal), signal, identifier, false); } public void TryFire(TSignal signal) @@ -147,12 +162,7 @@ public void TryFire(TSignal signal) public void TryFireId(object identifier) { - var declaration = GetDeclaration(typeof(TSignal), identifier, false); - if (declaration != null) - { - declaration.Fire( - (TSignal)Activator.CreateInstance(typeof(TSignal))); - } + InternalFire(typeof(TSignal), null, identifier, false); } public void TryFire() @@ -162,11 +172,7 @@ public void TryFire() public void TryFireId(object identifier, object signal) { - var declaration = GetDeclaration(signal.GetType(), identifier, false); - if (declaration != null) - { - declaration.Fire(signal); - } + InternalFire(signal.GetType(), signal, identifier, false); } public void TryFire(object signal) @@ -174,6 +180,31 @@ public void TryFire(object signal) TryFireId(null, signal); } + private void InternalFire(Type signalType, object signal, object identifier, bool requireDeclaration) + { + var signalId = new BindingId(signalType, identifier); + + // Do this before creating the signal so that it throws if the signal was not declared + var declaration = GetDeclaration(signalId); + + if (declaration == null) + { + if (requireDeclaration) + { + throw Assert.CreateException("Fired undeclared signal '{0}'!", signalId); + } + } + else + { + if (signal == null) + { + signal = Activator.CreateInstance(signalType); + } + + declaration.Fire(signal); + } + } + #if ZEN_SIGNALS_ADD_UNIRX public IObservable GetStreamId(object identifier) { @@ -187,7 +218,7 @@ public IObservable GetStream() public IObservable GetStreamId(Type signalType, object identifier) { - return GetDeclaration(signalType, identifier, true).Stream; + return GetDeclaration(new BindingId(signalType, identifier)).Stream; } public IObservable GetStream(Type signalType) @@ -354,7 +385,13 @@ void SubscribeInternal(SignalSubscriptionId id, Action callback) Assert.That(!_subscriptionMap.ContainsKey(id), "Tried subscribing to the same signal with the same callback on Zenject.SignalBus"); - var declaration = GetDeclaration(id.SignalId, true); + var declaration = GetDeclaration(id.SignalId); + + if (declaration == null) + { + throw Assert.CreateException("Tried subscribing to undeclared signal '{0}'!", id.SignalId); + } + var subscription = _subscriptionPool.Spawn(callback, declaration); _subscriptionMap.Add(id, subscription); @@ -393,12 +430,7 @@ public void DeclareSignal( _localDeclarationMap.Add(declaration.BindingId, declaration); } - SignalDeclaration GetDeclaration(Type signalType, object identifier, bool requireDeclaration) - { - return GetDeclaration(new BindingId(signalType, identifier), requireDeclaration); - } - - SignalDeclaration GetDeclaration(BindingId signalId, bool requireDeclaration) + SignalDeclaration GetDeclaration(BindingId signalId) { SignalDeclaration handler; @@ -409,12 +441,7 @@ SignalDeclaration GetDeclaration(BindingId signalId, bool requireDeclaration) if (_parentBus != null) { - return _parentBus.GetDeclaration(signalId, requireDeclaration); - } - - if (requireDeclaration) - { - throw Assert.CreateException("Fired undeclared signal '{0}'!", signalId); + return _parentBus.GetDeclaration(signalId); } return null;