4
\$\begingroup\$

I'm currently using the following general pattern for most of my configurable components (i.e. MonoBehaviours):

public class MyComponent : MonoBehaviour
{
    // Configurable fields
    [SerializeField]
    private bool someParameter;
    [SerializeField]
    private int anotherParameter;

    // Fake constructor to act as a replacement for GameObject.AddComponent
    // with additional parameters.
    public static MyComponent AddTo(GameObject gameObject, bool someParameter, int anotherParameter)
    {
        var myComponent = gameObject.AddComponent<MyComponent>();

        myComponent.someParameter = someParameter;
        myComponent.anotherParameter = anotherParameter;

        return myComponent;
    }

    // Public properties and other methods
    // ...
}

This works around the lack of constructors for MonoBehaviours but still encapsulates the parameters in such a way, that they can only be set at the time the object is created, or through the Inspector (which is effectively the same thing once the game is built).

Unfortunately, this approach doesn't help once I want to use my component inside a prefab. When I instantiate the prefab, the component has already been created, and I can no longer touch those parameters. And as far as my google-fu can tell, there's no way to pass parameters into prefab instantiation (much like there isn't a way to pass a parameter into AddComponent). It seems like everyone just instantiates their prefabs and then fiddles with some public properties to configure them.

So the question is, is there any way to set things up, such that I can configure some fields on my prefabs, but without exposing them publicly and breaking encapsulation?

(I realise that I could add a similar static "factory" method which instantiates a specific prefab and then configures the component inside that prefab, but a) this means adding one such method for each prefab and b) it breaks down once I want to configure multiple components within the same prefab.)

\$\endgroup\$
1

1 Answer 1

0
\$\begingroup\$

A naive (simple) approach would be to simply have an object which encapsulates the state you want and store it in a static field, then access that field in Awake.

    public class MyComponent : MonoBehaviour {
      [SerializeField]
      private bool someParameter;
      [SerializeField]
      private int anotherParameter;

      public class InitInfo {
        public readonly bool someParameter;
        public readonly int anotherParameter;
        public InitInfo(bool someParameter, int anotherParameter) {
          this.someParameter = someParameter;
          this.anotherPatameter = anotherParameter;
        }
        void Apply(MyComponent c) {
          c.someParameter = someParameter;
          c.anotherParameter = anotherParameter;
        }
      }
      public static InitInfo OnAwake;

      //note: you may be able to use the constructor instead of Awake,
      //but you must not interact with Unity in any way (eg instantiating
      //other objects, modifying the
      //transform, adding other components, calling Mathf.Random,
      //etc.) because this may be called on another thread.
      //Assigning a few fields should be fine though. 
      //public MyComponent() {

      void Awake() {
        if (OnAwake != null) {
          OnAwake.Apply(this);
          OnAwake = null;
        }
      }
    }

and elsewhere:

  MyComponent.OnAwake = new MyComponent.InitInfo(true, 5);
  Instantiate(myPrefab);

Although this breaks down if you want to configure multiple instances of the same component type within the prefab. I’m not sure if that’s what you were referring to when you were talking about configuring multiple components?

Other options include using BroadcastMessage to send the information in after instantiation is complete, or a callback where the caller registers a special method to be called in Awake, passing in the current component, and returning the data necessary, although these are just variations on the same theme.

For example:

    public class MyComponent : MonoBehaviour {
      [SerializeField]
      private bool someParameter;
      [SerializeField]
      private int anotherParameter;

      public class InitInfo {
        public readonly bool someParameter;
        public readonly int anotherParameter;
        public InitInfo(bool someParameter, int anotherParameter) {
          this.someParameter = someParameter;
          this.anotherPatameter = anotherParameter;
        }
        void Apply(MyComponent c) {
          c.someParameter = someParameter;
          c.anotherParameter = anotherParameter;
        }
      }
      public static Func<MyComponent, InitInfo> OnAwake;

      //public MyComponent() {
      void Awake() {
        if (OnAwake != null) {
          OnAwake(this).Apply(this);
        }
      }
    }

and elsewhere:

  int i = 0;
  MyComponent.OnAwake = c => new MyComponent.InitInfo(true, i++);
  Instantiate(myPrefab);
  MyComponent.OnAwake = null;
\$\endgroup\$
8
  • 1
    \$\begingroup\$ Thanks, I guess this sort of works, but you're right that it doesn't work if the component appears multiple times in a single prefab, and I also wonder whether this amount of boilerplate code is actually worth it. Plus, I'd have to make sure I always set and unset the OnAwake correctly, which seems a bit unsafe, although I guess I could wrap that entire 3-line instantiation in another static method to avoid that. This is good food for thought though, I'll think a bit more about whether I can turn this into something more feasible. \$\endgroup\$ May 29, 2018 at 8:38
  • \$\begingroup\$ Also, doing the actual initialisation in Awake is interesting. It's too bad you can't do anything between Instantiate and Awake, but if I can defer setup until Start, I could just have some public properties which are copied over to private fields on Start and then never used again. That would also provide a way to configure the object without being able to mess with it afterwards. \$\endgroup\$ May 29, 2018 at 8:46
  • \$\begingroup\$ Actually, I can intercept the Awake call, provided I make sure the prefab itself is inactive. \$\endgroup\$ May 29, 2018 at 9:40
  • \$\begingroup\$ Actually, I think there is a call between Instantiate and Awake, which is the constructor. It does still get called, although it may happen on a different thread. If you’re just passing in some types to apply to fields this shouldn’t matter. \$\endgroup\$
    – Ed Marty
    May 29, 2018 at 12:11
  • \$\begingroup\$ Also note that my second suggestion does allow for multiple instances in the same prefab. \$\endgroup\$
    – Ed Marty
    May 29, 2018 at 12:13

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .