Universal function like Start() in Unity C#

97 Views Asked by At

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.

3

There are 3 best solutions below

1
derHugo On BEST ANSWER

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.

4
Jens Steenmetz On

Unity's Monobehaviour class does not implement methods such as Awake, Start, and Update, but instead searches for them and stores them whenever a new Monobehaviour class is loaded. It is not clear how Unity does this exactly, but you can learn a little bit more about it here.

Regarding your desired functionality, there are a few possible ways to achieve that. The most straightforward solution is probably using C# Reflection. However, you would still need some direction as to what classes you look through when looking for your 'universal' method. Unity solves this problem by only looking through Monobehaviour classes. As a consequence, the Awake, Start, and Update methods only work like 'universal' methods inside a class that derives the Monobehaviour class. The easiest would probably be to create your own base class for that. However, in that case, you could also simply make a virtual or abstract method that requires the user to add the keyword override before the method name. A little bit less elegant, but in many cases a perfectly fine solution (on the plus side, this would make the method show up in the IDE, as opposed to when you use C# Reflection).

I hope this answers your question.

1
The Epic Bacon On

What I ended up doing was using C# Reflection (as Jens brought up (thanks)) and looking through all Monobehaviour scripts in the scene for a given function.

While it is a bit of a rough implementation, it does get the job done. If I were calling these methods a lot more, changing the inherited class will be the way I go for optimization, but for now this is the method I made:

public void callGlobalMethod(String methodName, object[] perameters = null){
    //get all enabled game object in the scene
    GameObject[] allGameObjects = GameObject.FindObjectsOfType<GameObject>();

    foreach (GameObject gameObject in allGameObjects) {

        //get all monobehaviours on the game object
        MonoBehaviour[] scripts = gameObject.GetComponents<MonoBehaviour>();
        foreach(MonoBehaviour script in scripts){

            //get method info (makes it possible to call private methods)
            MethodInfo methodInfo = script.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);

            //if the method exists in the Monobehaviour
            if(methodInfo != null){

                //try-catch if parameters don't match up
                try
                {
                    //run the method with optional parameters
                    methodInfo.Invoke(script, perameters);
                }
                catch (Exception e)
                {
                    Debug.LogError("Error when calling global method: " + e);
                }
            }
        }
    }
}

I will edit this post when I inevitably optimize this sketchy chunk of code (: