Search Unity

Discussion Should we be using coroutines to manage things like cooldowns or buff durations?

Discussion in 'Scripting' started by gaazkam, Nov 21, 2022.

  1. gaazkam

    gaazkam

    Joined:
    Mar 30, 2016
    Posts:
    24
    As far as I'm aware (correct me if I'm wrong), the standard recommendation to manage stuff like cooldowns or buff durations is to use coroutines like this:

    Code (CSharp):
    1. public float powerupDuration = 5;
    2.  
    3. private bool powerupActive = false;
    4.  
    5. private void EnablePowerup()
    6. {
    7.     powerupActive = true;
    8.     StartCoroutine(DisablePowerup());
    9. }
    10.  
    11. private IEnumerator DisablePowerup()
    12. {
    13.     yield new WaitForSeconds(powerupDuration);
    14.     powerupActive = false;
    15. }
    I can't help but it seems to me that this solution, while appealing, is horrible!

    Assume that the player will then pickup another powerup while the previous one is still active - then the above solution breaks. Well yes, we could hack something together like this:

    Code (CSharp):
    1. public float powerupDuration = 5;
    2.  
    3. private bool powerupActive => powerupStacks == 0;
    4. private int powerupStacks = 0;
    5.  
    6. private void EnablePowerup()
    7. {
    8.     powerupStacks++;
    9.     StartCoroutine(DisablePowerup());
    10. }
    11.  
    12. private IEnumerator DisablePowerup()
    13. {
    14.     yield new WaitForSeconds(powerupDuration);
    15.     powerupStacks--;
    16. }
    But this can soon become unwieldy if we then wish to add stuff like dispel abilities (that will have to take down all stacks instantly).

    Even worse, I can't see how this solution could ever enable us to put something like a progress bar, precisely giving the player the information about the remaining buff duration in real time.

    It would appear to me that instead of using coroutines we should, from the very beginning, manage such durations in Update():

    Code (CSharp):
    1. private float remainingBuffDuration = 0;
    2.  
    3. private void Update()
    4. {
    5.     remainingBuffDuration = Mathf.Max(0, remainingBuffDuration-Time.deltaTime);
    6.     UpdateBuffDurationIcon(remainingBuffDuration); // Displays remainig duration in real time
    7. }
    8.  
    9. public void ApplyBuff()
    10. {
    11.     remainingBuffDuration = 5; // Correctly handles stacking
    12. }
    As far as I'm aware this solution is disrecommended because it puts additional logic in Update, which might cause performance problems and framerate reduction. However, as I wrote above, it seems to me to be the only viable solution.

    Am I correct that the general recommendation is to use Coroutines for such purposes rather than putting logic in Update()?

    If this is the case then how to solve problems I described above and what am I failing to see?

    Thanks in advance!
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,911
    "Cooldown" is much too vague and much too bespoke of a concept to even have a general recommendation. Every game handles cooldowns differently and even does so differently for different things within the same game. And as you noted, every game has different requirements when it comes to things like displaying progress bars, or stacking abilities, or stacking cooldowns, etc...

    in any case, no Update is not the only viable solution for the things you mentioned at all, though I do thing it is a perfectly fine one, especially if there's only one of these objects. Coroutines can still be used for those things without issue. You just can't necessarily blindly use WaitForSeconds and StartCoroutine etc. without thinking.

    Let's take your example of picking up another powerup while the first one is active. I can think of at least two other ways (besides the one you used) to implement that using coroutines. For example:

    Code (CSharp):
    1. Coroutine powerupCoroutine;
    2. public bool PowerUpIsActive { get; private set; }
    3.  
    4. void PickupPowerUp() {
    5.   if (powerupCoroutine != null) StopCoroutine(powerupCoroutine);
    6.  
    7.   powerupCoroutine = StartCoroutine(PowerUp());
    8. }
    9.  
    10. IEnumerator PowerUp() {
    11.   PowerUpIsActive = true;
    12.   yield return new WaitForSeconds(cooldown);
    13.   PowerUpIsActive = false;
    14. }
    And here's another way, with a progress bar:
    Code (CSharp):
    1. Coroutine powerupCoroutine;
    2. public bool PowerUpIsActive => remainingTime > 0;
    3. float remainingTime = 0;
    4.  
    5. void PickupPowerUp() {
    6.   if (powerupCoroutine != null) StopCoroutine(powerupCoroutine);
    7.  
    8.   powerupCoroutine = StartCoroutine(PowerUp());
    9. }
    10.  
    11. IEnumerator PowerUp() {
    12.   remainingTime = 5;
    13.   while (remainingTime > 0) {
    14.     remainingTime = Mathf.Max(remainingTime - Time.deltaTime, 0);
    15.     UpdateCooldownUI(remainingTime);
    16.     yield return null;
    17.   }
    18. }
     
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    Says who? We're not allowed to use Update? In Unity???

    This is one of those situations where you just need to get it working. If it works, great! Doesn't matter if it's one way or the other. Most of the time you won't have an actual performance issue unless you're running this on hundreds if not thousands of objects.

    Besides, you could have buffs/debuffs handled by their own component, that flips it's
    .enabled
    property on and off depending on if there's any buffs or not.
     
    PraetorBlue likes this.
  4. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    I agree with spiney.

    i work with updates. I have worked with coroutine but feel them unnecessary most of the time. Though it is probably easier to build a progressive loading screen under coroutine but it can still be achieved in an update. As ultimately both things are just numerators whichever one you use is your decision. But it is likely that an update will trigger the coroutine anyway for this use case. So why bother when you could just handle it all in an update, even the depleters can be handled in a single for loop for every depleter clock every update frame.
     
  5. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,445
    My own personal philosophy about Coroutines is they're only for "fire and forget" logic. This thing needs to happen over a period of time, and I don't want the game to think about it anymore. The maximum I will tolerate is a need to Stop() it. They're handy for blinking light sequences, brief or sustained special effects, traffic signals which never alter or interact with anything else, and so on. They're super convenient, but I feel like you lose the convenience if you have to keep holding onto job tokens and deciding if/when they might overlap and conflict with each other.

    If they have to communicate back to the main state at all, or if they alter the physics world, I use FixedUpdate/Update/LateUpdate, and keep track of timestamps myself. Clearing a temporary buff, allowing or disallowing movement during a cooldown, moving an elevator around, all those things interact with too much for me to be comfortable leaving it to a Coroutine.

    Again, my personal take.
     
    orionsyndrome and Kiwasi like this.
  6. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    As you have seen, coroutines get very messy very fast if you need to do anything complicated.

    I get around it in two basic ways. The first is not to use coroutines and to do the timing job myself. This gets complicated in its own way. But it does work.

    The second is to treat coroutines as finite state machines attached to specific GameObjects. This way has a lot of potential and can be fun.
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    NEVER! Ugh... I wish all the collective Youtube tutorial makers would stop suggesting this horribly fragile easy-to-get entangled with coroutine solution.

    Just use a cooldown timer. Sooooo much simpler.

    Cooldown timers, gun bullet intervals, shot spacing, rate of fire:

    https://forum.unity.com/threads/fire-rate-issues.1026154/#post-6646297

    GunHeat (gunheat) spawning shooting rate of fire:

    https://forum.unity.com/threads/spawning-after-amount-of-time-without-spamming.1039618/#post-6729841

    Coroutines are NOT always an appropriate solution: know when to use them!

    https://forum.unity.com/threads/things-to-check-before-starting-a-coroutine.1177055/#post-7538972

    https://forum.unity.com/threads/heartbeat-courutine-fx.1146062/#post-7358312
     
    mopthrow, orionsyndrome and Kiwasi like this.
  8. Max-om

    Max-om

    Joined:
    Aug 9, 2017
    Posts:
    499
    Coroutines are best suited when you want to execute async code in a sync manner. Its still asynchronous but the code looks more like synchronous code and is easier to read and maintain.

    Take is example.

    Code (CSharp):
    1. var result = dialog.Show("Do you really want to quit?");
    2.  
    3. yield return result;
    4.  
    5. if(result.Outcome == Outcome.Cancel) yield break;
    Here we wait for the UI to display a dialog and then for the user to press Ok or Cancel. But the code looks and feel very synchronous or sequential if you like. Stuff like that is where couetines shine
     
    Last edited: Nov 22, 2022
  9. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    I agree with this, as this is truly the only thing where coroutines really shine and the only clever coroutine-based solution I was ever willing to deploy in my games and apps.

    I always had this suspicion that too many people in this forum irrationally abuse coroutines, ever since I started participating here, and finally someone drops a hint that the Update is "disrecommended" by someone out there?

    Who is it? I'm burning to find out which tutorial creators do this or why people go down these quite frankly more mentally convoluted route. I am also curious to see how such tutorials made themselves popular because this kind of advice should not generate any popularity at all, considering the amount of head scratching and regularity of questions regarding coroutines. This is a great mystery to me.
     
    Kurt-Dekker likes this.
  10. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Or a list of buffs which are just standard C# objects, not even MonoBehaviours, if that works well with whatever else is going on.

    I have no idea, but it's not new. I remember years ago one of my colleagues telling me about how it's cheaper to use coroutines or invoke for stuff. And I'm pretty sure that was before coroutines gave you a handle, so they were basically uncontrollable without even more hacks.

    I suspect it comes from people learning programming and learning Unity as the same thing. So they're used to doing everything by using stuff Unity has told them about, without a broader C# / programming / game dev / graphics background to draw from.
     
  11. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    That's what I was thinking, but probably should've specified. I do love me some plain old C# objects (probably use them more then monobehaviours these days).

    Ideally you'd have some form of interface that a component can implement, to signify it can be the receiver of buffs/debuffs.

    Probably the same people recommending the use of Binary Formatter.
     
    Kurt-Dekker and orionsyndrome like this.
  12. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    730
    The thing is, Coroutines states are checked every single frame (after Update calls). So thinking that it's more performant than Update calls is totally wrong. Add to that the garbage and overhead that they generate, I'd confidently say that Coroutines have worse performance than plain Update calls...
     
    orionsyndrome likes this.
  13. Max-om

    Max-om

    Joined:
    Aug 9, 2017
    Posts:
    499
    If you have 10000 mono-b updates and only 5 are doing something relevant you have 9995 that are traveresed and called unnecessary. Probably need a open world game for it to matter
     
  14. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    In both cases it would depend somewhat on how they're called. But I've never compared them as it's never mattered to me, and if it did then there are other, less hacky ways to optimise and control what is called and when.

    That's easy to handle, though. If they're not doing anything, enabled = false until they need to do something again.
     
    orionsyndrome likes this.
  15. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    This is such a hot issue, Unity has invested much of its development time making sure this can be optimized. On the other hand, coroutines have always been niche. As you've said yourself, when they shine, they shine, but they're not supposed to replace Update in any shape or form.
     
  16. Max-om

    Max-om

    Joined:
    Aug 9, 2017
    Posts:
    499
    Agree completly. But you can't fireaway with update either
     
  17. Max-om

    Max-om

    Joined:
    Aug 9, 2017
    Posts:
    499
    In our game we have optimized like crazy where it matters, using structs that reside on the stack instead of the heap, etc, etc. But at the same time we also use readability features like courotines even when they allocate a bit. Allocation is not bad per say if you make sure it's not done from a hot loop.
     
    orionsyndrome likes this.
  18. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    I don't mind this being a thing. But I will never understand how learning C# gets the backseat in the process. People will regularly use coroutine without having even a 1% of understanding of what it does and how it does it. It's like they're wielding magic, you just utter "couroutinus bufficus" and voila...

    I shiver to even think about the codebase at large. So much energy wasted.

    But ok, let's assume I'm exaggerating again. Then these people go ahead and promote their half-assed approach on the internet, with all that preposterous faux confidence and sense of authority you find on so many Youtube/Twitter accounts. They can't fool me, but oh boy, we live in a world where people regularly fall for good haircuts and well done teeth.

    On the other end, you then learn that a Steam title earns a total of $750 on average. $750!! We all know the shape of the curve by heart -- it's that 0.3% percent that gets 99% of the revenue. No wonder with so many brilliant programmers and designers! As a systems engineer, I can't help but notice that fixing one end would significantly improve the overall statistics, either by culling the lower end, or by improving the value of it.

    I'm needlessly broadening the topic, I know, but that's my 2 cents. We need to invest more in how these people are instructed/educated, and this is largely Unity's fault.
     
  19. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    You have stackalloc now. Pretty powerful. Have yet to find a good use for it, though.
     
  20. Max-om

    Max-om

    Joined:
    Aug 9, 2017
    Posts:
    499
    Typo there btw. Should say stack not heap
     
  21. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    It's ok, I've parsed it correctly anyway.
     
    Max-om likes this.
  22. gaazkam

    gaazkam

    Joined:
    Mar 30, 2016
    Posts:
    24
    I'm sorry, I don't remember where I read this. I tried looking for it to answer you, but I couldn't find it.

    Sadly, it happens to me all the time. I read something somewhere, I remember what I read but not where. Then I can't provide a source to backup my own words when I need to tell it to others :(
     
  23. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    @gaazkam It's okay. So the mystery lingers on.
    I believe tutorial creators deliberately pick the choices that will intimidate their audience the least. But this is so shallow, because once you get past the friendly basic use, you begin to realize the maintenance hell from scaling it up. And this is why these creators are irresponsible, they basically help no one, because they defer the inevitably intimidating reality of programming to somewhere else.

    In the end, you
    1) feel stupid or less confident,
    2) spam the forums with the same questions over and over,
    3) adopt and proliferate the superstitious misinformation that coroutines are somehow superior.

    That's really not a good thing for the ecosystem at large. And it's been like this for years.
     
    Last edited: Nov 23, 2022
  24. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Citation? This seems like a really naïve implementation. I've always assumed coroutines were put in an ordered queue structure, which meant only ones that can trigger this frame are checked. But I'll admit I've never cared enough to try and verify this.

    Plus your argument misses that a coroutine that is not running is never checked. This is the main performance advantage of coroutines over Update for occasional tasks. If the OPs GameObject spends 99% of life of the game unbuffed, checking for buff every frame mostly a waste.

    Of course you can achieve the same effect by simply disabling a Component or GameObject. So you don't need coroutines for this.

    The main advantage of a coroutine over update is for times when the code is not running. Coroutines cost nothing when they are not running. But update gets called every frame. For simple tasks, this can be easier than enabling or disabling a GameObject.

    There are also plenty of things Update should be avoided for, because it runs every frame. Its pretty common to see tutorial makers recommend not using Find or GetComponent in Update, because much of the time these values can be cached in Awake or Start. So its easy to see how this advice can be taken to extremes.

    The general advice "don't run code you don't need to" still makes a lot of sense. Its just that moving the code from Update to a coroutine doesn't actually mean the code isn't running.
     
  25. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    This is a good advice in general, which assumes that people tend to have too many Update calls or tend to have too many gameobjects with many scripts running "concurrently". I have never ever encountered a situation where this was an issue, because I tend to design relatively smart systems that rely on just a dozen Updates at most, which I believe to be the reasonable amount of such calls per frame. Any other dynamic quantity should be handled internal to the game logic.

    So the advice to use coroutines instead is moot. You can't fix a bad programmer with a negligibly faster solution, especially when that solution very quickly turns into a maintenance hell. You get instead a clump of superstitious and needlessly clever spaghetti that is doomed to being mediocre at everything it does.

    I'm not criticizing you, but the tutorial creators who then spend exactly 0 seconds to explain how exactly coroutines achieve this magical neatness or why we have or need Update in the first place. I have never seen a tut that explains any of this, and though I rarely need them, I should've at least seen such a caption when browsing. There are none, yet everybody and their grandma's poodle use coroutines.
     
  26. gaazkam

    gaazkam

    Joined:
    Mar 30, 2016
    Posts:
    24
    Not sure if I understand you correctly.

    Contrary to what eg learn.unity.com teaches you advise NOT to put scripts with Updates on every GameObject such as spawnable enemies etc, but rather to put a few central objects that
    - they and only they are allowed to have an Update method,
    - any other GameObject will subscribe to these central objects on Start (or OnEnabled) and unubscribe on OnDestroy (or OnDisable),
    - These central objects will move everything else around?
     
  27. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    @gaazkam You understood that correctly. That's exactly what I described. However you can solve the implementation details differently. Whether you subscribe, allocate, pool, or aggregate, that doesn't matter. The core premise is to have centerpieces of logic and connect with the underlying update mechanism in as few points of contact as possible. You obviously do not want to sacrifice a good code design, especially separations of concern. Use another Update and be smart about enabling/disabling.

    Of course, feel free to use coroutines for simple and/or really useful behaviors if that would make the code more readable and expressive. I rarely use them, but there are always niche cases here and there and it's really useful that they exist already, otherwise I would never implement them, because if I would I'd have to make sure to make them so thoroughly robust in order to cover all use cases, which is just a dev spiral.
     
    Last edited: Nov 23, 2022
  28. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    If you are writing time critical apps, don't rely on stuff like WaitForSeconds, you need a solid time reference and plan your events accordingly. If a buff starts at time = 1234567 and ends at time = 234567, then all you need to check if that time >= buff_start + buff_duration. If you really have a lot of time based events then you can improve this by adding a priority check and ignoring the low priority ones when you start lagging,i.e. when time >=event_start + event_duration + lag_threshold.

    With Unity I would start with using Time.deltaTime in Update to maintain a time reference. Something like that but it's untested and would require some tweaking:

    Code (csharp):
    1.  
    2. public struct TimeEvent
    3. {
    4.     public int eventId;
    5.     public UnityEvent<int> callMe;
    6.     public float callTime;
    7.     public int priority;
    8. }
    9.  
    10. public class TimeManager : MonoBehaviour
    11. {
    12.     private float totalTime;
    13.     public float TotalTime
    14.     {
    15.         get
    16.         {
    17.             return totalTime;
    18.         }
    19.     }
    20.  
    21.     private List<TimeEvent> timeEvents;
    22.  
    23.     void Start()
    24.     {
    25.         totalTime = 0;
    26.         timeEvents = new List<TimeEvent>();
    27.     }
    28.  
    29.  
    30.     void Update()
    31.     {
    32.         totalTime += Time.deltaTime;
    33.         foreach (var timeEvent in timeEvents.ToArray())
    34.         {
    35.             if (totalTime >= timeEvent.callTime)
    36.             {
    37.                 timeEvent.callMe.Invoke(timeEvent.eventId);
    38.                 timeEvents.Remove(timeEvent);
    39.             }
    40.         }
    41.     }
    42. }
    43.  
     
    orionsyndrome likes this.
  29. gaazkam

    gaazkam

    Joined:
    Mar 30, 2016
    Posts:
    24
    I must be missing something because with regard to performance it seems to me that your solution is equal to the naive solution with many Updates.

    It shouldn't matter if we write:

    Code (CSharp):
    1. public class Gun : MonoBehaviour
    2. {
    3.     public float fireCooldown;
    4.    
    5.     private float cooldownRemaining = 0;
    6.    
    7.     private void Update()
    8.     {
    9.         cooldownRemaining = Mathf.Max(0, cooldownRemaning-Time.deltaTime);
    10.     }
    11.  
    12.     public void Fire()
    13.     {
    14.         if(cooldownRemaining <= 0)
    15.         {
    16.             Instantiate(bulletPrefab, muzzle);
    17.             cooldownRemaining = fireCooldown;
    18.         }
    19.     }
    20. }
    Or if we, instead, write:

    Code (CSharp):
    1. public class GunManager : MonoBehaviour
    2. {
    3.     public static GunManager instance {get; private set;}
    4.  
    5.     private void Awake()
    6.     {
    7.         instance = this;
    8.     }
    9.  
    10.     private HashSet<Gun> guns = new();
    11.  
    12.     public void AddGun(Gun gun)
    13.     {
    14.         guns.Add(gun);
    15.     }
    16.  
    17.     public void RemoveGun(Gun gun)
    18.     {
    19.         guns.Remove(gun);
    20.     }
    21.  
    22.     private void Update()
    23.     {
    24.         foreach(Gun gun in guns)
    25.         {
    26.             gun.cooldownRemaining = Mathf.Max(0, gun.cooldownRemaining - Time.deltaTime);
    27.         }
    28.     }
    29.  
    30.     public void Fire(Gun gun)
    31.     {
    32.         if(gun.cooldownRemaining <= 0)
    33.         {
    34.             Instantiate(gun.bulletPrefab, gun.muzzle);
    35.             gun.cooldownRemaining = gun.fireCooldown;
    36.         }
    37.     }
    38. }
    39.  
    40. public class Gun : MonoBehaviour
    41. {
    42.     public float fireCooldown;
    43.    
    44.     public float cooldownRemaining = 0;
    45.  
    46.     private void OnEnable()
    47.     {
    48.         GunManager.instance.AddGun(this);
    49.     }
    50.  
    51.     private void OnDisable()
    52.     {
    53.         GunManager.instance.RemoveGun(this);
    54.     }
    55. }
    If anything, the second solution seems to me far more convoluted while offering little actual benefits.

    So I must be understanding you incorrectly after all?
     
  30. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    The difference is that if you have 1216276172671627 guns then you will have update called 1216276172671627 times in the first case.
     
    Last edited: Nov 23, 2022
  31. gaazkam

    gaazkam

    Joined:
    Mar 30, 2016
    Posts:
    24
    OK, but why should it matter? In the second case instead of 1216276172671627 update calls we have 1216276172671627 loop iterations. The complexity is the same.

    Meanwhile, as far as I understand, the touted benefits of using coroutines are that (a) only the coroutines associated with guns on cooldowns are running (rather than all guns), and (b) I'm not even sure, but do coroutines use CPU power on each frame, or only when starting and stopping?
     
  32. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    The complexity is the same but an Update() call is not free since the system maintains a list of the objects waiting for an update. Also, you can optimize the second method, for example by caching Time.deltaTime.
     
  33. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    You're naively treating complexity as if it's two numbers that you add up, while in reality there are many hidden variables.

    There are a few simple postulates worth keeping in mind:

    - Unity is C++ while your scripting is in C#, every time you cross between the two domains you introduce a slight overhead and/or produce some work for the underlying engine.

    - ECS and DOTS exist for a reason; small things nobody normally thinks about, add up: the worst cause of poor performance in large projects is not rendering or computation, because these things are accounted for, it's not even GC if a system is designed well -- it's actually the rhythm and patterns of memory access. Random access is a (convenient) lie. You want to streamline every nut and bolt, that's something that's beyond duty of the underlying engine. ECS and DOTS are simply pragmatic (enforced) guidelines that funnel you into verified patterns of overall optimization. They don't employ magic to provide you with ludicrous boosts in performance. You can achieve a similar thing if you only use discipline in how you arrange your project.

    - To have the most control, which lets you optimize your solution properly, Unity should do the least amount of work. Do not delegate your tasks to Unity, or else you have very few means to sort it out later, because you have no access to C++ but you have access to C# you write.

    With that in mind, try to add these hidden costs to your solution: if your 2nd example avoids having to pay that cost (it doesn't but it could), which one is better? Sure, it's more work, but sets you up to fulfill all three postulates in a generous manner.
     
    Last edited: Nov 23, 2022
    Kurt-Dekker likes this.
  34. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Honestly unless you are running millions of objects, this really isn't going to make much of a difference. And if you are running millions of objects, you are better off going straight to ECS or DOTS.

    I personally prefer the "robot swarm" approach where everything is in charge of its own logic. This gives you a bunch of really simple components that do really simple things.

    You are also missing the cost of maintaining your collection. Every gun you use has to be added to the collection, and guns have to be removed from your collection when they are no longer in use. Mess this up and you get memory leaks or null reference errors. All to do something that Unity already handles very robustly for you.

    Developer time is not free. You really shouldn't be going down the path of redoing work Unity already does for you, unless you actually need it. And the many games simply don't have enough objects around that they will benefit from this type of optimisation.

    Just had to giggle at the irony. It appears we have come full circle!
     
    Kurt-Dekker likes this.
  35. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    I have a hard time considering this as a serious argument. You're clearly not a systems designer. I am. I think ahead about the maintenance of my code. I think twice about my collections and the ways I will add or remove anything to them. Unity maybe deals with it robustly, but doesn't know S*** about my game, everything's generic. I do. I make sure to responsibly maintain my own data, my own lists, and the ways I'm iterating or calling anything.

    You do you, but in my universe, my approach makes a huge difference in how responsive and robust things are, and this is always CUSTOM-TAILORED to my needs. Sure, if I make an error, it will cost me, but I don't make errors on this level. I test and evaluate each step, making sure I fully develop all fundamentals before I move on with building a higher-level logic on top of it.

    Typically, I would a) quickly develop a working prototype, then b) an extensive proof of concept, then c) I would work on fundamental libraries or partially dynamic or proxy systems that would encapsulate some broader idea of operation. Finally d) I would implement the full system and improve upon it iteratively, until all or most of the vertical functionalities are included. Ideally in the end e) I would expand this horizontally as well, and add more systems, until it starts to resemble the desired user experience. Steps (a), (b), and (c) are optional depending on the feature/system size and my prior confidence on the subject.

    If you're the one who likes to work differently, then our techniques massively differ, and this is because we work for different markets altogether. My work is top-notch, my expertise is flawlessness, I aim at feelings of luxury and precision because my products shoot for a longer shelf-life and are costly. Your mileage may vary. That doesn't make any of us better. You are much more agile and better at producing a hundred games a year.

    Maybe one of them will sell.

    Now you're just gaslighting. If being smart about how you use your tools is "redoing" for you, that's your problem. I have arrived at such an educated opinion after 15 years of working with Unity, and sure, you don't agree with it or don't need it, fine. But don't drag me down to your level because I haven't worked this hard to understand the system better for nothing.
     
  36. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    There is nothing wrong with a robot doing its stuff, the thing is that it should not decide whether to do it or not. And not only for performance reasons. Maybe you want to disable groups, or assign priorities, or have some effect disable one of their functionalities, or have them stop doing work when the character is too far, and so on....
     
  37. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Chill bro. I'm not calling into question your credentials. For large scale games, building custom systems for everything does make sense.

    All I'm pointing out is that not every game actually needs a system designer. For smaller games, you can afford to let Unity handle things for you. In some cases, the best system is the one that you don't need to build.
     
  38. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    Fair enough. This topic is open for discussion and anyone's voice matters. Nothing is ever black or white.

    In my experience, however, vanilla Unity is so scarce with proper solutions, that even a small project requires a decent system, albeit a smaller one. Otherwise we wouldn't need Asset Store.
     
  39. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    The above contains SO many poignant phrases of wisdom:

    Kiwasi: Mess this up and you get memory leaks or null reference errors

    - Orion and other sr engineers aside, most folks are NOT systems engineers and airtight bookkeeping code is hard to write and maintain, especially over time and through iterations and refactorings. Plus... it's just boring work, IMNSHO.

    Kiwasi: Developer time is not free.

    - Every line of code you write is a line of code where a bug could be. The cheapest best lines of code are the ones you do without.

    Kiwasi: redoing work Unity already does for you

    - Yes! Use what the API offers, don't decide you need to make your own special variant if there's another easier way

    Kiwasi: many games simply don't have enough objects around

    - Yes! I would argue that if you are posting in the forum about how to use coroutines, you probably should not be using coroutines. Same goes for object poling: if you are posting about how to write an object pooling solution, then you don't need an object pooling solution.
     
    Last edited: Nov 24, 2022
    Kiwasi likes this.
  40. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    Generally you would not need a large variable system that can account for infinite entries, such as an indeterminate sized list of coroutines to start this frame when a list of floats or int will work fine. An int clock deal with ticklengths 1000 intervals vector 2 int (Time, MultiplesOfTime)
    How many multiples of thousands does my clock tick for. If both are zero dump from the list. If x is zero x = 1000 multiples of time - 1
    I wanted my clock for 10,000 ticks so I says LengthOfTick by 10.