6
\$\begingroup\$

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 MonoBehaviours (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 in Start for example, which wouldn't happen until the next frame, or making users have to call some custom Initialize 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?

\$\endgroup\$
2
\$\begingroup\$

Disclaimer: I've only played very briefly with Unity, so cannot comment on accepted practice, or whether my comments are actually viable.

It seems to me that the only reason for your AddTo() method is to enforce the initialization parameter sets. However, this does not actually enforce construction in this way, because you are always free to just write gameObject.AddComponent<ContentButton>();

Could you not just keep AddComponent calls vanilla, and just initialize immediately afterward? By returning the instance from your Init() method you could easily chain calls to something like this:

var contentButton = gameObject.AddComponent<ContentButton>().Init(blah, blah);

You do lose your runtime debug check for active objects doing this, but this could be moved into a base implementation of Init() along with the awake check:

public class BehaviourBase : MonoBehaviour
{
    private bool isAwake;

    [Conditional("DEBUG")]   // Or whatever Unity equivalent is
    protected void Init()
    {
        if (this.isAwake)
            throw new InvalidOperationException("Cannot initialize component after Awake().");

        if (this.gameObject != null && this.gameObject.activeInHierarchy)
            throw new ArgumentException("Cannot add component to active game object.", nameof(this.gameObject));
    }

    [Conditional("DEBUG")]
    protected void Awake()
    {
        this.isAwake = true;
    }  
}

Which would make your ContentButton implementation something like:

public class ContentButton : BehaviourBase
{
    [SerializeField] private string contentPath;
    [SerializeField] private bool isActive;

    public ContentButton Init(string contentPath, bool isActive)
    {
        base.Init();

        // standard constructor like stuff
        this.contentPath = contentPath;
        this.isActive = isActive;

        return this;
    }

    private void Awake()
    {
        base.Awake();

        /* Perform any initialization logic here, like reading the file
           at contentPath. */
    }

    /* other MonoBehaviour callbacks omitted */
}
\$\endgroup\$
  • \$\begingroup\$ Thanks, I like that Conditional attribute, I'll see which compilation symbols Unity defines. One issue with separating the calls like this is that Init can't actually determine whether the component was added to an active object (the moment you activate the object, Awake gets called, so the first check would fail already; you couldn't ever get to the second check and have it fail). I realise that I can't actually prevent use of a plain AddComponent<>() call, but I guess the idea was to include AddTo to suggest the correct usage to the programmer. [tbc] \$\endgroup\$ – Martin Ender Jun 20 '18 at 11:01
  • \$\begingroup\$ I mean in principle there's nothing wrong with allowing the programmer to add an uninitialised component to an active game object... they'd just get the default values for the fields. But I was hoping the structure of AddTo and Initialize would make it clear that if they want to configure the component, they'll have to use AddTo or do something equivalent themselves. \$\endgroup\$ – Martin Ender Jun 20 '18 at 11:03
  • \$\begingroup\$ Couldn't you just swap the checks around to check for an active gameObject first? I understand what you were trying to achieve with the AddTo, but it just feels fairly clunky to me just to try and prevent something that, like you say, is perfectly legal. But, lIke I said, I'm no Unity guy. \$\endgroup\$ – GazTheDestroyer Jun 20 '18 at 11:15
  • \$\begingroup\$ That would lead to misleading exceptions when you add the component correctly but try to configure it after Awake. The real issue is that at the point of Init you can no longer tell how the component was added. What I'm considering now though is to add an extension method AddConfigurableComponent<T> to GameObject as an alternative to AddComponent which includes the check. (And then chain the calls with Init like you suggested.) This gives the programmer the option to choose whether or not to include the check when creating the component. \$\endgroup\$ – Martin Ender Jun 21 '18 at 11:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.