Search Unity

Discussion Using static variable as constructor workaround hack

Discussion in 'Scripting' started by olejuer, Nov 25, 2022.

  1. olejuer

    olejuer

    Joined:
    Dec 1, 2014
    Posts:
    211
    Hi,

    I am thinking about hacking my way around the limitation that I cannot initialize MonoBehaviour through a constructor. Usually you would do this through Awake and Start. However, this is a problem when I need the MonoBehaviour to execute functionality in Awake or OnEnable that depends on data passed from the outside.

    My use case is a hit-detection object that uses Physics.CheckSphere in OnEnable. The object might be destroyed in the same frame, so Start() and Update() are never called. I need to set some properties, e.g., a reference to a causing entity, before the actual hit is detected. So I was thinking of passing that data through static properties and then cache them in Awake.

    This will work, but it feels hacky. I was wondering what other design choices I have to solve this. Ideas?

    This is an outline of the concept:

    Code (CSharp):
    1.  
    2. public class Entity : MonoBehaviour
    3. {
    4.     [SerializeField] private HitDetector _hitDetector;
    5.     public int HitCount { get; set; }
    6.     public void Hit()
    7.     {
    8.         HitDetector.InitCause = this; // set initialization data before Instantiate
    9.         Instantiate(_hitDetector);
    10.         if (Random.value < 0.5f) Destroy(_hitDetector.gameObject); // I want the hit to be detected anyway.
    11.     }
    12. }
    13.  
    14. public class HitDetector : MonoBehaviour
    15. {
    16.     [SerializeField] private float _radius;
    17.     public static Entity InitCause { get; set; } //  hacky static property to hold initialization data
    18.     private Entity _cause;
    19.  
    20.     private void Awake()
    21.     {
    22.         _cause = InitCause; // cache data so the static properties can be used for the next initialization
    23.         if (Physics.CheckSphere(transform.position, _radius))
    24.         {
    25.             _cause.HitCount++; // need cause data here. Cannot wait until Start().
    26.         }
    27.     }
    28. }
    29.  
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,935
    Does it have to be a monobehaviour though?

    I don't see anything here that couldn't be done with a plain class.
     
  3. olejuer

    olejuer

    Joined:
    Dec 1, 2014
    Posts:
    211
    Yes, it does. I added the random check to illustrate that the HitDetector gameobject might persist.
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,935
    Even then, you could instantiate a plain object and register it to a collection. If it needs to be updated, you can just walk the collection each update. When you no longer need, just pull it from the collection.
     
  5. olejuer

    olejuer

    Joined:
    Dec 1, 2014
    Posts:
    211
    It's a gameobject and can have other components, such as a renderer.
    Yes, if it didn't need to be a monobehaviour, I wouldn't have the problem of initializing monobehaviour ;)
     
  6. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,188
    I haven't tested the flow, so maybe it's a timing issue. But, once you instantiate the object, instead of doing all that stuff in Awake, couldn't you just have a method that you call on it? I know instead of using constructors, I usually have an Init method which I can call on the script to initialize any values, thus functioning like a constructor.

    So, instantiate, get the component, call init with the values you want passed in, then have init trigger the other stuff that is currently in Awake.

    You could even possibly have the HitDetector check the random value and destroy itself if you wanted. (Just a thought on this last bit)
     
    olejuer and spiney199 like this.
  7. olejuer

    olejuer

    Joined:
    Dec 1, 2014
    Posts:
    211
    Yes, you are right, this is much better!
    I felt like from a design perspective the HitDetector should have the responsibility to check for hits on Awake, but in the scenario where it is destroyed in the same frame, the instantiating script should enforce it.
    I also instantiate HitDetectors from timeline clips where I cannot do this, though. But neither can I do any custom initialization from the outside, so I should have a separate script or a flag for this usecase.
     
  8. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    You can even make a base class for this to reuse the base functionality of the init:
    Code (csharp):
    1.  
    2. public abstract class MonoBehaviourInit<T> : MonoBehaviour
    3. {
    4.    public abstract void Init(T value);
    5.  
    6.    // TODO: Add for other versions of Instantiate
    7.    public MonoBehaviourInit<T> InstantiateInit(Transform parent, T value)
    8.    {
    9.       MonoBehaviourInit<T> instance = Instantiate(this, parent) as MonoBehaviourInit<T>;
    10.       instance.Init(value);
    11.       return instance;
    12.    }
    13. }
    14.  
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
  10. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    I think that trying to compensate for a missing c-tor is an XY problem.

    Why not make a dedicated static class whose job is either to maintain data persistence or to mediate between objects that should talk? It's basically a non-allocating "factory". You supply it with a target reference (MB in your case), and it knows what to do with it. I typically call these things populators or initializers.

    If you really want the Init method (because some states could be private) this doesn't affect this design, because the initializer's job is to know how to inject data into an existing MB. This allows you to have multiple ways of setting data, and also a clipboard behavior if you need it, without bloating the actual data object.

    You simply treat your MBs as someone else's objects, as Kurt pointed out. The resulting code will look non-standard, but that's because it's supposed to look non-standard. It's not ugly either.
     
    Kurt-Dekker likes this.