MonoBehaviours DO support constructors

Oliver Oliver • Published 2 years ago Updated 2 years ago


Quite often, I come across people who are under the impression that a class which inherits MonoBehaviour cannot or should not define a constructor. If you are one of those people then buckle up. I'm going to dispel this myth once and for all.

The myth

I stumbled across an article which attempted to explain this myth, but it has some glaring issues with it. Let's just yoink that code and show it here.

using UnityEngine;

public class ConstructorTest : MonoBehaviour
{
    public ConstructorTest()
    {
        Debug.Log("Constructor is called");
    }

    private void Awake()
    {
        Debug.Log("Awake is called");
    }

    private void Start()
    {
        Debug.Log("Start is called");
    }
}

We have a class named ConstructorTest which logs to the debug console in 3 separate situations:

  • In the constructor
  • In Awake
  • In Start

If we hit Play, we see the following output:

Hm, yes, I also like to spread misinformation.

So, in a way, yes the constructor is kind of being called twice - but the article seems to completely ignore some vital information regarding this behaviour.

They are separate instances

To start off, I need to clear the air and say that the constructor is not actually called twice. In fact, it is impossible for that to happen. The constructor is called once - when we use the new operator (or when using Activator.CreateInstance - which is along the same lines as Unity is doing), and after that the instance simply exists. You cannot* call the constructor again.

* Well - you can with reflection - but let's not get into that mess.

We can prove this by keeping track of each instance's ID to differentiate them, as well as the Unix timestamp at which the instance was created. We'll output these values with each Debug.Log call.

using System;
using System.Runtime.Serialization;
using UnityEngine;

public class ConstructorTest : MonoBehaviour
{
    <mark>private static readonly ObjectIDGenerator IDGenerator = new();</mark>
    <mark>private readonly long _instanceId;</mark>
    <mark>private readonly long _createdAt;</mark>

    public ConstructorTest()
    {
        _createdAt = DateTimeOffset.Now.ToUnixTimeMilliseconds();
        _instanceId = IDGenerator.GetId(this, out _);
        Debug.Log($"Constructor is called for instance #{_instanceId} ({_createdAt})");
    }

    ~ConstructorTest()
    {
        Debug.Log($"Finalizer is called for instance #{_instanceId} ({_createdAt})");
    }

    private void Awake()
    {
        Debug.Log($"Awake is called for instance #{_instanceId} ({_createdAt})");
    }

    private void Start()
    {
        Debug.Log($"Start is called for instance #{_instanceId} ({_createdAt})");
    }
}

You'll notice I've also added a finalizer (also known as a destructor), so that we can see when objects are being garbage collected.

Immediately after compilation, before we even hit Play, we see the following:

Nothing out of the ordinary here.

Unity created a new instance and called the constructor. This is necessary because the object must be serialized to exist in the scene - even during edit mode. Unity must know what field values to display in the inspector, and so the behaviour must exist in some form so that it can read those initial values. This is normal and expected.

Now, hit Play, and you should see the following:

Instance Zero is rumoured to be where the first segfault originated.

What's interesting to me is that we have an instance 0 which got collected. I've spent a little while trying to figure this out, but with the Unix timestamp returning 0 - I'm completely lost. I don't know to which instance this is referring, so I'm going to just ignore it for now. I'll come back to this another time.

The second output is Finalizer is called for instance #1 - this is the instance which existed during edit mode. We can see that it's the same one, because of the timestamp of its creation. It was garbage collected when we launched the game. I'll explain why in just a moment!

The next output is Constructor is called for instance #1 - but heads up, this is misleading! This is actually a completely disparate instance - which we've proven with the creation timestamp. When we hit Play, Unity reloaded the app domain and essentially caused all static values to reset. This particular instance #1 now refers to a fresh instance in Play mode.

Okay, so we have an instance of our class in the game.

Then we see the constructor is called yet again - but this time it's for instance #2. We can also see that no extra logic is performed on instance #1 - Awake and Start are only called on instance #2. So instance #2 is the actual object that exists in the scene, and is the instance on which Unity messages are called.

So what's up with #1?

Instances are created for serialization purposes

As we've proven, the constructor is not actually called twice. It's only called once per instance, it's just that two instances get created. The reason for this is simple: serialization.

Unity needs to create a temporary instance in order to retrieve the default values of serialized fields. We know this because when you stop the game, anything that was applied during Play mode gets undone. Objects move back to their original positions, anything that was “Instantiated” gets destroyed, and all values that were serialized (e.g. values that you can see in the inspector) get reset.

