I've been grappling with Unity's architecture for the last few days, trying to work around the fact that you can't use constructors for your MonoBehaviour
s (and by extension, you can't easily configure components before Awake
if you create them programmatically). I've got a pattern now that I'm fairly happy with, but before I get to the code, here are a few things I'm trying to solve with it:
- Keep self-initialisation code in
Awake
, because that's where Unity developers would expect it, and it also ensures that the component is usable as soon is its game object is active. (As opposed to putting initialisation code inStart
for example, which wouldn't happen until the next frame, or making users have to call some customInitialize
method which they might forget.) - Make certain component fields configurable in all three forms of component creation: a) in the Inspector, b) when adding a component to a game object, c) when creating a prefab which contains the component somewhere in its hierarchy. In any case, this configuration needs to happen before the self-initialisation which will likely depend on these values.
- Otherwise, hide those fields from the outside, so that other code can't mess with the values after self-initialisation is complete (unless there's an explicitly exposed setter, of course).
- Statically control possible sets of initialisation parameters (like the signatures of available constructors would do).
The main concession I had to make to achieve those goals is that my object creation pattern can only be used when adding components to inactive game objects (because otherwise, Awake
gets run before I can do anything). In practice, that's a restriction I can easily work with in my project.
Anyway, here is a complete component implementing the pattern:
using System;
using UnityEngine;
namespace MyProject
{
public class ContentButton : MonoBehaviour
{
[SerializeField] private string contentPath;
[SerializeField] private bool isActive;
private bool initialized = false;
public static ContentButton AddTo(
GameObject gameObject,
string contentPath,
bool isActive)
{
if (gameObject.activeInHierarchy)
{
throw new ArgumentException("Cannot add component to active game object.", nameof(gameObject));
}
var contentButton = gameObject.AddComponent<ContentButton>();
contentButton.Initialize(contentPath, isActive);
return contentButton;
}
public void Initialize(string contentPath, bool isActive)
{
if (initialized)
{
throw new InvalidOperationException("Cannot initialize component after Awake().");
}
this.contentPath = contentPath;
this.isActive = isActive;
}
private void Awake()
{
/* Perform any initialization logic here, like reading the file
at contentPath. */
initialized = true;
}
/* other MonoBehaviour callbacks omitted */
}
}
The idea is that you use MyComponent.AddTo(gameObject, ...)
instead of gameObject.AddComponent<MyComponent>()
to create single components and that prefabs can be configured by instantiating them with an inactive parent and then calling Initialize
on the relevant components before switching the lights on.
So first of all, is there anything wrong with this pattern on its own? Are there easier or more idiomatic ways to achieve the above goals?
The thing is that this is a lot of boilerplate code which I'd need to repeat on every single component in my project, which is just really prone to messing things up. I'd like to have a (potentially abstract) BaseComponent
class which inherits from MonoBehaviour
and from which all of my actual components inherit. That base class would then implement and/or enforce this pattern for all of its derived classes. A lot of the pattern should be the same on all derived classes (e.g. the entire body of AddTo
, the exception in Initialize
and the initialize = true;
at the end of Awake
). The body of Initialize
is also essentially completely determined by the list of fields I want to expose, because this method should never do more than setting up the fields in the Inspector.
I'd normally enforce things like this with the template method pattern, but I'm not sure how to apply it in this case, where the signatures of AddTo
and Initialize
depend on the derived class. Is there any clean solution for this?