Search Unity

Be careful about run order of Awake functions

Discussion in 'Scripting' started by mahdiii, Jan 23, 2018.

  1. mahdiii

    mahdiii

    Joined:
    Oct 30, 2014
    Posts:
    799
    Hi
    I intend to explain why run order of Awake functions is vital because it can cause weird behaviours.
    I don't know the reason why unity executes Awake functions without any order.So if awake functions depend on together, in every execution you may see different behaviours
     
  2. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    6,434
    Lysander, Ryiah, SparrowsNest and 3 others like this.
  3. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    779
    You could use events to daisy-chain the initialization in the order you want I suppose, but it's far easier to just divide the job between Awake and Start as @LaneFox said.
     
    mahdiii likes this.
  4. mahdiii

    mahdiii

    Joined:
    Oct 30, 2014
    Posts:
    799
    yes but in singleton design pattern you initialize them in Awake. They can depend on together
    I know we can use the following ways:
    1- lazy initialization
    2- give orders with Edit->Script Execution order
    3- initialize them in one main entry point
    4- use service locator
     
  5. mahdiii

    mahdiii

    Joined:
    Oct 30, 2014
    Posts:
    799
    and why does unity implement them like this,random orders!
     
  6. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    6,434
    It is not random, as already stated. Study the links if it's not clear to you.
     
    lordofduct likes this.
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    It's not a random order.

    It's an undefined order.

    From my practice, the order appears to be the order of the GameObject's the components are attached to in the hierarchy, from top to bottom. Child GameObjects are called starting deep and backing out. (Execution Order taking precedence over this of course)

    But Unity has this order undefined, because they don't want you relying on any defined order. If they did define it, then they could never change it. If down the line they wrote a faster/efficient initialization algorithm, but required changing the order, by defining it they wouldn't be able to change.

    So instead they tell us users to not expect it to be in any particular order. And to use Awake/Start to do any ordering.

    In the case of Singletons... as you your self brought up, you have 4 alternative ways to deal with the ordering issues.

    Personally I have foregone singletons for the most part. In my latest version of spacepuppy I have only 1 true singleton left. And I have migrated everything else to a service locator system.
     
    Ryiah, Suddoha and mahdiii like this.
  8. mahdiii

    mahdiii

    Joined:
    Oct 30, 2014
    Posts:
    799
    So you say it is not random and in a build version or editor they are called the same?
    Because I see myself that my game was executed differently in build and editor
    In editor it was completely true and expected
    When I added the singleton in the execution order correctly, it was run appropriately in build as well!
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    Different builds may have different orders.

    My point is that it's not random.

    It's undefined.

    Unity doesn't just randomly select objects to initialize. There's a method to the madness behind the scenes. We just don't know WHAT that method is. And that method may vary from target platform to target platform. I can see different algorithms being used because different ones may be better optimized for specific platforms.
     
    RecursiveFrog likes this.
  10. mahdiii

    mahdiii

    Joined:
    Oct 30, 2014
    Posts:
    799
    I didn't change the platform (Always android). Thank you
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    Android is a different platform than the editor running on Windows/Mac/Linux.

    I'm also not saying that it *does* do this, I'm saying that I can understand *why it could* do this.

    In the end... it's undefined, so all of our guesses are the best we can say.
     
  12. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    6,434
    The solution is to design scripts that work and support each other in the given circumstances. The documentation explains that you are guaranteed to get every Awake() call before Start(), and you are guaranteed to get all Start() calls before any Update() call. You also have the option of forcing a sequence of scripts to get their calls via the Execution Order panel.

    That is enough structure to guarantee safe initialization and communication of scripts, you just have to design within those features.
     
  13. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,483
    This. You can't say that often enough. :D

    @mahdiii
    Even when Singletons are used, you can still resolve most of the dependencies if you use Awake and Start correctly.

    Anyway, similarly to @lordofduct, I also decided to design my modules and systems without singletons. There's at most just one singleton in my current framework and that's just for the common quick start (it's basically the application's root "container" or context that provides access to all modules / systems - either instantly or lazily, i.e. on demand). It doesn't even need to be a singleton.

    Early in the days I was kind of in love with singletons. It was my very first widely-used design pattern. It was easy to access the instance from literally everywhere (which is actually a problem), easy to code (there are some common pitfall in Unity but it's not difficult either) and people who read the code immediately understand what's being accessed there and can adapt that.

    But after
    1) further extensive research about the pattern and also relevant topics (such as an overuse / misuse of statics)
    2) various approaches to code singletons in a more flexible way
    3) much unit testing

    they always revealed the ugliness and pain that comes with singletons in big projects once they're introduced on higher layers. There are still ways to work around some of the major problems, but I personally wouldn't use these anymore.

    This also changed my mind about DI and IOC in general, as I first thought it was just one of these hypes and put it aside evily smiling at it. I still don't use DI frameworks (rather my own system which adapts a few ideas), but it's a powerful approach to eliminate some major design flaws when it comes to resolving dependencies.
     
    Last edited: Jan 23, 2018
    mahdiii likes this.
  14. passerbycmc

    passerbycmc

    Joined:
    Feb 12, 2015
    Posts:
    1,470
    whats so hard about this, setup the internal state of your object in Awake, then if you need to talk to other objects do it in Start
     
    SparrowsNest likes this.
  15. mahdiii

    mahdiii

    Joined:
    Oct 30, 2014
    Posts:
    799

    My problem was not about Awake Start, it was about OnEnable and Awake.
    See like below:
    Code (CSharp):
    1. public void OnEnable(){
    2.    CEventManager.GetInstance().AddListener("EventName",OnEvent);
    3. }
    4. public void OnDisable(){
    5.       CEventManager.GetInstance().RemoveListener("EventName",OnEvent);
    6. }

    You know that OnEnable can be executed before some Awakes(other Awakes that don't belong to the gameobject)

    Maybe it was better to write:
    Code (CSharp):
    1. public void Start(){
    2.    CEventManager.GetInstance().AddListener("EventName",OnEvent);
    3. }
    4.  
     
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    mahdiii and Suddoha like this.
  17. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    6,371
    This is the first time you've mentioned OnEnable in this thread. For future reference, perhaps you could describe the entire problem instead of expecting us to read your mind.

    It's true that A's OnEnable might be called before or after B's Awake if both are in the scene at the start of the scene. So if CEventManager's instance is null because its Awake hasn't been run yet, that OnEnable might break. As mentioned, you simply need to design within the system that exists.

    Perhaps something like this would work. This will add and remove the listener as designed in your first attempt, but will also work if this object runs OnEnable before CEventManager's Awake.
    Code (csharp):
    1.  bool listenerAdded = false;
    2. public void OnEnable(){
    3. if (CEventManager.GetInstance() != null) {
    4.    CEventManager.GetInstance().AddListener("EventName",OnEvent);
    5. listenerAdded = true;
    6. }
    7. }
    8. public void Start(){
    9. if (!listenerAdded) {
    10.    CEventManager.GetInstance().AddListener("EventName",OnEvent);
    11. listenerAdded = true;
    12. }
    13. }
    14. public void OnDisable(){
    15.       CEventManager.GetInstance().RemoveListener("EventName",OnEvent);
    16. listenerAdded = false;
    17. }
     
    Ryiah, KWaldt, mahdiii and 3 others like this.
  18. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    I will point out... this is similar to a conversation we all had in another thread.

    Mahdii you have a knack for leaving out key information about the conversation (like the use of OnEnable, this is the first you mention it in this conversation). This confuses everyone as you talk about things in strange absolutes that don't apply to the context up to the point you mention new information.
     
    Ryiah, LaneFox and StarManta like this.
  19. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,483
    Oh, you did not say that explicitly in your first post. Now that's some information you should have added earlier.

    That's true, but a simple "yes, do it like that" is not always sufficient. You may still want to add/remove when enabling/disabling the component.
    There's a little more to that but it's not that tricky either, I've seen some solutions already on the forums.

    *EDIT
    Just saw @lordofduct has already linked his approach. You may wanna look at that.
     
    mahdiii likes this.
  20. Lumpazy

    Lumpazy

    Joined:
    Apr 24, 2018
    Posts:
    5
    How nice would it be if there was :
    Awake (unspecified order)
    Awoken (now all are awake and can talk)
    OnEnable (now i need to do something)
    Start ..
    Why specifically does Unity do OnEnable before Start ?
    Usually Onenable will be needed often. With values that may depend on other scripts having executed.
    So why iniztialize in start ???
    Or at least ... as above. Make a prewarmup = awoken...
     
  21. RecursiveFrog

    RecursiveFrog

    Joined:
    Mar 7, 2011
    Posts:
    149
    Because you can’t start something if it isn’t enabled. As far as Unity is concerned, anything that isn’t enabled can be treated as not participating in the engines lifecycle. If it’s not in the lifecycle it cannot Start.

    Likewise, disabling something doesn’t destroy it. It’s simply taken out of the lifecycle for a time. Re enabling won’t cause a reawaken or a restart.
     
  22. smallstep

    smallstep

    Joined:
    Jan 25, 2016
    Posts:
    28
    I'm in the same situation, with a (single) "AppServicesHolder" singleton of which the Awake runs *after* an MB's OnEnable that tries to use the singleton.Both are root GOs inside the "loading..." scene.

    What I did was to put the singleton prior to the "Default Time" in editor: scripts execution order. However I would love to give a better (yet simple and future-proof) solution to this issue. Any ideas?
     
  23. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    779
    Daisy chaining delegate events is probably the only way for something completely in C#. I'd say the Unity way is more convenient since there's lesser setup, whether its future proof or not, hard to say.
     
    smallstep likes this.
  24. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    I'm running into a similar situation. I have 3 components: Health, BaseStats and Experience.

    1. XP 
      variable of the Experience component is restored from a save file after Awake but before Start.
    2. BaseStats calculates the
      currentLevel 
      in start based off the
      XP 
      from Experience.
    3. Health then calculates the starting health based off the
      currentLevel
      .
    My problem is with steps 2 and 3. If I put them both is Start then the health might get calculated before the
    currentLevel 
    is ready. But I can't put 2 in Awake because the save file state won't be restored yet.

    I was considering the following guidelines for solving this issue more generally:
    1. You cannot call another components methods in Awake.
    2. If you use another components method in Start, you must first call that component's Start method.
    This seems to fix the issue in my case above and seems like a pretty good guideline with less boilerplate than lazy initialisation or chaining of events.
     
  25. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,051
    Standard pattern to avoid ordering bugs is to restrict object access.
    1. In Awake/OnEnable script allowed only access itself.
    2. All talks to other scripts are made in Start and later.
     
  26. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    Doesn't help if you have a chain of initialisation to do like my example.
     
  27. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,051
    Rephrase your initialization algorith to meet those two rules and it will work.
     
  28. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,483
    None of the above helps if it some point, you'll talk to
    2) is critical for various reasons. Start is defined to run only once and follows some certain rules, a manual call could easily mess things up, and you should not need to implement an "start has already be run" flag.

    Generally, when you have initialization steps that have to run in sequence and appear to be dependent on each other in some way, it's usually easier to make some sort of controller that controls the access, the activation and initialization process and possibly the communication between multiple components.

    This makes those components a little more passive, but suppose the fetching-operation of the values becomes slower so that you take the decision of asynchronous loading. At this point, you'll have to come up with a different approach anyway.

    Another advantage is that you can avoid direct coupling between component types, because you have class that acts as middle man for the communication. You can also use those controlling components to effectively stay highly flexible, because you can always introduce new components without editing the existing components.
    The latter is great because often enough, making lots of components depend directly on others leads to very bloated components, which keep growing because there's a need for one feature that's only used here, another that's only used there...
     
    Ryiah and lordofduct like this.
  29. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    This, a whole lot of this.
     
  30. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    True, I was just considering making a separate Init method that can support being reentrant. That init could be called in Start but also before the class is used in another MonoBehaviour.

    Maybe I'm a little on clear on how the controller would work. I wonder if you would be able to give a more concrete example?

    I imagine you mean an object responsible for setting up components in order and injecting there dependencies and potentially calling init methods on them when there dependencies are ready.

    I don't quite get how it would reduce coupling. Don't you still need to have those classes talk to each other? If they don't, won't that make the controller into a bit of a god class that knows too much about all the subsystems?

    Thanks for the reply, good to get input on other ways folks are solving these issues.
     
  31. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    Instead of Health retrieving BaseStats, it instead is handed the information it needs by the controller class. Thusly decoupling Health from BaseStats. (Same goes for BaseStats from Experience).

    Also, this is not what a "god object" or "god class" is. A god object is when a single class/object does too much on its own. This is an anti-pattern that breaks the "single responsibility prinicipal". The controller class being suggested to you doesn't do a bunch of things... it does a single thing: "injects the necessary data needed by the various stat/ability scripts of the entity it's attached to".

    In actuality, by having your Health do BOTH locating BaseStats as well as configuring itself and doing whatever else Health does makes it more a god object. It is responsible for more things than are necessary.

    The reason this is a problem is for scalability. What if down the line you needed to have a Health script whose initial values are configured not based on 'BaseStats' but on some other data... you now have to refactor Health script to handle both BaseStats AND the other data type... or refactor the configuration job out and move that task elsewhere (effectively creating the controller Suddoha is suggesting).
     
  32. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    Yup that's what I thought was meant. I can see that the controller makes the injection of data easier to change without refactoring. However, it also makes it harder to add new data. Suppose I want to pull in more data in health from another component. Now I have to change both the controller and health script to achieve this.

    Another question is what level should the controller sit at? Would I put it at the level of the player in which case it would have to change whenever any of the dependencies change in any of the player's components change. That seems like a violation of the SRP. Or do I make it specifically the controller between Health, BaseStats and Experience in which case the choice of these classes is a bit arbitrary and might later need to be change if other classes need to depend on each other.

    Generally all for minimising direct dependencies and providing just what's needed. Demeters law and all that. But maybe I've been in Unity land too long as I'm struggling to see a nice way to achieve this without the above issues.
     
  33. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    Also, my Health component relies on the BaseStats component later on in its lifecycle too, not just in initialisation. It registers for events and restores health based on the values returned from BaseStats at runtime. So I think a hard dependency there is justified.
     
  34. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,483
    @lordofduct has already given the answer to that, so let's think about your example.

    Suppose you have Health Component, and the Experience / Level component as mentioned in your example.
    So yes, in your current project, it may be a given fact that health and other stats depend on the experience / level.

    The question is - why should it?
    Health (in its very own little world) can be implemented in a way that it knows nothing about the experience. Health for enemies, would they even need experience at all? Or a wall?

    Does health itself even require behaviour?

    What if experiences changes, all the stats dependent on it have to be updated. So you'd have all of them either poll the experience or wait for a level up event - each instance would do so. That's questionable, but could be considered "still acceptable".

    Sounds convenient I guess. But what if there's a special bonus for certain levels? Would that go into the health component? If so, it'd need to handle very specific behaviour and responsibilities.

    Next, what if you decide to add a health multiplier (temporary boosts, permanent boosts etc)? Well, from my experience I'd bet that this one will surely make it into the Health component. The problem: once again, the health component becomes more specific. It suddenly needs to track boosts.

    We could come up with infinite additions that will most-likely all go into the health component, so it will either grow or be subclassed or anything alike.

    The next thing to address:
    Loosely coupled components are great, but if you only ever talk to a "Health component", you're forced to either have a "health god component" that takes everything into account that's relevant for computing the new health value, or overridden methods for something simple as health, that you'll re-implement for every specific case / variation - or, maybe something that knows all the details? A component that wraps all these details?

    Example: A character that has it's usual health, some bonuses and a shield for damage absorption, some resistance , perhaps even invincibility skills.

    Your task, as the programmer: Apply some damage.

    1) Health takes all of the above into account: Invincibility, Shield, Resistance, Health+Bonus
    => here's your god class
    Not good at all.

    2) The component that applies damage takes all of that into account
    => Easy to introduce bugs, because you have to query all the components every time you want to apply damage, calling them in sequence and somehow get the logic right.
    Not that great either.

    3) Speak to some type of component that represents a specific entity, or for this matter, coordinates "a set of components". This can be a very specific class, because that's what assembles a complex entity in your application.
    => Only tell that component what you wanna do, and it can take everything into account as an "implementation detail". In this case, you can keep your self-contained components, and your controlling class turns them into something bigger.
    Seems reasonable (in my opinion).
     
  35. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    I'll just offer up a very basic implementation of what I believe your design is shooting for:
    Code (csharp):
    1.  
    2.  
    3. public class HealthMeter : MonoBehaviour
    4. {
    5.  
    6.     #region Fields
    7.  
    8.     [SerializeField]
    9.     private float _currentHealth;
    10.  
    11.     [SerializeField]
    12.     private float _maxHealth;
    13.  
    14.     #endregion
    15.  
    16.     #region Properties
    17.  
    18.     public float CurrentHealth
    19.     {
    20.         get { return _currentHealth; }
    21.     }
    22.  
    23.     public float MaxHealth
    24.     {
    25.         get { return _maxHealth; }
    26.     }
    27.  
    28.     #endregion
    29.  
    30.     #region Methods
    31.  
    32.     public void Configure(float currentHealth, float maxHealth)
    33.     {
    34.         _maxHealth = maxHealth;
    35.         _currentHealth = Mathf.Clamp(currentHealth, 0f, _maxHealth);
    36.     }
    37.  
    38.     public void Strike(float damage)
    39.     {
    40.         _currentHealth = Mathf.Clamp(_currentHealth - damage, 0f, _maxHealth);
    41.         //maybe dispatch an event
    42.     }
    43.  
    44.     public void Heal(float heal)
    45.     {
    46.         _currentHealth = Mathf.Clamp(_currentHealth + heal, 0f, _maxHealth);
    47.         //maybe dispatch an event
    48.     }
    49.  
    50.     #endregion
    51.  
    52. }
    53.  
    54. public class BaseStats : MonoBehaviour
    55. {
    56.  
    57.     public int CurrentLevel;
    58.     public float MaxHealth;
    59.     public float Strength;
    60.     public float Dexterity;
    61.     //etc
    62.  
    63.     public void ConfigureFromExperience(int experience)
    64.     {
    65.         //set currentlevel and stats based on the experience points
    66.      
    67.         //this method doesn't necessarily have to be here... it could be performed by some factory...
    68.         //this way the factory can be swapped out based on the character. Allowing for different leveling paths based on the factory.
    69.     }
    70.  
    71. }
    72.  
    73. public class Experience : MonoBehaviour, IPersistentDataMessage
    74. {
    75.  
    76.     public string Id;
    77.     public int ExperiencePoints;
    78.  
    79.     void IPersistentDataMessage.Save(SaveDataToken token)
    80.     {
    81.         token[Id + "*Experience"] = this.ExperiencePoints;
    82.     }
    83.  
    84.     void IPersistentDataMessage.Load(SaveDataToken token)
    85.     {
    86.         ExperiencePoints = (int)token[Id + "*Experience"];
    87.     }
    88.  
    89. }
    90.  
    91. //a message that can be broadcast to obejcts telling them to save their data into
    92. //some token container which will then be saved to disk or loaded form disk
    93. public interface IPersistentDataMessage
    94. {
    95.     void Save(SaveDataToken token);
    96.     void Load(SaveDataToken token);
    97. }
    98.  
    99. public class PartyMemberController : MonoBehaviour
    100. {
    101.  
    102.     public Experience Experience;
    103.     public BaseStats Stats;
    104.     public HealthMeter Health;
    105.     //you maybe could put here the 'type' of party member it is so that the factory I referred to can be determined
    106.  
    107.     private void Start()
    108.     {
    109.         //we wouldn't necessarily dispatch the load message here...
    110.         //but I'm including this just to show that the load message should have already occurred prior to configuring health/stats
    111.         MessagingSystem.Dispatch<IPersistentDataMessage>(this, o => o.Load(SaveManager.SaveToken));
    112.      
    113.         //make sure our stuff is configure correctly
    114.         Stats.ConfigureFromExperience(Experience.ExperiencePoints);
    115.         Health.Configure(Stats.MaxHealth, Stats.MaxHealth);
    116.     }
    117.  
    118. }
    119.  
    Note that in my code I reference a messaging system, a save system, and a factory. I didn't go into fleshing those aspects out and instead put adhoc code about the place just to show the principal of the matter. Those systems should be designed and fleshed out on your own terms.

    I'm not saying this is necessarily how you should do it... but this is the sort of design I'd probably start at and scale from there.
     
  36. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    Thank you both lordofduct and Suddoha for your indepth replies. I decided to have a go at refactoring towards a controller for the Character/Player/Enemy entities.

    First it tried removing all direct dependencies between Health, BaseStats and Experience. This lead me to realise there is some functionality differences between an Enemy and Player as the former has no Experience. So I created a polymorphic solution with Player and Enemy classes inheriting from Character. Links to Github are included for the actual implementations I tried.

    I don't like the inheritance trap here. My entities might not nicely categorise in the future this way. If I add an NPC, that shares some behaviour with Player but some with Enemy, what do I do? So I reverted the polymorphic aspect and decided to flesh out just the Character controller class. I added into the mix the Fighter class which also has dependencies on BaseStats. You can see that the class is becoming large and would have to change for many different reasons.

    So finally I stripped it all back and justed used the Character class for dependency injection. This is the approach that I liked most. But it still creates a class with dependencies on many different subsystems. Which leads me to wonder: if I'm just using this class for dependency injection, why not use GetComponent and Unity GameObjects as the dependency injection mechanism?
     
  37. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    I'd like to start by apologizing for not going into depth explaining the code I posted yesterday. I was at work and was writing that in my spare time.

    So... my motivations in my design I showed.

    HealthMeter - note that HealthMeter has no dependency to BaseStats. The reason for this is that it doesn't matter how 'Configure' on it gets called. It might get configured statically in the editor for an enemy that doesn't level... or heck for a destructible item in the world. Otherwise it could be configured based on stats like BaseStats (such as how PartyMemberController does it). Or maybe there is a completely different way... like maybe there's MobController that sets its health to always be 2x the player's health. The point is HealthMeter doesn't know, nor care, how that is all determined. All it cares is that SOMETHING configures it. By reducing the jobs it needs to do, it allows HealthMeter to be used anywhere. Then if you have anything that impacts the health of any entity you just say:
    Code (csharp):
    1. entity.GetComponentInChildren<HealthMeter>().Strike(damage);
    BaseStats - so in my example I just have a really dumb method that configures it based on experience points. But really... I wouldn't go about it that way. I mention a factory. The factory would configure BaseStats based on whatever that specific factory likes... you might have one that is based on experience points and has a leveling curve. Another might again be based on the player (I've played many RPGs where a boss's stats are based on like 3x player stats to a maximum of N). Again, BaseStats doesn't know how it got its values... it merely exists as a container to read/write the current stats (a buff system can be built around this which ALSO modifies the stats). This again makes BaseStats modular, any and all entities that need stats just get it. And anything that does logic and needs it can retrieve it. For example you might have an AOE that does:
    Code (csharp):
    1.  
    2. private void OnTriggerStay(Collider other)
    3. {
    4.     var health == other.GetComponentInChildren<BaseStats>();
    5.     if(health == null) return; //can't hurt something that has no health
    6.     var stats = other.GetComponentInChildren<BaseStats>();
    7.  
    8.     float dmg = this.DamageAmount;
    9.     if(stats != null) dmg *= CalculateDamageMultiplier(stats);
    10.  
    11.     health.Strike(dmg * Time.deltaTime); //it's a damage over time AOE
    12. }
    13.  
    Things like PartyMemberController would just be the central piece that coordinates all these components together in a way that describes that sort of entity.

    It decides the kind of factory is used to set the stats. It configures the health. It defines how these modular parts integrate together. It's "single task" is to "relate the entities parts as a whole be defining their relationship with one another".

    I would avoid inheritance chains in defining this. At most I might have an "IEntityController" type (or to work around the annoying unity hating serializing refs by interface... a base abstract class with zero implementation, just abstract property/method definitions).

    The upside to this is that a lot of your entities will probably have hierarchies of gameobjects... its hitbox collider might actually be inside the entity so the above code 'GetComponentInChildren' may actually fail. So instead, and this is how I do it in my games, you can say something like:
    Code (csharp):
    1. var entity = otherCollider.GetComponentInParent<IEntityController>();
    2. var health = entity.GetComponentInChildren<HealthMeter>();
    This is all embracing the 'component/composite pattern'. None of the scripts don't necessarily know that each has some specific component (maybe you have a 'Death/Grim Reaper' enemy, so it gets no health... all the code logic out there still works regardless... only thing is AOE no longer hurts it, but maybe your buffs still do since it has BaseStats).

    So when you say:
    It's not just for dependency injection... it's defining how those dependencies are resolved.

    A PartyMember gets its stats factory based on the experience of that party member.
    But the GrimReaper gets its stats factory as a "everything maxed out" configuration.
    A 'BasicMob' gets its stats statically defined via the editor when you create its prefab.
    A 'MercinaryPartyMember' (a party member you can temporarily hire) gets its stats factory based as a scale of the player's experience (that scale could be calculated as some multiple based on how much money you paid for them, or what town they came from)

    The concept of dependency injection isn't just the injection of a dependency. It's abstracting the idea of how dependencies are resolved, and then injected.

    ...

    And back to the original problem, and how this benefits. It means Health isn't waiting for BaseStats to initialize so it can initialize. The controller defines when it gets initialized, the health doesn't concern itself with that (especially if it doesn't NEED to be initialized). No more race condition there, since the race is no longer defined by "Unity's deserialization/load processs" which is arbitrary... but instead the race is defined by your "entity controller". Something that makes sense and isn't arbitrary, since you have control over its ordering.
     
    Last edited: Jun 21, 2019
    sampattuzzi and Lysander like this.
  38. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    Great post, thanks so much for replying. I think you've gone above and beyond with your reply. I've certainly got something to mull over and chew on. Maybe my different entities need themselves some controllers. But perhaps I only need to do dependency injection on the classes that really need configuration dependencies at this stage. The rest can be left to self configure.

    Do you have any best practices for which classes you choose to configure in the controller or how you group your controller classes?

    Two concerns still outstanding:
    1. Adding a new dependency to a class requires a change to two or more files. The class itself and any controllers that might be using it. I suppose this is acceptible and will be flagged by the compiler if you change the
      Configure
      signature.
    2. I don't think I can get away with not passing the BaseStats to Health. At the moment health configures itself with BaseStats but also reference the max health whenever asked for percentage health. It also gets notified of level up events and receives a health bonus.
    But that aside I will consider this approach a bit harder. I think that part of my problem might just be that deserialisation of the save file doesn't happend till after Awake. It this could happen before or give another callback before Start (something like "DeserialisationFinished") then it might be possible to solve a lot of these dependency issues that way. But I think I'm rediscovering the idea of using a Controller class anyway.
     
  39. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,609
    Yes.

    Your 'ScriptThatHasDependency' and 'TheScriptThatInjectsTheDependency' will have to change. Though this sort of thing shouldn't happen very much... You may want to ask yourself why this script is gaining a new dependency? What is causing this change? There's no reason I can think of that Health would need any additional dependencies. If it is, it's probably taking on too many new roles... breaking that 'single responsibility principle'.

    See this is where I can see Health is gaining a new dependency... because Health has received new jobs on top of being a health script. Why should Health need to register for an event that notifies it when the entity levels up? What if your Health is attached to something that doesn't level up? It's now doing work it doesn't need to. The concept that health increases when a level increases isn't on the HealthMeter, it's on the Entity who has the HealthMeter. That's a behavioural aspect of the entity, not the health.

    Think of it like this... lets replace Health with "Rechargeable Battery". A battery can be told to recharge, but it doesn't recharge itself. It's up to whatever is using the battery to recharge it. The battery just has the ability to be recharged. Furthermore, if you want to upgrade your battery giving it a new maximum, it again isn't the batteries job to upgrade itself... it's again the job of whoever is using the battery to upgrade (due to the real world limitations this would mean replacing the battery).

    So why should Health know about BaseStats? So it can know its MaxHealth? When you configure it, tell it its MaxHealth. That's now its MaxHealth regardless of what BaseStats says. What if you wanted to buff your Health and give a 50 hp bonus while wearing the 'gauntlets of good will'... are you going to now have to go into HealthMeter and write logic that not just checks BaseStats but also checks for inventory equipped and buffs attached and every possible way its MaxHealth is increased??? Talk about a god class!

    Why is it not instead that when the 'Gauntlets of Good Will' are equipped, the healthmeter is just told "your new maxhealth is X".

    ...

    With all that said, this is just the way I look at the problem. It's technically not the only/best way to do it. It's what I consider to be my preferred way of approaching the problems outlined so far in this thread.
     
    Last edited: Jun 21, 2019
  40. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    Yeah this does make sense. It might well be too much given the enemies don't level up.

    Maybe base stats isn't the best name. It actually aggregates all bonuses already. So all you have to do is ask it for the stat and it will give it with all bonuses applied. I personally still think this is neater than caching maxHealth in the Heatlh component. But it could be distrupted with an interface potentially.
     
  41. sampattuzzi

    sampattuzzi

    Joined:
    Apr 21, 2016
    Posts:
    9
    I think controllers are a great idea in general. But I've also come up with this solution that helps with values that need initializing after
    Awake
    .

    First of all I have a custom wrapper for the state called
    LazyInit
    . It allows me to declare a variable like this in
    Awake
    :
    Code (CSharp):
    1. currentWeapon = new LazyInit<Weapon>(InitDefaultWeapon);
    Then I can use that
    currentWeapon
    anywhere in the knowledge that it will be initialized on first use.

    If I need it initialized before rendering I do this:
    Code (CSharp):
    1. private void Start() {
    2.     currentWeapon.ForceInit();
    3. }
    The full code for
    LazyInit
    is here:
    Code (CSharp):
    1. public class LazyInit<T>
    2. {
    3.     private T _value;
    4.     private bool _initialized = false;
    5.     private InitializerDelegate _initializer;
    6.  
    7.     public T v
    8.     {
    9.         get
    10.         {
    11.             ForceInit();
    12.             return _value;
    13.         }
    14.         set
    15.         {
    16.             _initialized = true;
    17.             _value = value;
    18.         }
    19.     }
    20.  
    21.     public delegate T InitializerDelegate();
    22.  
    23.     public LazyInit(InitializerDelegate initializer)
    24.     {
    25.         _initializer = initializer;
    26.     }
    27.  
    28.     public void ForceInit()
    29.     {
    30.         if (!_initialized)
    31.         {
    32.             _value = _initializer();
    33.             _initialized = true;
    34.         }
    35.     }
    36. }