If we stop the game, we can see that the constructor is called again. We get an entirely new instance, because Unity needed to instantiate the class in order to know what values the fields should be reset to:

The real question is, why didn't Unity immediately clean up instance #1?

Your first thought might be to say “couldn't it use the values from #1?” - and, well, yes it could - but it shouldn't. It doesn't know what happened to instance #1 during the interim of gameplay. For all Unity knows, I could have started a timer in the constructor which altered the state of the class; ultimately leading Unity to receive incorrect information about the initial state. That'd be pretty dangerous behaviour. A third instance was created so Unity can fetch values before anything like that even remotely has a chance to happen.

Field initializers

Another point I'd like to make on this, is that if you don't define one - a public parameterless constructor is generated for you by the compiler.

Let's assume you have a behaviour with a serialized field - and you initialize it to some value:

using UnityEngine;

public class ConstructorTest : MonoBehaviour
{
    [SerializeField] private bool _isAlive = true;
}

Let's take a look at the compiled result:

.class public auto ansi beforefieldinit
  ConstructorTest
    extends [UnityEngine.CoreModule]UnityEngine.MonoBehaviour
{

  .field private bool _isAlive
    .custom instance void [UnityEngine.CoreModule]UnityEngine.SerializeField::.ctor()
      = (01 00 00 00 )

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    // [5 22 - 5 51]
    IL_0000: ldarg.0      // this
    IL_0001: <mark>ldc.i4.1</mark>
    IL_0002: <mark>stfld        bool ConstructorTest::_isAlive</mark>
    IL_0007: ldarg.0      // this
    IL_0008: call         instance void [UnityEngine.CoreModule]UnityEngine.MonoBehaviour::.ctor()
    IL_000d: nop
    IL_000e: ret

  } // end of method ConstructorTest::.ctor
} // end of class ConstructorTest

Okay, so if you don't understand IL, I'll break this down for you.

On line 6 we have a .field declaration. This is where we declare _isAlive to exist in the class. You can see that the assignment of true isn't here - that's because IL doesn't support field initialization this way.

Below that, we have the SerializeField decoration for the field. This part is not important here.

Below that, we have a method named .ctor - this was generated by the compiler! This is what happens when you don't define a constructor for a class; the compiler generates a public parameterless one by default on your behalf.

In the constructor, on line 17, we have the instruction ldc.i4.1. This stands for “load constant integer with size 4 bytes with value 1”. Essentially, this pushes 1 (aka true) onto the stack. The next instruction is a stfld, which stands for “set field” or “store field” - whichever you fancy - and is where the assignment of _isAlive is happening.

If we transpiled this back into C#, we see the field initializer for what it really is. I've highlighted the assignment in both the IL codeblock above and the C# codeblock below.

using UnityEngine;

public class ConstructorTest : MonoBehaviour
{
    [SerializeField] private bool _isAlive;

    public ConstructorTest()
    {
        <mark>_isAlive = true;</mark>
    }
}

What this means is when you initialize a field to some value, what it actually compiles to is an assignment in the constructor anyway. This is how C# works.

Adding your own logic

Because of the very nature of how field initializers work, we can take advantage of this and define any logic we want in the constructor. We could even have something like the following:

using System.Collections.Generic;
using UnityEngine;

public class ConstructorTest : MonoBehaviour
{
    [SerializeField] private List<int> _numbers;

    public ConstructorTest()
    {
        <mark>_numbers = new List<int>();</mark>
        <mark>for (int i = 0; i < 10; i++)</mark>
        <mark>{</mark>
        <mark>    _numbers.Add(i + 1);</mark>
        <mark>}</mark>
    }
}

If we viewed this behaviour in the inspector, we'd see that it does - in fact - execute the loop and populate the list:

Works like a charm, actually.

I believe the whole origin of the “MonoBehaviours don't support constructors” myth stems from the fact that you cannot perform any Unity-related logic in them. For example, the following code:

using UnityEngine;
 
public class ConstructorTest : MonoBehaviour
{
    private Vector3 _startPosition;
 
    public ConstructorTest()
    {
        <mark>_startPosition = transform.position;</mark>
    }
}

Will throw the following error in the console:

UnityException: get_transform is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call it in Awake or Start instead.

This is because at the moment of the constructor being called, Unity has not finished serialization - nor has it finished initialization of the component. It's not possible for transform (or any Unity related things) to have a value here. Unity is - in its most basic form - doing something like this:

// we are accessing transform.position in the constructor
// before the transform property even has a chance to be set

var instance = new ConstructorTest(); // constructor is called
instance.transform = /* new transform component */;

