Search Unity

Why is Unity missing an OnCreate event?

Discussion in 'Scripting' started by TheHeftyCoder, Nov 1, 2020.

  1. TheHeftyCoder

    TheHeftyCoder

    Joined:
    Oct 29, 2016
    Posts:
    91
    The Awake event only happens if the said gameobject is active at the start-up time. This means that it cannot work as an initialization event, since it is not always guaranteed to happen; in the simple case that you want that specific gameobject to be disabled on instantiation.

    Is there any specific reason this happens? And if so, why isn't there a guaranteed function that initializes the script? Or is there and I can't seem to find it?

    Example: Suppose you have a script that needs another component of the same gameobject to work. Furthermore, that initial script receives a message from another script to call a specific method. If, at the time the script receives its message, Awake has not been called due to it being inactive, then the component that it needs to work on hasn't been set up and you we get exceptions.

    Possible Solutions:
    1) Have a manager of sorts for that family of scripts and call a virtual Initialize() function.

    2) Use SerializedField and attach them through the editor.

    3) Use ISerializationCallbackReceiver, which is the only thing that actually gets called on start up every time.

    4) Use a dependency injection framework (Extenject)

    Out of all these, 1 and 2 I find redundant and unnecessary. 3 is actually what we might want, but it was certainly not built for this purpose and 4 might add complexity for no reason (Leaving aside that I haven't used it and do not know if it actually solves the problem).

    Does anyone know of a better solution? Because if there isn't, an OnCreate event of sorts seems like an easy thing to implement.. And I've encountered this problem one too many times already.
     
    NotaNaN and neonblitzer like this.
  2. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    I've found the main problem is realizing that Awake won't be called on things that start disabled. Realizing that is 90% of the solution. It could be in top 10 Unity-gotchas lists. Having OnCreate would only help with that a tiny bit (people might see it and think "how is that different from Awake? Ohhh... .").
     
  3. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    Code (CSharp):
    1. public class MyClass : MonoBehaviour
    2. {
    3.   MyClass() : base()
    4.   {
    5.     Debug.Log("constructor");
    6.   }
    7. }
    Edit: forgot the
    : MonoBehaviour
    part, added
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,739
    I take your point, given that we can't make constructors on MonoBehaviors.

    However, I have rarely found this limiting in practice.

    If you're loading stuff to turn on later, I figure you already need some kind of manager to control that, so why not make everything instantiated Active, then deactivate what you don't need. Non-lazy object poolers would do this I imagine, but I tend to make my poolers lazy af. :)

    One pattern I really like to use in order enforce and structure my code-created objects is the basic factory pattern. In a Unity context it looks something like:

    Factory Pattern in lieu of AddComponent (for timing and dependency correctness):

    https://pastebin.com/euGb7t3k

    Also I highly recommend NOT using dependency injection with Unity. It just plays badly with the lifecycle of the editor, IMNSHO, and anyone who is used to "the Unity way" will be baffled and bamboozled with DI.
     
    TheHeftyCoder and Bunny83 like this.
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,739
    Are you sure that is kosher with Unity? It definitely did not used to be but perhaps my info is out of date.
     
  6. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    I don't know - I haven't needed to do this, but it came to mind as a possible solution so I just tried and it worked in Play Mode. I haven't tested in a build.

    Edit: Worth mentioning - when I first started learning Unity I was using destructors, but had issues freeing memory from ~MyClass() until I switched to OnDestroy().
     
    Kurt-Dekker likes this.
  7. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    Once upon a time, when I tried tried doing some stuff in a MonoBehaviour constructor, I discovered that Unity calls the constructor multiple times per object (at least in the editor). Maybe related to the fact that Unity uses the constructor to figure out default values for serialized fields; maybe some other weirdness in the Unity lifecycle.

    It's my belief that it's fine to use MonoBehaviour constructors for basic self-initialization (stuff like myListVar = new List), but you want to avoid anything that would have side-effects outside the object, as well as anything that would have a noticeable performance cost. And I also avoid most of the UnityEngine namespace since presumably the rest of the scene might not have been created yet.

    On the occasions when I've had a MonoBehaviour that needed to do initialization before the first time it was used and where Awake was not guaranteed to have run by that point, I ended up creating an internal flag to keep track of whether initialization was done and testing on every incoming call whether that flag had already been set.
     
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,739
    Interesting... makes sense. I think for that kinda reason (as well as what Antistone added above) alone I would avoid using constructors. Besides, pretty sure I read once "DO NOT DO THIS," and there certainly is lots of net discussion about it:

    https://answers.unity.com/questions/862032/c-constructor-in-monobehaviour.html

    When I am given an API (part of the API is the use of AddComponent), I do try to stay within it, just because I can happily create enough of my own bugs so that I don't really want to incur bugs in Unity that might confound what I'm doing. :)
     
    adamgolden likes this.
  9. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    I've been avoiding them as well, I don't think I use any - but there have been times I've had the same desire for something like an OnCreate, just always ended up finding another way. Still, the only answer re OP I can think of is a constructor.. if that's best to avoid at all costs, then I can't think of another way to do it (aside from just suggesting to come up with an approach to the problem that won't need anything sooner than Awake).
     
    Kurt-Dekker likes this.
  10. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,739
    The other thing I seem to recall is when you load scenes / prefabs, or call Instantiate, or call .AddComponent<>(), the constructor(s) are NOT called on the main thread but rather on the Unity loader thread, so there are very few things you can do in the ctor() in any case.
     
    adamgolden likes this.
  11. TheHeftyCoder

    TheHeftyCoder

    Joined:
    Oct 29, 2016
    Posts:
    91
    Holy crap, that actually works? Well I'll be damned for never searching for constructors in Unity! It seems to have the same faults as the ISerializationCallbackReceiver, but it's cleaner none the less and can probably work correctly with some flags. I think this might do for now...

    I have been using the factory pattern for some years but I've recently switched mindsets when it comes to that. I now generally prefer to have an object be solely responsible for its initialization and only have a manager for external dependencies. To me, being unable to have the same behavior as SerializeField on the creation of a component is "criminal".

    Take this example. I have a component that requires a TMP_Text. Therefore I know that a TMP_Text is present on the object and decide that I don't want to use a SerializeField, because it costs more, in disk and in set-up. Trying to do GetComponent<TMP_Text>() on Awake() though is inconsistent, since Awake might not be called if the gameobject starts as inactive. Although this could be solved with a lazy getter, I think we can agree that it's a weird solution.
     
    CodeRonnie and neonblitzer like this.
  12. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Rather edgecase that you want stuff called for a disabled Gameobject. Also stuff like that are probably better suited for the creator not the created.
     
  13. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    If you really need this you can fix it with

    Code (CSharp):
    1.     public static T Instantiate<T>(T prefab) where T : Object, ICreated
    2.     {
    3.         var obj = Object.Instantiate(prefab);
    4.         obj.OnCreated();
    5.         return obj;
    6.     }
     
    TheHeftyCoder and Bunny83 like this.
  14. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    How can I use that? I just tried pasting it into a class inheriting MonoBehaviour and it said "The type or namespace ICreated could not be found" - I searched for ICreated figuring I was missing a using but there were no results. I've seen code like that before but never worked with it, can you explain its use/integration? I feel like now is right the time to acquire some knowledge about this T stuff :rolleyes:
     
  15. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Code (CSharp):
    1. public interface ICreated
    2. {
    3.    void Oncreated();
    4. }
    Google generics. Its a must have tool for any C# developer. Basicly here I say that T must be a Unity Object and ICreated. Other than that it can be of any type.
     
    Bunny83 likes this.
  16. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555
    A quick search and skimming a "for dummies" kind of article later.. it does seem useful. It's also a bit much.. I woke up this morning thinking I already knew everything there ever was to know about everything ever, and now this!! :(
     
    seejayjames and MDADigital like this.
  17. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Btw I think its best practice anyway to abstract stuff like this from unity in your domain. You can eaay add logic in a generic way this way.
     
  18. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    You seem to be assuming that the object is going to be created dynamically from a script. That version of the problem is easy.

    The hard version is when the object is part of your scene, created in the Unity editor, but starts out disabled. In this case, there is no "creator" script for you to piggyback on.
     
  19. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    But someone needs to enable that object for it to be awake. Who is that?

    It sounds like bad seperation and low cohesion if you run into problem with Awake not being fired for disabled objects, but their might be valid cases, but not many of them.
     
  20. TheHeftyCoder

    TheHeftyCoder

    Joined:
    Oct 29, 2016
    Posts:
    91
    The reason I made this post was to argue that Unity isn't consistent on its initialization. When I make a component, I sometimes expect things to be initialized despite Awake not running, i.e a List<T> which you can initialize on declaration. The same thing cannot be said for a component, because GetComponent<T>() cannot be called on declaration. Having the same object that you get through GetComponent as a SerializeField WORKS though. Doesn't that ring a bell that their design is missing something?

    Personally I've encountered the problem many times. I'm making a framework for board games and tend to work component-wise a lot, so that may also be a reason why it's happening to me all of the time.
     
  21. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    But activating the object is something that might happen multiple times (unlike creation, which must only happen once). If an object is being repeatedly activated and deactivated, you don't want to call OnCreate for every activation.

    More importantly, other scripts might be calling functions on the component even before it is active, and those functions might need the component to be initialized.



    Simple example:

    Suppose I have a UI window in my game, and in this window there is a smaller panel that opens or closes based on some game event. (Maybe the window is the player's status screen, and the extra panel opens if the player is currently poisoned in order to warn about the poison.)

    However, the window isn't visible at all times, and when hidden, its root GameObject is inactive (implicitly deactivating all children in the hierarchy, including the panel). The window starts inactive when the game loads, so Awake isn't called until some unknown future time.

    The game event that opens or closes the panel doesn't currently know or care whether the window is currently visible, or whether it has ever been visible since the app started; it just tells the specific panel to open or close. The panel needs to carry out that instruction even if the window is hidden, so that it will be in the correct position the next time the window is revealed.

    The panel has some initialization that needs to happen before the first time it opens or closes (but that shouldn't happen every time it opens or closes). Notice that means it might need to be initialized even if it has never been active, and still isn't going to become active any time soon. Where does that initialization go?
     
  22. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Awake only triggers once. After that you need to use OnEnable etc
     
  23. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    You have completely missed the point. We're not trying to do something on every activation, we're trying to do initialization exactly once. The problem with Awake isn't that it only runs once, it's that it doesn't run early enough.
     
  24. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    My one issue with "Awake doesn't run is disabled" was also a display that started hidden. My solution was to break it into an empty parent with the script, always enabled, and a child with the visible part, flipping active/inactive on the child as needed. I think I did it at first to fix a scale/origin issue, and realized it also fixed my Awake problem.
     
    seejayjames likes this.
  25. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    "But activating the object is something that might happen multiple times"

    No thats not true, it only happens once., After that OnEnable is triggered when reactivaing a object
     
  26. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    I've legitimately never run into the "Awake problem" (I wasn't even aware of this edge case until reading this thread!), and this pattern is essentially why. As a rule I don't tend to deactivate things except via scripts. Anything that starts out disabled is always managed by something else. I think this habit came from how many Unity events are lost when things are disabled, so I trained myself to move all critical components onto things that are never disabled.
     
    TheHeftyCoder likes this.
  27. seejayjames

    seejayjames

    Joined:
    Jan 28, 2013
    Posts:
    691
    I typically don't enable/disable GO's much either. If they need to be invisible and/or have their colliders off, just turn those off. But the parent/child approach works well too. I did that with a "Settings" menu that pops up, so everything in that Canvas is under a parent GO. But I didn't enable/disable, I just moved it in and out of the viewable area...can't recall why I did it that way exactly, but it's another option.
     
  28. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    I've found moving a canvas out-of-the way never works. It always manages to creep into view of some other camera which can see it. I once had a tiny yellow square stuck in a scene until I figured out it was another canvas camera able to see some "hidden" canvas in the distance. Disabling canvases seems safest. Except I like to put UI scripts on Canvas so I can find them. That would be a problem, except I use the same child trick. For organizational purposes, I almost always put the elements in empty RextTransforms and disable/enable those, leaving the Canvas always active.
     
  29. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Or dont let the item itself be responsible for showing/hiding it self. A bit strange seperation anyway

    I often let the item be acrtive and then disable from code. For example

    Code (CSharp):
    1.             template = GetComponentInChildren<TabItem>(true);
    2.             template.gameObject.SetActive(false);
    Code (CSharp):
    1. public class TabMenu : MonoBehaviour
    2.     {
    3.         private TabItem template;
    4.  
    5.         public TabItemConfig[] Tabs;
    6.         private readonly List<TabItemInstance> tabItemInstances = new List<TabItemInstance>();
    7.  
    8.         public ColorBlock Colors;
    9.  
    10.         private TabItemInstance selected;
    11.  
    12.         protected virtual void Awake()
    13.         {
    14.             template = GetComponentInChildren<TabItem>(true);
    15.             template.gameObject.SetActive(false);
    16.  
    17.             foreach (var config in Tabs)
    18.             {
    19.                 var instance = Instantiate(template);
    20.                 instance.gameObject.SetActive(true);
    21.  
    22.                 instance.Image.sprite = config.Icon;
    23.  
    24.                 instance.transform.SetParent(transform, false);
    25.  
    26.                 var item = new TabItemInstance
    27.                 {
    28.                     Tab = instance.GetComponent<Image>(),
    29.                     Config = config,
    30.                 };
    31.                 tabItemInstances.Add(item);
    32.  
    33.                 item.Tab.gameObject.AddComponent<EventTrigger>()
    34.                     .triggers.AddRange(new ValueTuple<EventTriggerType, Action>[]
    35.                     {
    36.                         (EventTriggerType.PointerClick, () => SetActive(item)),
    37.                         (EventTriggerType.PointerEnter, () => SetActiveColor(item, true)),
    38.                         (EventTriggerType.PointerExit, () => SetActiveColor(item, item == selected))
    39.                     }.Select(h =>
    40.                     {
    41.                         var et = new EventTrigger.Entry
    42.                         {
    43.                             eventID = h.Item1
    44.                         };
    45.                         et.callback.AddListener(o => h.Item2());
    46.                         return et;
    47.                     }));
    48.             }
    49.  
    50.             SetActive(tabItemInstances.First());
    51.         }
    52.  
    53.         private void SetActive(TabItemInstance activate)
    54.         {
    55.             foreach (var item in tabItemInstances)
    56.             {
    57.                 var active = item == activate;
    58.  
    59.                 SetActiveColor(item, active);
    60.                 item.Config.Panel.gameObject.SetActive(active);
    61.                 if (active)
    62.                 {
    63.                     selected = item;
    64.                 }
    65.             }
    66.         }
    67.  
    68.         private void SetActiveColor(TabItemInstance item, bool active)
    69.         {
    70.             item.Tab.color = active ? Colors.highlightedColor : Colors.normalColor;
    71.         }
    72.  
    73.         private class TabItemInstance
    74.         {
    75.             public TabItemConfig Config { get; set; }
    76.             public Image Tab { get; set; }
    77.         }
    78.      
    79.     }
     
    Last edited: Nov 3, 2020