Chris McCole

View Original

Static Classes within Unity Game Engine

One thing that I did frequently when I first started programming in Unity was to make manager classes, that had to be placed within the first loaded level, were set to DontDestroyOnLoad, and were set up as Singletons (containing a public static reference to their own object, only ONE of these may exist at a given moment). Then when I wanted to access the save function, I did something like SaveManager.Instance.Save(); When working in other scenes, you need to make sure that an instance was created and loaded, since it was most likely placed in just the first scene. This frequently causes annoyances where you need to load it, check if it’s loaded, wait for it to initialize, put it in every scene, dynamically load scenes, destroy extra instances of the script, etc, and that’s only relevant in the testing environment. When booting the game from start, you know that you will have it and it will be setup, it’s only in the testing environment that any of this matters, it’s quite annoying and frequently causes grievances for things I want to exist throughout the entire execution of the program. This is when I started using static classes as opposed to static references to a Monobehaviour instance. The main difference here is that all the functions and variables contained must be static or const (for variables), and a static class CANNOT be Monobehaviour, however, that never causes me any trouble.

Now that I’ve gotten into this groove of using static classes, I use them ALL over the place for any kind of Utility functionality, or overarching managers such as Save Managers. One thing that’s nice about static classes, particularly with save managers, is that you can write and call editor functionality that will clear or write save data for you with the click of a button, in play mode or not. I’ve also setup a load of static utilities and helpers. I have a static class that contains delegate references, or variations of WaitForSeconds, so I don’t need to dynamically allocate them at runtime. I also have converters for things like PercentAsBool, that simply performs the calculation { return percent>= 1f; }. I have many UI helpers, for converting world-space to screen-space, lerping color values, toggling canvas groups, generating radial positions, check if the mouse is over a UI element, etc. I can access any of these simply like so: “UIHelpers.ToggleCanvasGroup(ref canvasGroup);” These functions are very general purpose and I have a whole collection of them that I package up together, host on git, and import into all of my projects! When I make updates to the package, it carries over to all of my projects for when I develop more useful tools! You can see the git package here: https://github.com/cjm399/Unity_UtilityImports and you can read about this utility creation process here, if you want to do something similar with your own code: https://www.chrismccole.com/blog/how-to-create-unity-libraries-for-use-across-your-projects

So now you can see how they are useful and how you can interact with them, but how do you actually make one? It’s actually quite simple, you simply define a class as you normally would, sans the Monobehaviour, and then add the static keyword like so:

public static class StaticClass
{
}

Easy right? Now, from anywhere in your code, you should be able to reference the StaticClass instance! Let’s say you wanted to create a CanvasGroup Toggler as I mentioned above, how would that be done?

using UnityEngine;
using System.Runtime.CompilerServices;
public static class StaticClass
{
     public const MethodImpl.MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;

    [MethodImpl(INLINE)]
    public static void ToggleCanvasGroup(ref CanvasGroup cg)
    {
        bool isVisible = !(cg.alpha >= 1);
        cg.alpha = (isVisible ? 1 : 0);
        cg.blocksRaycasts = isVisible;
        cg.interactable = isVisible;
    }
}

Here, you can see we added the ToggleCanvasGroup function, and now, given a canvas group, it will automatically toggle it on/off without us having to track it. We’ve also added a const variable to allow us to help tell the compiler to try and inline the CanvasGroup function if possible. The inline does not change functionality and is not important for this problem, but this is how I have my code setup, you can feel free to omit that portion of the code.

Now from some other class we could call StaticClass.ToggleCanvasGroup(ref myCanvasGroup); and sure enough it would toggle on/off whenever we call the function! Pretty swell considering I used to write functionality like this by hand ALL the time, particularly in Start if I wanted to make sure that the canvas was off. Speaking of which, we could also override this function if we want to be able to specify it’s end state like so.

using UnityEngine;
using System.Runtime.CompilerServices;
public static class StaticClass
{
     public const MethodImpl.MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;

    [MethodImpl(INLINE)]
    public static void ToggleCanvasGroup(ref CanvasGroup cg)
    {
        bool isVisible = !(cg.alpha >= 1);
        cg.alpha = (isVisible ? 1 : 0);
        cg.blocksRaycasts = isVisible;
        cg.interactable = isVisible;
    }

    [MethodImpl(INLINE)]
    public static void ToggleCanvasGroup(ref CanvasGroup cg, bool isVisible)
    {
        cg.alpha = (isVisible ? 1 : 0);
        cg.blocksRaycasts = isVisible;
        cg.interactable = isVisible;
    }
}

Now we could alternatively call StaticClass.ToggleCanvasGroup(ref myCanvasGroup, false) to ensure that it is off on the first frame of the game!