Obviously it is not exactly like this. But this is the most basic version of what is happening; thus the error we see above. But plain ol' C# / .NET logic? Not an issue. Unity has no control or say over what happens outside of its own API.

Why that third instance matters for serialization

During gameplay, the state of the class obviously changes a whole lot. Player health goes down, inventory items get added, scores increase. But when you stop the game, that data doesn't persist.

Let's demonstrate that with a coroutine which will remove elements from the list, one by one, every second.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConstructorTest : MonoBehaviour
{
    [SerializeField] private List<int> _numbers = new();

    public ConstructorTest()
    {
        for (int i = 0; i < 10; i++)
        {
            _numbers.Add(i + 1);
        }
    }

    private void Start()
    {
        StartCoroutine(RemoveElements());
    }

    private IEnumerator RemoveElements()
    {
        while (_numbers.Count > 0)
        {
            _numbers.RemoveAt(0);
            yield return new WaitForSeconds(1);
        }
    }
}

Play the game, and you'll see the following:

Bye bye, numbers.

You'll notice that when we stop the game, the list is repopulated using the values provided by that third instance which Unity created.

By adding logic in the constructor (the for loop as in my example), we're providing Unity with the initial state we want. We want that list to be populated by default, and so we definitely want that constructor here. This is intended behaviour!

Static fields are not serialized

To finish off this post, I thought it would be interesting to show how we can break Unity's serialization. You've probably heard, at some point, that static fields are not serialized by Unity.

To show what this really means, consider this behaviour:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConstructorTest : MonoBehaviour
{
    private static readonly List<int> StaticNumbers = new();
    [SerializeField] private List<int> _numbers = new();

    static ConstructorTest()
    {
        for (int i = 0; i < 10; i++)
        {
            StaticNumbers.Add(i + 1);
        }
    }

    private void Start()
    {
        StartCoroutine(RemoveElements());
    }

    private void Update()
    {
        _numbers.Clear();
        _numbers.AddRange(StaticNumbers);
    }

    private IEnumerator RemoveElements()
    {
        while (StaticNumbers.Count > 0)
        {
            StaticNumbers.RemoveAt(0);
            yield return new WaitForSeconds(1);
        }
    }
}

We have a static list of numbers which gets populated in the static constructor.

Inside Update, we're simply updating the instance _numbers list to display those numbers in the inspector.

And finally we have a coroutine which - when the game launches - removes elements one-by-one.

If we hit Play, we see similar behaviour as before. Although _numbers is initially empty, it's populated on the first frame and we see the values slowly being removed as time passes.

If we stop the game, and play again, we notice the same thing happening. This isn't because Unity serialized the static field, it doesn't know about the static field. The reason this happens is because of what I mentioned earlier - the app domain gets reloaded.

This is Unity reloading the app domain.

When this happens, all static variables are reset. The entire environment is sandboxed, which prevents anything too drastic from happening in the long-run.

However, we can override this. If we go to the Editor tab in our Project settings, we have the option to disable domain reloading:

You shouldn't do this unless you know what you're doing.

Now when we run the game, we'll truly see that the state of the static list is not serialized. It persists across different Play sessions because the app domain was not reloaded. If we let the list empty out, stop, and restart the game, nothing else happens:

Welp, might as well die.

The list was cleared of all entries and now we are stuck with that state until we either:

  • Restart Unity
  • Recompile any script (this forces the domain to reload anyway)
  • or re-enable the Reload Domain option

So the next time you see the “Reload Script Assemblies” dialog and complain that it's taking too long, be thankful - it's doing you a huge favour.




3 legacy comments

Legacy comments are comments that were posted using a commenting system that I no longer use. This exists for posterity.

Spencer Tofts Spencer Tofts • 2 years ago

This was an incredibly helpful post. Being able to actually use field initializers (as opposed to needing to initialize every map and list in Awake()) will make my life quite a bit easier. Thanks for writing it!

Viễn Nguyễn Viễn Nguyễn • one year ago

Nice post, actually i was kinda lost too when i saw Unity seems to create “extra” instance when first started in play mode. After talking with a colleague, he told me this may happened because of the serialization system on Editor. The theory is maybe Unity do some save/load (serialize/deserialize) stuff on the current scene u open on Editor before pressing play. You can check on built game or load a new scene while in play mode. To summarize, it will happen to Editor + first Scene load only.

Lion Lion • 10 months ago

Nice article, it does makes thing a bit clearer to look at. Unity really sometimes is a mystery of its own, I really wished we had access to source code. Nevertheless, great read!