Search code examples
c#functionunity-game-engineglobal

Universal function like Start() in Unity C#


I am creating a multiplayer system for unity that is aimed towards people who don't want to mess with anything with networking.

To simplify it, I want to create universal functions (for lack of a better name) such as Start() or Update() that can be called by another script without a reference to the parent script.

I can use Unity's event system, but that requires the user of the function to add a subscription to the universal function (and I don't want this).

Another way is to just search through every script for the function but this has a few problems like functions needing to be public, some complicated system for when scripts are added to gameobjects during runtime, and just being a bit sketchy.

My goal is to be able to put a function like "ClientDisconnected()" in any script, and it be called without any extra lines or references.


Solution

  • As already said by Jens Steenmetz' answer those methods are invoked by the Unity engine itself via a message system. This actually happens deeper in the underlying c++ engine. The c# API level on compile time already pre-caches which of those special message methods are implemented by your component(s).


    I would definitely discourage from using magic methods and especially Reflection on runtime. Even at editor/compile time it has a huge disadvantage: Your users don't know.

    The same stands for those Unity messages - just that most IDEs now have a Unity integration that is aware of those and can recognize and tell you if a method name is one of Unity's special messages or if e.g. the signature is wrong.

    In your case you - and most importantly your users - don't have such mechanics and if your users implement a wrong signature it could throw runtime exceptions only - or you would have to cover those cases while compiling. Or e.g. someone just happens to use a method name that matches with one of your magic methods - there is nothing that will prevent that method to be called, but it is not obvious to your user why and where from.

    => At best it is very tricky and error prone - and done wrong extremely slow!

    Such as in your approach - iterating all existing components is extremely expensive - and additionally you do it every single time without caching your results anywhere. If now two clients connect you start re-checking all components over and over again.

    Do not stick to that!


    I would say

    • Use (a) certain interface(s)
    • Have a central "manager"
    • Have your implementors register themselves
    • Have the manager simply inform all registered listeners when events occur

    Look at how e.g. Photon does it - See Photon's MonoBehaviourPunCallbacks

    public class MonoBehaviourPunCallbacks : MonoBehaviourPun, IConnectionCallbacks , IMatchmakingCallbacks , IInRoomCallbacks, ILobbyCallbacks, IWebRpcCallback, IErrorInfoCallback
    {
        public virtual void OnEnable()
        {
            PhotonNetwork.AddCallbackTarget(this);
        }
    
        public virtual void OnDisable()
        {
            PhotonNetwork.RemoveCallbackTarget(this);
        }
        
        ...
    

    and then have some central manager instance where those registered listeners are collected and informed - they basically more or less just have a type check looking for the interfaces and sort the instances into according lists.

    This is Example is way to oversimplified here but just to give you an idea which comes pretty close to what they actually do deep internally:

    // Not the actual implementation - but actually pretty close
    public static class PhotonNetwork
    {
        private readonly static List<ILobbyCallbacks> lobbyListeners = new ();
        private readonly static List<IConnectionCallbacks> connectionListeners = new ();
        ....
    
    
        public static void AddCallbackTarget(object listener)
        {
            if(listener is ILobbyCallbacks lobbyListener)
            {
                lobbyListeners.Add(lobbyListener);
            }
    
            if(listener is IConnectionCallbacks connectionListener)
            {
                lobbyListeners.Add(connectionListener);
            }
    
            ...
        }
    
        public static void RemoveCallbackTarget(object listener)
        {
            if(listener is ILobbyCallbacks lobbyListener)
            {
                lobbyListeners.Remove(lobbyListener);
            }
    
            if(listener is IConnectionCallbacks connectionListener)
            {
                lobbyListeners.Remove(connectionListener);
            }
    
            ...
        }
    
        // or maybe it even handles the networking itself
        internal static void ClientConnected(SomeArgs args)
        {
            foreach(var connectionListener in connectionListeners)
            {
                try
                {
                    connectionListener.OnClientConnected(args);
                }
                catch(Exception e)
                {
                    Debug.LogException(s);
                }
            }
        }
    }
    

    There isn't even really the need to maintain your custom lists (like photon does). event itself anyway already basically is a list of callbacks itself so you could as well directly subscribe and unsubscribe.

    => Could even take this a step further and decouple it from MonoBehaviour by having certain event wrapper instances - not a magic message method directly but maybe close enough / even better:

    public class ClientConnectedEvent, IDisposable
    {
        public ClientConnectedEvent(Action<SomeArgs> callback)
        {
            onClientConnected = callback;
            YourCentralManager.onClientConnected += onClientConnected;
        }
    
        public void Dispose()
        {
            YourCentralManager.onClientConnected -= onClientConnected;
        }
    
        private Action<SomeArgs> onClientConnected;
    }
    

    and the manger would simply have e.g.

    internal static class YourCentralManager
    {
        internal static event Action<SomeArgs> onClientConnected;
    
        // again or maybe this class already handles the networking itself anyway
        internal static void ClientConnected(SomeArgs args)
        {
            onClientConnected?.Invoke(args);
        }
    }
    

    so your users could simply create one of these in any MonoBehaviour (or other scripts)

    public class Example : MonoBehaviour
    {
        private ClientConnectedEvent clientConnectedEvent;
    
        private void OnEnable()
        {
            clientConnectedEvent = new ClientConnectedEvent(OnClientConnected);
        }
    
        private void OnClientConnected(SomeArgs args)
        {
            // ...
        }
    
        private void OnDestroy()
        {
            clientConnectedEvent?.Dispose();
        }
    }
    

    and then if you feel like it you could still additionally also provide a base class implementation like mentioned MonoBehaviourPunCallbacks that already implements that stuff as virtual for your users so they only override whatever they need.