Great! Well, now we have some basics, but what if I still need to initialize some attributes inside of my class on start? Now that we are not using Monobehaviour, we nolonger have access to monobehaviour functions such as Awake, OnEnable, Start, Update, Fixed Update, OnDestroy, etc. Well in terms of initialization, there are actually C# attributes that you can mark your classes or functions with to make sure they run at initialization, the most common example of this in my case, is I have a random number generator in which I cache all the values on start, and they need to be generated before someone can get a random number, so how do we do this? Well, if we want something at runtime to run on Awake, similar to how it would work in Monobehaviour, we could use the attribute [RuntimeInitializeOnLoadMethod]. This works in runtime, for builds and play mode. Here is an example:

[RuntimeInitializeOnLoadMethod]
public static void InitializeCachedRandomValues()
{
    for (int i = 0; i < RAND_INT_COUNT; ++i)
    {
        CachedRandomIntegers[i] = Random.Range(0, MAX_RAND_INT_VAL);
    }
    InitializeStartIndex();
}

Now, when the game boots up, you will have access to all of the CachedRandomIntegers right off the bat! No need to call an Initialization function on them from another monobehaviour. What about in the Editor though? You might want random values in your editor functionality, well, you can make a class that is using the [InitializedOnLoad] attribute, here is an example where I wanted random numbers to still appear in the editor, outside of play mode.

#if UNITY_EDITOR
    using UnityEditor;
    [InitializeOnLoad]
    public class RandomEditor
    {
        static RandomEditor()
        {
            Rand.InitializeCachedRandomValues();
        }
    }
#endif

Now I was able to use my initialized cached static function within the editor whenever I wanted (note that you don’t need this at all for these functions to work in editor, it’s purely so that the initialization can be ran in the editor without having to explicitly call it).

Unfortunately when it comes to things like Update, we are left out to dry a little, also for things like Coroutines, they need a monobehaviour in order for the Coroutines to run! Well, if that’s the case, then how did I mention that I was able to have a “lerp color value” function within my static utilities? Well, just because you need a monobehaviour, it doesn’t mean that your static class has to be, you can reference another Monobehaviour instead. This is the perfect example of using that [RuntimeInitializeOnLoadMethod] attribute.

What we need to do, is create a monobehaviour, save a reference to it, and then call our coroutines on that instance. We can accomplish the following like so!

public static class UIHelpers
{
    public class MyStaticMB : MonoBehaviour 
    {
    }
    private static MyStaticMB myStaticMB;

    [RuntimeInitializeOnLoadMethod]
    public static void Initialize()
    {
        if (myStaticMB == null)
        {
            GameObject gameObject = new GameObject("Static_UI_Coroutines");
            myStaticMB = gameObject.AddComponent<MyStaticMB>();
            UnityEngine.Object.DontDestroyOnLoad(gameObject);
        }
    }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void LerpImageColor(ref UnityEngine.UI.Image _img, ref AnimationCurve _animCurve, Color _start, Color _end, float _time)
    {
        myStaticMB.StartCoroutine(Internal_LerpImageColor(_img, _animCurve, _start, _end, _time));
    }

    private static IEnumerator Internal_LerpImageColor(UnityEngine.UI.Image _img, AnimationCurve _animCurve, Color _start, Color _end, float _lerpTime)
    {
        float timer = 0;
        float percentage = 0;

        while (percentage < 1)
        {
            percentage = (timer / _lerpTime);
            _img.color = Color.Lerp(_start, _end, _animCurve.Evaluate(percentage));
            timer += Time.deltaTime;
            yield return null;
        }
    }
}

Here you can see the three main pieces, we have the overarching static class UIHelpers, inside of which we define a new class that IS of type monobehaviour, we also have a reference to this monobehaviour. Then, inside of the Initialization function, we create a GameObject and give it the component of type MyStaticMB, and we set this to Don’t Destroy On Load, so that we know we will always have it. Then we define the hook for the user to interface with this function called “LerpImageColor” then in LerpImageColor, we tell our monobehaviour to actually do the work of running our coroutine. Now if you call UIHelpers.LerpImageColor(ref myImage, ref myAnimationCurve, Color.red, Color.blue, 3f); you can see your image lerp from red to blue, over 3 seconds using your curve specified!

Earlier, I mentioned functions such as Update that we nolonger have access to due to our lack of a Monobehaviour. Well, we could use this same solution to fix that, simply add an Update function the the MyStaticMB class, and call your custom static Update function. That would look like so:

Change the MyStaticMB definition to

 public class MyStaticMB : MonoBehaviour
    {
        private void Update()
        {
            UIHelpers.CustomTick();
        }
    }

And make sure that you have the public static CustomTick() function defined inside of your static class that you want to call. You could technically do this with any other Monobehaviour function if you wish, although I think Update is probably the most applicable.

You can extrapolate these techniques to any Utilities, Managers, Savers, or Helpers that you can think of. Once I started using them, I’ve never thought about going back, they are so convenient, and they generally don’t relate too much to my game-specific code which helps me to more easily create a library of reusable code. Best of luck and I hope this helps!


If you found this tutorial helpful and want to help support me in creating this content that I host and publish for free, please consider contributing to my Patreon or Ko-fi!

See this form in the original post