OPTIMIZE your Unity game using these performance tips | Tutorial

Поділитися
Вставка
  • Опубліковано 25 лис 2024

КОМЕНТАРІ • 77

  • @sasquatchbgames
    @sasquatchbgames  7 місяців тому +22

    Hey guys!
    Another easy optimization tip that some of you may not know about: Build your game using IL2CPP (instead of Mono, in the Player Settings)
    Mono is better for iteration because the build time is faster, but IL2CPP runs faster, so you always want to use that option on any demos/vertical slices/final builds

    • @gameboardgames
      @gameboardgames 7 місяців тому

      These are super useful tips for an intermediate Unity guy such as myself, thanks muchly!

    • @ggwp8618
      @ggwp8618 7 місяців тому +3

      IL2CPP is also irreversible.
      Your code can easily be decompiled on mono.
      But not in il2cpp

  • @leonard4
    @leonard4 7 місяців тому +7

    Been using Unity for 10yrs, never played with Sprite atlases. I have a survivor style game with 50 to 100 enemies at a time. Each enemy has a canvas and a scroll view with debuffs. I added all of the debuff sprites to an atlas and the performance gain is significant. Thanks!

  • @sealsharp
    @sealsharp 7 місяців тому +15

    Nice one Brandon!
    1) 02:55 There's rarely a need to define custom delegates. System.Action and System.Func are fine.
    2) The tick-system is a nice way to reduce overall CPU load, however, it still executes all of them in the same frame, which may lead to microstutters.
    If you try this to spread calculations over time, it requires time slicing.
    3) Bonus tipp: One common waste of performance and unneccesary memory allocations i see in Unity tutorials are coroutines. Not just because the coroutines themselves add allocations, but so do the closures that happen hidden from the developer. There's a place for coroutines, but coroutines require a reason to be used.

    • @Dragoncro0wn
      @Dragoncro0wn 7 місяців тому

      Wouldn't a coroutine be better in that tick implementation? Id like to learn more about when a coroutine should be used since i am using them heavily in my game. Maybe i shouldve been using them less

    • @sealsharp
      @sealsharp 7 місяців тому +4

      @@Dragoncro0wn A coroutine is a tool to have a linear sequence of code run over multiple frames. A sequence that starts and ends.
      By using a coroutine, you can avoid a stateful Update() which is why it's used in tutorials so much.
      The way the tick-system is used here is for something that happens continuously and it's already using a local state ( the isFacing and distance code in the if()-conditions). So a coroutine in this case is not the right solution, because this is not a sequence.
      The most common mis-use of coroutines is when multiple coroutines run at the same time and just accidentally work by overwriting each other.
      And example is when you got grass and you want it to wobble when the player or an enemy touches it. You could make a coroutine that starts on collide and does the wobble for a few seconds.
      In this case, when the players re-collides while the wobble is still active, a second coroutine is started, both still overwriting the same value.
      When enemies collide with the grass, more coroutines would be started, overwriting the same value.
      Now scale that up, multiple pieces of grass, multiple enemies, and now you have dozens if not hundreds of coroutines all existing at the same time, mostly overwriting the same value.
      A very resonably use of a coroutine is switching levels with asynchronous loading.
      A sequence of...
      * sychronously loading the loading scene
      * unloading the last scene
      * starting the asynchronous loading of the next scene
      * updating the load-percentage while loading lasts
      * waiting for the end of the asynchronous loading
      * activating the newly loaded next scene
      * unloading the loading scene
      ...can be nicely done in a coroutine. Now add a savesystem with saving, loading, autosave to the mix and it's simple where to put the code because it's a sequence. It's visually clear, it's easy to debug. It shows something that logically is a sequence as a sequence in code and that's what coroutines are worth it for.
      So the question for coroutines is:
      * how often is my coroutine created?
      * do they overwrite each other?
      * is it really a sequence with a start and an end?
      * will it be running from start to end? If you think about how to cancel a coroutine, it's a symptom that another way of doing this would be preferable.

    • @veeper
      @veeper 7 місяців тому

      @@sealsharp
      What's wrong with using a coroutine for something that happens continuously and uses the local state?
      Is there a concrete reason why you think it's wrong?
      It seems to me that it would simplify the code a lot and would achieve the same thing that Tick() did, just without the "executes all of them in the same frame" part?

    • @sealsharp
      @sealsharp 7 місяців тому +3

      @@veeper Hey veeper!
      1) "Whats wrong with..."
      Wrong is maybe the wrong word. But it's a tradeoff between memory pressure and convenience.
      Example: This coroutine executes the logic every 0.2 seconds.
      void Coroutine() // started in Start()
      {
      while()
      {
      return yield new WaitForSeconds(0.2);
      DoLogic();
      }
      }
      Example: This Update() executes the logic every 0.2 seconds.
      float lastTime;
      void Update()
      {
      if(Time.time >= lastTime + 0.2)
      {
      lastTime = Time.time;
      DoLogic();
      }
      }
      Both are functually the same as Brandons 0.2s Ticker.
      Brandons ticker is slightly better performing than my Update method because it does the time-check only once for all instances.
      The Coroutine performs minimally worse because it has a per instance time comparison and it has overhead from the allocation of the coroutine and the allocations from the yield object, though this could be improved by yield-caching.
      So it is up to you to decide if you like to sacrifice a little bit of performance and create a little bit of memory pressure for writing it the way you think looks nicer.
      If we are honest, we often sacrifice a little bit of performance for minimally nicer looking code.
      Garbage collection however is a problematic issue in Unity. Has been for a long time and replacing the classic Unity GC with a new one ( mono to CoreCLR ) is something Unity has been working on for some time, and it's probably the most drastic technological change in the history of the engine.
      2) "without the executes all of them in the same frame"
      It will not solve this.
      If you have 1000 objects, all created at scene-load, all executing Start() and starting their coroutine in the same frame, then all of their yield new WaitForSeconds(0.2) expressions in the coroutine function will be evaluated equally and at the same frame.
      Coroutines are executed on the main thread right after Update() as seen in the unity documentation page "Order of execution for event functions".
      Hope this answers your question!

    • @veeper
      @veeper 7 місяців тому +1

      @@sealsharp yes, thank you!

  • @gregbradburn
    @gregbradburn 7 місяців тому +8

    Great stuff Brandon! A couple more tips: You can limit how often something runs inside Update by using a modulus operator on the frameCount: for example: if (Time.frameCount % 2 == 0) will only be true every other frame. You could limit it to every 10th frame by if (Time.frameCount % 10 == 0)...
    The other tip, is in addition to using compression, you can reduce the MaxSize of your textures. Like you said, play around with those settings to see if there's any noticeable difference. Also take advantage of the input quality settings for audio clips because you drag that quality slider waaaaaay down before you start to notice any difference, significantly reducing the size of audio files in your game.

    • @sealsharp
      @sealsharp 7 місяців тому

      The modulus version mentioned by Greg here can be combined with a static instance counter for very simple time-slicing.

  • @rickiousproductions
    @rickiousproductions 7 місяців тому +8

    Never realised you could pack your own sprite atlas in unity!!! Cheers dude 👍

  • @RobLang
    @RobLang 7 місяців тому +20

    Tips are good but I would recommend always starting any video on optimisation with the process of setting a frame rate budget on a target system, measure frame rate and fix where there are issues. You can have a thousand draw calls reduced to 2 but the player won't ever see the change from 200FPS to 300FPS.

    • @adventuretuna
      @adventuretuna 7 місяців тому +3

      Should also start with a baseline performance so that when he's done optimizing, he can quantify the effectiveness of his work.

    • @depralexcrimson
      @depralexcrimson Місяць тому

      yes but someone with a 240hz monitor WILL notice the change of fps from 300 to 90

    • @RobLang
      @RobLang Місяць тому +2

      @@depralexcrimson the point of my comment is that a dev must work out their frame budget on a target system. Be it 60hz, 144hz or above. But that's up to the game dev to decide on their target market, a cozy gamer on an old laptop has different needs to a FPS online competition.

  • @Lazzarus7
    @Lazzarus7 7 місяців тому +3

    Great tips, love this type of videos. A lot of youtubers don't explain debugging and optimization, very important concepts. Thank you

  • @beatrageGameSalad
    @beatrageGameSalad 7 місяців тому +1

    This is a freaking GOLDEN prize video for every developer in the world! Thanks guys! 😎

  • @ragerungames
    @ragerungames 7 місяців тому +3

    Great tips! Thank you for making this video!

  • @LabitokuDev
    @LabitokuDev 7 місяців тому

    Damn this truly hits the nail !
    Last week I was trying to optimize some application at work and after a few different ideas, I just thought about a ticker to fix issues related to multithreading and UI, which was effectively a working solution.
    As you said, sometimes, bits of code don't have to run as many times as possible in order to keep the application running as fast as possible. Moreover, I find it really convenient to manage the "scope" of a running thread. By handling how many times it can run its code each second, I feel like I have a better grasp on how much performance some logic can take.
    This kind of content explained and demonstrated in such ways is so valuable, thanks for these !

  • @nstch-root-a
    @nstch-root-a 6 днів тому

    The update callback is a neat system. For anyone interested, it's also possible to inject this system into unity's playerloop directly, making it independent of any game manager instances. For Updates that happen irregularly it's better to spread them evenly across frames, so not all update callbacks happen on the same frame.
    A small thing on the benchmark, in order to get accurate results you'd want to run the test at least in a single itteration before starting the timer, otherwise you also time the JIT and not the raw execution time of the code you're interested in.
    Adding a warmup function to the benchmark base class to run the function under test once would solve this issue completely.
    An additional benefit of the SOs for shared data: if all your scripts update in order there is a higher chance it still sits in cache as the CPU has to churn through less memory. It's usually requesting data from main memory that bottlenecks algorithms, not the algorithm itself (unless we start to go into actually large datasets)

  • @Jacob___THE_Jacob
    @Jacob___THE_Jacob 3 місяці тому

    love all the different test and results. Great vid man :)

  • @MrCrompz
    @MrCrompz 2 місяці тому +1

    This is actually super useful!

  • @SheCreatesGames
    @SheCreatesGames 3 місяці тому

    Wow I had no idea about frame debugger, its so useful!! thank you so much

  • @KIRA-kz8pn
    @KIRA-kz8pn 7 місяців тому

    i used to pack my sprite manually by taking them and importing into photoshop, this gonna save a lot of time, thanks

  • @mracipayam
    @mracipayam 7 місяців тому +2

    Great tips! you should make more optimizing videos.

  • @bgoldpanda7265
    @bgoldpanda7265 7 місяців тому

    I’ve become a fan of you ever since I watched the SO state machine video. That completely changed the way I programmed.
    I’ve noticed a great improvement in your content between that video and this one. Great work!
    I don’t know if you have heard this but you should have also mentioned that premature optimization is the root of all evil. If the benefits you gain are tiny from making the changes and it makes your code less readable, then don’t make the changes. Only make changes when you have bottle necks or it is easy to write code in the new format you are teaching.

  • @mugmugmugtattoo
    @mugmugmugtattoo Місяць тому

    I'm already subscribed to the channel. But every video I watch I'm compulsed to subscribe even more...
    A- MAZING

  • @gamedevblueprint
    @gamedevblueprint 7 місяців тому +1

    And again an another amazing video 😀🙏

  • @ArcanePengi
    @ArcanePengi 7 місяців тому +1

    This is going to save my game 🙏

  • @publicmmi
    @publicmmi 7 місяців тому

    Nice video :) I guess there are many approaches for the ticker problem. Mine is this:
    public class MonoBehaviourTicker : MonoBehaviour
    {
    public static float tickTimer = 0.2f; // 10 fps default

    private float _tickTime = 0f;
    private Action _callback;

    // Update is called once per frame
    protected virtual void Update()
    {
    _tickTime += Time.deltaTime;

    if (_tickTime >= tickTimer)
    {
    _callback?.Invoke();
    _tickTime = 0f;
    }
    }
    public void Init(float rate, Action callback)
    {
    tickTimer = rate;
    _tickTime = 0f;
    _callback = callback;
    }
    }
    Every class which needs to update "regulated" can derive from 'MonoBehaviourTicker' and in Awake call the Init and in Update() it needs to call "base.Update()" first.

  • @karatepunk
    @karatepunk 7 місяців тому

    some really nice tips in here. I really appreciate your efforts!

  • @vazzius
    @vazzius 7 місяців тому

    One note is that the Sprite Atlas will take care of compression when you setup one, so in this case you don't need to do the importer settings setup for each individual sprite.

  • @alexey6343-x0
    @alexey6343-x0 6 місяців тому

    This is from Unity's blog. Was posted in 2022.
    >You should aim to profile a development build of your game, rather than profiling it from within the Unity Editor. There are two reasons for this:
    1. The data on performance and memory usage from standalone development builds is much more accurate compared to results from profiling a game in-Editor. This is due to the Profiler window recording data from the Editor itself, which can skew the results.
    2. Some performance problems will only appear when the game is running on its target hardware or operating systems, which you’ll miss if you profile exclusively in-Editor.

  • @txown
    @txown 5 місяців тому

    Thanks for your video

  • @Dragoncro0wn
    @Dragoncro0wn 7 місяців тому +2

    Instead of a ticker in update why not use a coroutine that has a while true loop and waitforseconds for whatever time you need. Then you have more control on when this coroutine runs as well

  • @cadafinn
    @cadafinn 7 місяців тому +2

    Gracias, maestro gracias a ti estoy aprendiendo como usar unity 2D para mi proyecto.

  • @Arkenhammer
    @Arkenhammer 7 місяців тому

    For your tick system, its better to break the event callbacks into pools. If your updates are actually taking more that 16ms, putting them all into a single event mean you have one slow frame every 200ms instead of all frames being slow. From a player experience point of view I am not sure that's better because 5 hiccoughs a second isn't great. Better is to distribute those callbacks across multiple frames. Let's say your total update time is 30ms. If you call them all once every 160ms, you end up with one 30ms frame every 10. If you split them into 10 pools and cycle through them you can spend 3ms every frame instead which will look much better.

  • @hefferwolff3578
    @hefferwolff3578 5 місяців тому

    This was great!

  • @halivudestevez2
    @halivudestevez2 7 місяців тому

    let me put here my little tip: in unity editor, the FPS may go down if you let the Scene window on while running the game. So: close Scene window on Play mode :)

  • @Soundy777
    @Soundy777 7 місяців тому

    Great Vid & Great Tips!

  • @megachebu
    @megachebu 2 місяці тому

    If you're making a 3D game, making LOD's is another important optimization technic. I fell into false assumption that number of vertices isn't that important for gpu nowadays. But it is, so don't overlook it.

  • @Cherry-jc8bo
    @Cherry-jc8bo 7 місяців тому

    Thanks a lot man ❤ for the tips love it❤

  • @mirandaart3012
    @mirandaart3012 7 місяців тому

    how is it anytime i think of how to do something, there's a video on it xD nice work!

  • @kobi665
    @kobi665 4 місяці тому

    so,
    there's a faster sqrt based on the quake 3 algorithm
    you can look it up, but in my benchmarks it is about 10-20% faster than magnitude

  • @Spellsword95
    @Spellsword95 2 місяці тому

    Great tips, thank you! I wonder what was the final CPU load reduction from 1.2% with Tick method?

  • @aaaalord
    @aaaalord 7 місяців тому

    Thank you for your tips

  • @GoeTeeks
    @GoeTeeks 7 місяців тому

    09:25 You should look into Power of Two texture sizes for Unity.

    • @sealsharp
      @sealsharp 7 місяців тому +1

      And dividable by 4 if you can't do Power of Two. Crunch compression requires it.

  • @martinchya2546
    @martinchya2546 7 місяців тому +1

    The stuff in this video are true. But the problem is that.. they doesnt really matter THAT much in most typical scenarios. Reducing 4 draw calls to 1 draw call wont change that much even on 10 year mobile phone (esp since URP handles draw calls a lot better). Hashing string for anim is nice.. if you actually do 100 000 of these in one frame, are you? Benchmarking is okay, but dont benchmark Vector2.Distance() call, benchmark things that you really know that could influence framerate (for example line of sight calculation, path finding, ai finding best move, etc.).
    I am not saying that topics covered in this video are wrong, because they are not. But in real world scenario they doesnt really matter as much as many think will do. Dont waste your time by profiling and optimizing every single block of code - just MAKE YOUR GAME. You are using Unity after all, it comes with its own overhead because its general purpose engine, not thing tailored for your specific use case.
    Good, you will save 0.000003 seconds by optimizing your linq into foreach or using dictionary instead of array, good. But by this time, Unity will calculate hundred of matrix transformations every frame, perform UI raycasts, split world into aabb boxes for collision checking, run physics tick, render 6 cameras for every light to render your nice soft shadows, render your view 12 times to make bloom happen, generate shadows for your floor because you forgot to disable shadow casting on them, etc.
    It just doesnt matter that much. My few perf tips:
    - dont use realtime shadows on mobile, they are performance killer
    - generally unless its impossible for you, always use baked lights instead of realtime
    - always profile to find performance issues on target device (if you have beefy computer or phone, buy or find some weaker one and profile on weaker as well)
    - be careful only in Update() functions or other real time stuff
    - time slice more complex actions (you dont need to pathfind 60 times per second, once per 0.25s is probably enough)
    - dont be too obsessive about allocations, they are fine even on mobile, only avoid alloc per frame or when you deal with huge amounts of entities (bullet hells) (also remember to tick incremental gc because its not enabled by default)
    I'll tell you my story, I am updating a "fog of war" texture pixel by pixel, the texture is 256x256 monochrome texture. I am taking 5x5 tiles around player and doing some simple raycast for every of those 25 tiles to determine what player see. The raycast is modified boehm line alghoritm. So its 5x5x~10 iterations every player step. I was terrified that it will kill my performance... and yeah it took a huge amount of 0.0004 seconds on my 7 year old phone.
    Optimize real thing, dont optimize just for sake of optimizing.

    • @Hietakissa
      @Hietakissa 7 місяців тому

      You suggest that because we use Unity that does more complex things behind the scenes than we do, optimizing our code would be useless, but I think that's a very backwards way of looking at things.

    • @martinchya2546
      @martinchya2546 7 місяців тому

      @@Hietakissa no, I suggest to optimize real problems instead of measuring whether caching transform will get you a net performance gain of 0.0000000000001 s ;)

    • @Hietakissa
      @Hietakissa 7 місяців тому

      @@martinchya2546 caching transform isn't even comparable to the things gone over in the video. Sure, doing them once per frame won't bring any noticeable performance improvements, but the same goes with most optimization methods, they are still all good to keep in mind for when they are applicable

    • @DeepCommaAI
      @DeepCommaAI 7 місяців тому

      i agree with you, many people waste their time optimizing things that dont matter very much in the end

    • @sinkingdutchman7227
      @sinkingdutchman7227 2 місяці тому

      I'd say object pooling DOES matter that much.

  • @ОлегМорев-ы9я
    @ОлегМорев-ы9я Місяць тому

    Why not using courutine with WaitForSeconds(required tick time) instead of timers in update? Is it less efficient?

  • @bhuvneetsaggu4011
    @bhuvneetsaggu4011 7 місяців тому

    Thanks for the tips, but for reducing update.. Can we do
    Start ()
    RepeatingInvoke("dumbUpdate", 0.2)

  • @EROSNERdesign
    @EROSNERdesign 7 місяців тому

    why do I keep getting this error " It looks like another Unity instance is running with this project open. Multiple Unity instances cannot open the same project."

  • @halivudestevez2
    @halivudestevez2 7 місяців тому

    I was afraid of using atlas so much, but is it so easy like you showed it? No need to slice sprites after putting them onto 1 asset?
    I will take a look at it.

    • @sealsharp
      @sealsharp 7 місяців тому

      You may be confusing to ways to do a sprite atlas:
      * If you create one from individual images in Unity like in the video, Unity already knows the sizes of the individual assets from the source image.
      * If you create one from a spritesheet, an single file image that contains multiple assets on it, then you need to mark which parts of the image are individual sprites.

  • @Diablokiller999
    @Diablokiller999 7 місяців тому

    9:05 Wait, empty functions would get removed by the compiler in C/C++ anyway.
    Didn't know C# is so bad o.O

    • @sealsharp
      @sealsharp 7 місяців тому

      The issue is not the compiler, remember that the C# compiler is just a preprocess for the JIT.
      The method stays because it could be required by derived classes that may potentially be loaded or created at runtime. There's no overhead except for a few bytes in the assembly until the use happens, but in this case, Unity links itself to instances of empty methods.
      Unity uses a custom version of the mono-runtime, so we can't say if it's a problem with mono-reflection or how Unity use it. In C# the possibilty to check for an empty method body exists since 2005.

    • @Diablokiller999
      @Diablokiller999 7 місяців тому +1

      @@sealsharp Thought that actually is the issue since C# isn't pre compiled like C/C++/Rust and thus can run into such pitfalls.
      Afaik the C-Compiler knows if a method isn't even used by derived classes because the preprocessor should tell him anyway. At least you get warnings for those events If I remember correctly - don't write unused methods that often ;)

    • @sealsharp
      @sealsharp 7 місяців тому

      @@Diablokiller999 dotnet had "that's empty, remove that!" warnings for years now and in my long time working with it, Unity is the one case I remember where empty methods are anything worse than wasted space in a text file.

  • @Coco-gg5vp
    @Coco-gg5vp 7 місяців тому +3

    First

  • @TheMeanArena
    @TheMeanArena 3 місяці тому +1

    Only optimize when your game needs to be. Otherwise you're optimizing for the sake of optimizing, adding more time to development where you won't see a difference.

  • @2u2y
    @2u2y 3 місяці тому

    Why don't you just use a InvokeRepeating or Coroutine

  • @regys9521
    @regys9521 7 місяців тому

    Gold tips

  • @S_Tadz
    @S_Tadz 7 місяців тому

    Wow, I just got given some 'optimization tips' by a guy who uses a float to iterate through a loop 🤣
    Also, simply doing benchmark tests in the editor without using the frame debugger is just plain useless and the results mean absolutely nothing, especially when measured in milliseconds and going 'tHeRe iS a 30 mILliSeCoNdS iNCreAse iN pErForMAncE'.

    • @DeepCommaAI
      @DeepCommaAI 7 місяців тому +2

      looking forward to your UA-cam video

    • @S_Tadz
      @S_Tadz 7 місяців тому

      @@DeepCommaAI 🤣
      Since I have an actual job in the industry and YT channels are for wannabe devs, you'll wait a long time, bud

    • @DeepCommaAI
      @DeepCommaAI 7 місяців тому +3

      @@S_Tadz looking forward to your game

    • @Hietakissa
      @Hietakissa 7 місяців тому +1

      It's not useless, it can still be compared to itself. Also where is this loop you're talking about?