Might be a bit tricky to sort out exactly what’s going on here, but I’ll just include the code for my SubscriptionDirector. It’s a singleton class. But first, here’s the code I most often use to register for events. This is part of the base class I use for almost all other classes, to make it easy for them to register for events:
private HashSet<SubscriptionToken> _subscriptionTokens;
private HashSet<ProviderToken> _providerTokens;
protected virtual void Awake()
{
_subscriptionTokens = new HashSet<SubscriptionToken>();
_providerTokens = new HashSet<ProviderToken>();
}
protected SubscriptionToken RegisterSubscription(SubscriptionType subscriptionType, Action<ISubscriptionPayload> payload)
{
var token = SubscriptionDirector.Instance.Subscribe(subscriptionType, this.gameObject, payload);
_subscriptionTokens.Add(token);
return token;
}
There’s also some cleanup code there to clear out any subscriptions when an object gets destroyed.
SubscriptionType is just an enum that contains the various “events” that are possible to register for.
And if I want to trigger an event, I use a Publish call to the SubscriptionDirector. Here I’m calling an event to make the player immortal, as various unrelated parts of the application might care when that happens:
SubscriptionDirector.Instance.Publish(SubscriptionType.TogglePlayerImmortalMode, new PlayerImmortalModeSubscriptionPayload()
{
ImmortalityMode = PlayerImmortalityMode.TakesDamage,
DamageTypeExceptions = GetAllDamageTypesButExplosions()
});
Anyway, this is probably confusing without having full access to the code base, but maybe it gives some ideas. Here’s the whole subscription director:
using GraviaSoftware.Gravia.Code.Common.Enums;
using GraviaSoftware.Gravia.Code.Data.Subscriptions;
using GraviaSoftware.Gravia.Code.GameManagers;
using GraviaSoftware.Gravia.Code.Interfaces.Subscription;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace GraviaSoftware.Gravia.Code.Directors
{
/// <summary>
/// This is a fairly generic service that allows subscribers to subscribe to various events
/// </summary>
public class SubscriptionDirector : DirectorBase<SubscriptionDirector>
{
[Tooltip("When enabled, verbosely logs subscribe and publish calls.")]
public bool Verbose;
private Dictionary<SubscriptionType, Dictionary<SubscriptionToken, Action<ISubscriptionPayload>>> _subscriptionStorage;
protected SubscriptionDirector()
{
// Protected because this is a singleton.
}
protected override void Awake()
{
base.Awake();
_subscriptionStorage = new Dictionary<SubscriptionType, Dictionary<SubscriptionToken, Action<ISubscriptionPayload>>>();
// These needs to be instantiated very early.
GameObjectTrackingManager.Instance.Ensure();
ElectricalChargeManager.Instance.Ensure();
}
// Use this for initialization
void Start()
{
}
#region Subscribe/Unsubscribe
/// <summary>
/// Subscribed to an event of a certain type.
/// </summary>
/// <param name="subscriptionType"></param>
/// <param name="action"></param>
/// <returns></returns>
public SubscriptionToken Subscribe(SubscriptionType subscriptionType,
GameObject subscriber,
Action<ISubscriptionPayload> callback)
{
VerboseLog($"Subscriber {subscriber.name} subscribing for {subscriptionType} events.");
var token = new SubscriptionToken(subscriptionType, subscriber);
lock (this)
{
if (!_subscriptionStorage.ContainsKey(subscriptionType))
{
_subscriptionStorage.Add(subscriptionType, new Dictionary<SubscriptionToken, Action<ISubscriptionPayload>>());
}
_subscriptionStorage[subscriptionType].Add(token, callback);
}
return token;
}
private void VerboseLog(string message)
{
if (Verbose)
{
Debug.Log(message);
}
}
public void Unsubscribe(SubscriptionToken subscriptionToken)
{
if (subscriptionToken.Subscriber != null)
{
VerboseLog($"Subscriber {subscriptionToken.Subscriber.name} unsubscribing for {subscriptionToken.SubscriptionType} events.");
}
lock (this)
{
if (_subscriptionStorage.ContainsKey(subscriptionToken.SubscriptionType))
{
_subscriptionStorage[subscriptionToken.SubscriptionType].Remove(subscriptionToken);
}
}
}
public bool HasSubscription(SubscriptionToken subscriptionToken)
{
return _subscriptionStorage.ContainsKey(subscriptionToken.SubscriptionType)
&& _subscriptionStorage[subscriptionToken.SubscriptionType].ContainsKey(subscriptionToken);
}
public int GetSubscriptionCount(SubscriptionType subscriptionType)
{
if (!_subscriptionStorage.ContainsKey(subscriptionType))
{
return 0;
}
else
{
return _subscriptionStorage[subscriptionType].Count;
}
}
/// <summary>
/// Gets all current subscriptions of a given subscription type.
/// </summary>
/// <param name="subscriptionType"></param>
/// <returns></returns>
public List<SubscriptionToken> GetSubscriptions(SubscriptionType subscriptionType)
{
if (!_subscriptionStorage.ContainsKey(subscriptionType))
{
return new List<SubscriptionToken>();
}
else
{
return _subscriptionStorage[subscriptionType].Keys.ToList();
}
}
/// <summary>
/// Gets all current subscriptions for a game object.
/// </summary>
/// <param name="go"></param>
/// <returns></returns>
public List<SubscriptionToken> GetSubscriptions(GameObject go)
{
if (go == null)
{
return Enumerable.Empty<SubscriptionToken>().ToList();
}
return _subscriptionStorage.SelectMany(s => s.Value).Select(s => s.Key).Where(s => s.Subscriber == go).ToList();
}
public string GetSubscriberSummary()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("SubscriptionDirector Subscriptions:");
foreach (var subscriptionType in _subscriptionStorage.Keys)
{
if (_subscriptionStorage[subscriptionType].Any())
{
sb.AppendLine($" {subscriptionType}");
foreach (var kvp in _subscriptionStorage[subscriptionType].Where(k => k.Key.Subscriber != null)
.OrderBy(k => k.Key.TimeAdded).ThenBy(k => k.Key.Subscriber.name))
{
sb.AppendLine($" { kvp.Key.Subscriber.name} (Time Added: { kvp.Key.TimeAdded})");
}
}
}
return sb.ToString();
}
/// <summary>
/// This is mainly just for testing, and probably should not be used often.
/// </summary>
public void ClearAllSubscriptions()
{
_subscriptionStorage = new Dictionary<SubscriptionType, Dictionary<SubscriptionToken, Action<ISubscriptionPayload>>>();
}
#endregion
#region Publish
/// <summary>
/// Publish an event of a particular type with a given payload, alerting all subscribers.
/// </summary>
/// <param name="subscriptionType"></param>
/// <param name="payload"></param>
public void Publish(SubscriptionType subscriptionType, ISubscriptionPayload payload = null)
{
VerboseLog($"Publishing events for subscriptionType {subscriptionType}");
Dictionary<SubscriptionToken, Action<ISubscriptionPayload>> subscriptions;
if (_subscriptionStorage.TryGetValue(subscriptionType, out subscriptions))
{
// These needs to act on a list, because it's possible that a subscriber's response to handling
// a publication is to immediately unsubscribe.
foreach (var kvp in subscriptions.ToList())
{
if (kvp.Key.Subscriber == null)
{
// The subscriber has probably been destroyed. Don't run the action, and warn that
// the subscription wasn't formally removed.
Debug.LogWarning($"Subscription for action {subscriptionType} associated with gameObject {kvp.Key.OriginalSubscriberName} is invalid. The subscriber has been destroyed, but the subscription was not removed. Subscription has automatically been removed, but look into why Unsubscribe was not called.");
Unsubscribe(kvp.Key);
}
else
{
try
{
kvp.Value?.Invoke(payload);
VerboseLog($"Notified {kvp.Key.Subscriber.name} of published event for subscriptionType {subscriptionType}");
}
catch (Exception ex)
{
// If any exceptions occur when informing the subscriber, we unsubscribe the subscriber.
Unsubscribe(kvp.Key);
Debug.LogException(ex);
}
}
}
}
}
#endregion
}
}