I am new to Unity. I wanted to implement the publisher-subscriber pattern in my game something that was explained in this tutorial: The Observer Pattern - Unity Learn.
But I am not sure if I done it the right way.
I did not wanted to use static fields or static methods. And I also preferred to use unity events instead of C# events.
What I come up with is to create a MessageBrokerService that is a component in my LevelManager, and via that component the MessageBroker instance is available. And also in case of Ciccio (the spawned alien in the game), during instantiation I call Initialize methods to initialize the MessageBroker in the components of the new GameObject. (Actually that was inspired by this article: Rant about Dependency Injection and Unity - Tai's Blog). See the class diagram in the attachment.
So my question is this approach a right approach? Or this is totally wrong?
And I also created a video about this which you can find here:
Hi,
I really appreciate your feedback, thank you very much. I have a question: this violation can be fixed by creating an IMessageBroker interface, which contains all the properties that the MessageBroker suppose to have. And then using that interface everywhere?
Not the one who commented it but I think in this case open-closed principle would be fixed by having the events available in MessageBroker be configurable without changing the code. For example if you wanted to add HumanDiedEvent
Right now if you add a new event, you need to edit MessageBroker. You could solve this by having a Dictionary<string/Event> events that you can add events to when initializing the broker.
So something like
var broker = new MessageBroker();
broker.AddEvent("alienDied", new Event());
broker.AddEvent("humanDied", new Event());
Using magic strings as keys is not a good practice. Events should be tied to identifiers, not strings. It is not a good practice because you can’t refactor string identifiers, and compiler doesn’t catch any errors there.
[
public class MessageBroker {
private UnityEvent _alienReachedDoor;
private UnityEvent _alienDied;
private GameOverEvent _gameOver;
public UnityEvent AlienReachedDoor {
get => _alienReachedDoor;
}
Is fairly dubious. Hiding UnityEvent behind a property means that you’ll be unable to reassign it, BUT due to the event being a reference type and C# not having const correctness, you can still wreck it, by removing all listeners from it, for example. So there’s not a lot of point in doing that.
I’d consider using C# events here, unless you want serialization. Because the main reason to use UnityEvent is to ahve them visible in Editor, or serialization support.
Thank you for your feedback. I guess In your code the EventBus is the event aggregator here. And if my understanding is correct in case of a PlayerKilled event it is only notifying those subscribers who are implementing IEventHandler?
But anyways this is an interesting idea because, if my understanding is correct, it makes the subscription/unsubscription logic easier.
Just want to throw this out there as a potential concept:
While the interface/implementation approach shown in the tutorial isn’t bad, it does mean you will have to create a new interface and implement it, then have your subject manually find/collect all implementations to notify for every event you want to create.
Something like Angular’s RxJS library implements the observer pattern by registering callback functions instead, which means the subject doesn’t need to manually find/collect all the observers to notify - that’s done automatically as soon as an observer subscribes to the subject.
In C#, a similar model could be created using delegates. Rough example:
public class Subject {
private readonly List<Action> callbacks = new List<Action>();
public void Subscribe(Action callback) {
callbacks.Add(callback);
}
public void Unsubscribe(Action callback) {
callbacks.Remove(callback);
}
public void UnsubscribeAll() {
callbacks.Clear();
}
public void Invoke() {
for(int i = 0; i < callbacks.Count; i++) {
callbacks[i].Invoke();
}
}
}
public class EventBroker {
public readonly Subject onPlayerSpawn = new Subject();
public readonly Subject onPlayerDamaged = new Subject();
public readonly Subject onPlayerDie = new Subject();
}
public class SomeObserver : MonoBehaviour {
EventBroker broker = new EventBroker();
void Awake() {
//Lambda expressions can be used, however, they cannot be individually unsubscribed without calling Subject.UnsubscribeAll().
broker.onPlayerDamaged.Subscribe(() => {
Debug.Log("Player was damaged");
});
}
//An individual callback must be registered as such to be able to unsubscribe without calling Subject.UnsubscribeAll().
void OnEnable() {
broker.onPlayerDie.Subscribe(OnPlayerDie);
}
void OnDisable() {
broker.onPlayerDie.Unsubscribe(OnPlayerDie);
}
void OnPlayerDie() {
Debug.Log("Game Over");
}
}
But to be honest, this does feel like re-creating the function of regular C# events, so you may as well just use them instead.
Thank you for the idea. I really appreciate it. And I agree with you. And unfortunately in this solution the events are explicitly defined in the EventBroker, which means it cannot be extended with new events without adding new lines of code to it, just like mine .
Open/close principle is about that. Open for addition but closed for modification. Adding a new entry in the EventBroker is just additional, you don’t have to change the baseline logic to work it. Works with inheriting from it too.
But SOLID in general is great, although during serious game development, use it as long as it doesn’t hinder your actual game development. Your game is more important than any kind of programming technique. Also, if it is working for you, consider it as good enough. Always.
True, but I fear as though the only way to create new events without modifying the logic would be to use strings to define the names of new events in a Dictionary.
Something like this:
public class EventBroker {
private readonly Dictionary<string, List<Action>>> eventsDict = new Dictionary<string, List<Action>>>();
public void Subscribe(string eventName, Action callback) {
if(eventsDict.ContainsKey(eventName)) {
eventsDict[eventName].Add(callback);
}
else {
eventsDict.Add(eventName, new List<Action> { callback });
}
}
public void Unsubscribe(string eventName, Action callback) {
if(eventsDict.TryGetValue(eventName, out List<Action> callbackList)) {
callbackList.Remove(callback);
}
}
public void UnsubscribeAll(string eventName) {
eventsDict.Remove(eventName);
}
public void Invoke(string eventName) {
if(eventsDict.TryGetValue(eventName, out List<Action> callbackList)) {
for(int i = 0; i < callbackList.Count; i++) {
callbackList[i].Invoke();
}
}
}
}
Personally, I would much rather deal with going into the EventBroker class and adding a new Subject to create a new event than deal with string comparisons like this.
public interface IEventHandler<in T> where T : struct
{
void Handle(T evt);
}
public void Subscribe<T>(IEventHandler<T> handler) where T : struct
{
var type = typeof(T);
if (!subscribers.ContainsKey(type))
subscribers.Add(type, new List<object>());
subscribers[type].Add(handler);
}
public void Publish<T>(T evt) where T : struct
{
var type = typeof(T);
if (!subscribers.ContainsKey(type)) return;
var handlers = subscribers[type];
foreach (var handler in handlers)
((IEventHandler<T>)handler).Handle(evt);
}
@DevViktoria So, I watched your latest video about your project, some thoughts.
First of all, great work! Keep it up!
As far as I know the UI Toolkit only has binding service in the editor, the runtime binding feature will be available in 2021.2 according to the roadmap: UI Roadmap - Q3 2020 which means, what you have done there won’t work in a build only in the editor (and that’s why you needed the UnityEditor using statement, because the SerializedObject is a UnityEditor object)
Your decision to postpone the rewrite of the MessageBroker is on point! Don’t fix what’s currently working only when you have the time and inclination to do it. In game development tech debt is normal and rarely paid off in full.