SMessage - A Simple and Predictable Event System for Unity

    Disclaimer
    This post is some development of the ideas of the post “Simple Event System in Unity” . I do not pretend to be the only true approach to the issue, and in general I am a mediocre programmer, regarding mastodons who live in the habr. I am also very superficially familiar with C #, since I mainly use Java. Nevertheless, fate brought me to Unity and I realized that I have some tool that can be repaid to the community for what I took from him. Simply put, make your own, albeit small, contribution to open source and, I want to believe, good code.

    Those who are too lazy to read the problems, conclusions, etc. can immediately see the code with examples on the github - github.com/erlioniel/unity-smessage
    There you can even .unitypackage download :)

    Problem
    Just starting to build a project in Unity, I immediately came to the conclusion that I needed some kind of event system. Whatever religious fanatics tell you, the situation where the GUI is tightly coupled with game objects is the worst that could be. The architecture of the project, built on the pasta transfer of objects of each other is very difficult to scale and undergo changes. Therefore, an event system should be.

    Another question is what is needed here without fanaticism, because it’s not so long to get to the situation when the program’s behavior becomes impossible to track, because events are quite unpredictable abstraction. But what can we do to simplify the task a little bit?

    Parameters - the first approach
    If you look at the code of the article to which I refer initially, it can be seen that the event system there is extremely simple, because the event can not contain any parameters. Initially, I implemented another system that allowed the use of parameters:

    // Подписка
    Callback.Add(Type.TURN_START, Refresh);
    // Вызов
    Callback.Call(Type.TURN_START, TurnEntity());
    

    As you can see, the ENUM value was used as the event key, which made it a little easier (you could always get a list of possible values ​​from the IDE), as well as pass some parameters without any problems. This generally suited me for the first time.

    Typing is the second approach. The
    main problem of the simple implementation of the event system is its poor predictability and the impossibility of helping the IDE to write code. At some point, I began to catch myself thinking that for any complicated events I had to remember in which order the arguments had to be passed so that they would normally be written to the model. And all these model castes in another in the handlers were also straining. In general, an overgrown system began to behave unpredictably and required more attention and knowledge of the old code in order to support itself.

    After one evening of witchcraft with generics, a system was created in which the IDE helps a lot in any situation. The list of events is easy to get if you accept some rules for naming event classes (for example, the SMessage prefix ...), no castes, handlers immediately receive objects of the final class, and all this is based on classic C # delegates.

    I suggest you to analyze the work of the manager yourself, here are a couple of listings. Below you can find an example of use, which is much more interesting to the end user.
    SManager.cs
    public class SManager {
            private readonly Dictionary _handlers;
            // INSTANCE
            public SManager() {
                _handlers = new Dictionary();
            }
            /// 
            /// Just add new handler to selected event type
            /// 
            /// AbstractSMessage event
            /// Handler function
            public void Add(SCallback value) where T : AbstractSMessage {
                var type = typeof (T);
                if (!_handlers.ContainsKey(type)) {
                    _handlers.Add(type, new SCallbackWrapper());
                }
                ((SCallbackWrapper) _handlers[type]).Add(value);
            }
            public void Remove(SCallback value) where T : AbstractSMessage {
                var type = typeof (T);
                if (_handlers.ContainsKey(type)) {
                    ((SCallbackWrapper) _handlers[type]).Remove(value);
                }
            }
            public void Call(T message) where T : AbstractSMessage {
                var type = message.GetType();
                if (_handlers.ContainsKey(type)) {
                    ((SCallbackWrapper) _handlers[type]).Call(message);
                }
            }
            // STATIC
            private static readonly SManager _instance = new SManager();
            public static void SAdd(SCallback value) where T : AbstractSMessage {
                _instance.Add(value);
            }
            public static void SRemove(SCallback value) where T : AbstractSMessage {
                _instance.Remove(value);
            }
            public static void SCall(T message) where T : AbstractSMessage {
                _instance.Call(message);
            }
        }
    
    SCallbackWrapper.cs
     internal class SCallbackWrapper
            where T : AbstractSMessage {
            private SCallback _delegates;
            public void Add(SCallback value) {
                _delegates += value;
            }
            public void Remove(SCallback value) {
                _delegates -= value;
            }
            public void Call(T message) {
                if (_delegates != null) {
                    _delegates(message);
                }
            }
        }
    

    Usage
    example You can find examples on the github - github.com/erlioniel/unity-smessage/tree/master/Assets/Scripts/Examples
    But here I will analyze the simplest case how to use this system. For example, I will use the singleton implementation of the event manager, although you have the right to create your own instance for any needs. Suppose we need to create a new event that will notify that some object has been clicked. Create an event object.

    The event model is the event marker itself, so you should create a new class for each event. This is the IDE help fee: <AbstractSMessage is used as a base class, which can store some kind of object
      public class SMessageExample :  AbstractSMessageValued {
        public SMessageExample (GameObject value) : base(value) { }
      }
    

    In the object itself, we will need to call it this event and pass the necessary arguments there
      public class ExampleObject : MonoBehaviour {
        public void OnMouseDown () {
          SManager.SCall(new SMessageExample(gameObject));
        }
      }
    

    Well, finally, create another object that will track this event
      public class ExampleHandlerObject : MonoBehaviour {
            // Добавлять слушателей лучше в OnEnable
            public void OnEnable() {
                SManager.SAdd(OnMessage);
            }
            // И не забывать удалять в OnDisable
            public void OnDisable() {
                SManager.SRemove(OnMessage);
            }
            private void OnMessage (SMessageExample message) {
              Debug.Log("OnMouseDown for object "+message.Value.name);
            }
      }
    

    Everything is quite simple and obvious, but more importantly, the compiler / IDE will check everything for you and help you in your work.
    PS I didn’t check the code, there may be errors :)

    Instead of the conclusion, the
    Event System is a very powerful tool and should not be underestimated. High code cohesion is not as good as it may seem to some programmers, especially when the project grows to medium size.

    I hope the code will be useful to someone. I will be glad to some comments and suggestions.

    UPD:
    Added an abstract class AbstractSMessageValued and slightly updated the example in the article.

    Also popular now: