Jump to content
  • Advertisement
  • 07/29/19 12:57 AM

    How We Optimised Our Scripts in Unity

    General and Gameplay Programming

    GameDev.net

    There are a lot of great articles and tutorials focusing on performance in Unity. This article is not trying to replace them or improve them, this is just a summary of steps that we went through after reading these articles and the steps that helped solve our issues. I strongly recommend to go through https://learn.unity.com/ at least.

     

    During the development of our game, we ran into issues that caused an occasional lag during the gameplay. After some time spent with the Unity Profiler we found two types of issues:

    • Unoptimised shaders
    • Unoptimised C# scripts

    Most of the issues came from the second group, so I decided to focus this article on C# scripting (and maybe also because I have never written a single shader in my entire life). If you want to know more about best practices for writing your shaders just wait for an article that my colleague is about to write soon.

     

    Finding the Weak Spot

    The point of this article is not to give a tutorial on how to use the profiler, I just want to highlight what we focused on during the profiling.


    Unity Profiler is always the best way to go while trying to find the scripts causing your lag. I strongly recommend profiling the game directly on the device instead of profiling in the editor. So because our game is an iOS game I just had to connect the device and use the Build Settings shown in the picture below and the profiler was connected automatically

    1_tIukx6gJnyCpZh9MtU4omQ.png

    Build Settings for profiling

     

    If you try to google “Random lag in Unity” or any other similar phrase you can find that most people recommend to focus on Garbage Collection, so I did exactly that. Garbage is generated anytime you stop using some object (instance of a class) then from time to time Unity’s Garbage collector is run to clear the mess and deallocate the memory which takes an insane amount of time causing the frame rate to drop.

     

    How to find the scripts causing garbage allocation in the profiler?

    Just Select CPU Usage -> Choose Hierarchy view -> Sort by GC Alloc

    1_YO-xbcKD_CgSWXI4G7EaqA.png

    Profiler settings for GC

     

    Your goal should be to get to all zeros in the GC alloc column in your gameplay scene.


    Another good point is to sort the records by “Time ms” (execution time) and optimise the scripts to take as little time as possible. This was a huge thing for us, because one of your components contains a large for-loop that took almost forever to execute (yeah we have not found a way to get rid of the loop, yet) so optimising the execution time for all the scripts was an absolute necessity for us, because we needed to save some execution time for this time-consuming for-loop while maintaining 60 fps.


    So based on the profiling, I split the optimisation into two parts :

    • Getting rid of the garbage
    • Lowering down the execution time

     

    Part 1: Fighting the Garbage

    This part focuses on what we did to get rid of all the garbage. These are the absolute basics that every developer should know and it also became an important part of our code review with every pull/merge request on a daily basis.

     

    1st Rule: No New Objects in Update Methods

    Ideally, you should have no “new” keywords used in the Update, FixedUpdate or LateUpdate methods. You should always try to use, what you already have.


    Sometimes the new object creation is hidden in some Unity’s internal methods so it is not so obvious. We will discuss these later.

     

    2nd Rule: Create Once and Reuse, Reuse and Reuse!

    This basically means to allocate everything you can in the Start and Awake methods. The rule is very similar to the 1st one. Actually, it’s just another way of removing “new” keywords from the Update methods.


    You should always try to move all code that:

    • creates new instances,
    • finds any game objects,

    out of the Update methods and move it to Start or Awake.
    Here are examples of changes that we did:


    Allocate Lists in the Start method, Clear them when needed and reuse wherever you want.

    //Bad code
    private List<GameObject> objectsList;
    void Update()
    {
        objectsList = new List<GameObject>();
        objectsList.Add(......)
    }
    //Better Code
    private List<GameObject> objectsList;
    void Start()
    {
        objectsList = new List<GameObject>();
    }
    void Update()
    {
        objectsList.Clear();
        objectsList.Add(......)
    }

     

    Store references and reuse them like this:

    //Bad code
    void Update()
    {
        var levelObstacles = FindObjectsOfType<Obstacle>();
        foreach(var obstacle in levelObstacles) { ....... }
    }
    //Better code
    private Object[] levelObstacles;
    void Start()
    {
        levelObstacles = FindObjectsOfType<Obstacle>();
    }
    void Update()
    {
        foreach(var obstacle in levelObstacles) { ....... }
    }

     

    The same applies to FindGameObjectsWithTag method or any other method that returns a new array.

     

    3rd Rule: Beware of Strings and Avoid String Concatenation

    Strings are horrible when it comes to garbage allocations. Even basic string operations can generate a lot of garbage. Why is that? Strings are just arrays and these arrays are immutable. That means whenever you try to concatenate two strings together a new array is created and the old one becomes garbage. Thankfully you can use StringBuilder to avoid or minimise this garbage allocation.

    Here is an example of how to improve this:

    //Bad code
    void Start()
    {
        text = GetComponent<Text>();
    }
    void Update()
    {
        text.text = "Player " + name + " has score " + score.toString();
    }
    //Better code
    void Start()
    {
        text = GetComponent<Text>();
        builder = new StringBuilder(50);
    }
    void Update()
    {
        //StringBuilder has overloaded Append method for all types
        builder.Length = 0;
        builder.Append("Player ");
        builder.Append(name);
        builder.Append(" has score ");
        builder.Append(score);
        text.text = builder.ToString();
    }

     

    The example shown above is ok, but there is still a lot of space to improve the code. As you can see, almost the entire string can be considered as static. So what we did is that we split the string into two parts, into two UI.Text objects. First, one containing only the static text “Player “ + name + “ has score “ which can be assigned in the Start method and the second one containing the score value which is updated every frame. Always make static strings really static and generate them in Start or Awake method. With this improvement, it is almost ok, but still, some garbage is generated by calling Int.ToString(), Float.ToString() etc.

    We solved this by generating and pre-allocating all possible strings. It might sound stupid and memory consuming, but it perfectly fits our needs and solve this issue completely. So we ended up with a static array that you can access directly using indices to get the required string representing the number :

    public static readonly string[] NUMBERS_THREE_DECIMAL = {
            "000", "001", "002", "003", "004", "005", "006",..........

     

    4th Rule: Cache Values Returned by Accessors

    This can be very tricky because even a simple accessor like this one generates Garbage:

    //Bad Code
    void Update()
    {
        gameObject.tag;
        //or
        gameObject.name;
    }

     

    Try to avoid using the accessors in the Update method. Call the Accessor only once in the Start method and cache the return value.

    In general, I recommend to NOT call any String accessors or Array accessors in the Update methods. In most of the cases, you only need to get the reference once in the Start method.

    Here are two common examples of another unoptimised accessor code:

    //Bad Code
    void Update()
    {
        //Allocates new array containing all touches
        Input.touches[0];
    }
    //Better Code
    void Update()
    {
        Input.GetTouch(0);
    }
    //Bad Code
    void Update()
    {
        //Returns new string(garbage) and compare the two strings
        gameObject.Tag == "MyTag";
    }
    //Better Code
    void Update()
    {
        gameObject.CompareTag("MyTag");
    }

     

    5th Rule: Use Non Alloc Functions

    For certain Unity functions, you can find their alternatives that don’t allocate anything. In our case, these functions are all related to Physics. Our collision detection is based on

    Physics2D. CircleCast();

     

    For this one specifically, it is possible to find a function that does not allocate anything called

    Physics2D. CircleCastNonAlloc();

     

    Many other functions have alternatives like this one, so always check the documentation for NonAlloc functions.

     

    6th Rule: Don’t Use LINQ

    Just don’t. I mean don’t use it in any code that is executed often. I know the code is easier to read when using LINQ, but in many cases the performance and memory allocation of such code is horrible. Of course, it is possible to use it sometimes, but I want to keep this simple and honestly, in our game we don’t use LINQ at all.

     

    7th Rule: Create Once and Reuse, Reuse and Reuse vol 2.

    This time it is about object pooling. I will not go into details of object pooling because it has been said many times, for example, check this tutorial https://learn.unity.com/tutorial/object-pooling

    In our case, the scenario for object pooling is this one. We have a generated level that is full of obstacles that live only for a certain period of time until a player passes this level section. These obstacles are instantiated from prefabs when certain conditions are met. The code is in the Update method. This code is absolutely inefficient considering both memory and execution time. We solved this by generating a pool of 40 obstacles and taking these obstacles from the pool when needed and returning the objects back to the pool after they are not needed anymore.

     

    8th Rule: Look out for Boxing!

    Boxing generates garbage! But what is boxing? The most common occurrence of boxing is when you pass a value type (int, float, bool etc) into a function that expects a parameter of type Object.

    Here is an example of boxing that we needed to solve in our project:

    We implemented our own messaging system in the project. Every message can contain an unlimited amount of data. The data were stored in a dictionary that was defined like this

    Dictionary<string, object> data;

     

    And we had a setter to set values into this dictionary

    public Action SetAttribute(string attribute, object value)
    {
        data[attribute] = value;
    }

     

    The boxing here is pretty obvious. You can call the function like this

    SetAttribute("my_int_value", 12);

     

    So the value “12” is boxed and that generates garbage.

    We solved this by having separate data containers for each primitive type and the previous Object container is used only for reference types.

    Dictionary<string, object> data;
    Dictionary<string, bool> dataBool;
    Dictionary<string, int> dataInt;
    .......

     

    and having separate setters for each data type

    SetBoolAttribute(string attribute, bool value)
    SetIntAttribute(string attribute, int value)

     

    And all these setters were implemented to call the same generic function

    SetAttribute<T>(ref Dictionary<string, T> dict, string attribute, T value)

     

    And the boxing is gone!

    To find more details check this article https://docs.microsoft.com/cs-cz/dotnet/csharp/programming-guide/types/boxing-and-unboxing

     

    9th Rule: Loops Are Always Suspicious

    This is very similar to the first and second rule. Just try to remove all unnecessary code from Loops for both performance and memory allocation reasons.

    We try to avoid loops in Update methods in general, but when it is really needed we at least avoid any allocation in such loops. So follow again all 1–8 rules and apply this for Loops in general and not just for Update methods.

     

    10th Rule: No Garbage in External libraries

    In case you find out that some of the garbage is generated by a code that you downloaded from Asset store you have multiple options how to solve this, but before doing any reverse engineering and debugging just check the Asset store again and update the library. In our case, all assets we used were still maintained by authors and they keep doing performance updates, so this solved all our issues. Keep your dependencies up to date! I would rather get rid of the library instead of keeping an unmaintained one.

     

    Part 2: Pushing the Execution Time to Its Limits

    Some of the rules mentioned here make barely noticeable difference if the code is not called often. In our case, we have a large Loop that is executed every frame so even these little changes made a significant difference for us.

    Some of these changes, when used incorrectly or in an inappropriate situation, might lead to even worse execution times. Always check the profiler after every single optimisation change in the code to be sure that it is going the desired direction.

    Honestly, some of these rules lead to a code that is much harder to read and sometimes even breaks coding best practices, for example, code inlining mentioned in the rules below.

    A lot of these rules overlap with the rules mentioned in the first part of this article. Usually, garbage allocating code performs poorly compared to non-allocating code. So I recommend going through the first part of the article before reading this one.

     

    1st Rule: Proper Order of Execution

    Move your code from FixedUpdate, Update, LateUpdate methods to Start and Awake methods. I know this sounds crazy but trust me, if you dig deep into your code, you can find hundreds of lines of code that can be moved to one of the methods that are executed only once.

    In our case, such code was usually related to:

    • GetComponent<> calls
    • Calculations that actually returns same result for every frame
    • Repeatedly instantiating same objects, usually Lists
    • Finding some GameObjects
    • Getting references to Transforms and using other accessors

    Here is a list of examples of code, that we moved from Update methods to Start methods:

    //There must be a good reason to keep GetComponent in Update
    gameObject.GetComponent<LineRenderer>();
    gameObject.GetComponent<CircleCollider2D>();
    
    //Examples of calculations returning same result every frame
    Mathf.FloorToInt(Screen.width / 2);
    var width = 2f * mainCamera.orthographicSize * mainCamera.aspect;
    var castRadius = circleCollider.radius * transform.lossyScale.x;
    var halfSize = GetComponent<SpriteRenderer>().bounds.size.x / 2f;
    
    //Finding objects
    var levelObstacles = FindObjectsOfType<Obstacle>();
    var levelCollectibles = FindGameObjectsWithTag("COLLECTIBLE");
    
    //References
    objectTransform = gameObject.transform;
    mainCamera = Camera.main;

     

    2nd Rule: Run the Code Only When It Is Needed

    In our case, this was mostly relevant for the scripts that update UI. Here is an example of how we changed the implementation of a code that displays the current state of “collectibles” in the level.

    //Bad code
    Text text;
    GameState gameState;
    
    void Start()
    {
        gameState = StoreProvider.Get<GameState>();    
        text = GetComponent<Text>();
    }
          
    void Update()
    {
        text.text = gameState.CollectedCollectibles.ToString();
    }

     

    Because we only have few collectibles in each level, it does not make any sense to change the UI text every frame, so instead, we change the text only when the actual number changes.

    //Better code
    Text text;
    GameState gameState;
    int collectiblesCount;
    
    void Start()
    {
        gameState = StoreProvider.Get<GameState>();    
        text = GetComponent<Text>();
        collectiblesCount = gameState.CollectedCollectibles;
    }
          
    void Update()
    {
        if(collectiblesCount != gameState.CollectedCollectibles) {
            
            //This code is ran only about 5 times each level
            collectiblesCount = gameState.CollectedCollectibles;
            text.text = collectiblesCount.ToString();
        }
    }

     

    The code above is much better, especially if the code is more complex than just simple UI change.

    If you are looking for a more complex solution I recommend to implement an Observer pattern (https://en.wikipedia.org/wiki/Observer_pattern) using C#’s Events (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/)

    Anyway, this was still not good enough for us and we wanted to implement a completely generic solution so we created a library that implements Flux (https://facebook.github.io/flux/) into Unity. This leads to a very simple solution where you have all game state stored in a “Store” object and all UI elements and other components get notified when any state is changed and they react to this change with no code needed in Update method. If you are interested in this solution, my colleague will write an article about this soon, so subscribe or follow us and stay tuned :).

     

    3rd Rule: Loops Are Always Suspicious

    This is exactly the same rule as the one mentioned in the first part of this article. If you have any loop in the code iterating over a large number of elements always apply all the rules mentioned above in both parts of this article to improve the performance of the loop.

     

    4th Rule: For over Foreach

    Foreach loop is so easy to write but “so complex” to execute. Foreach loop internally uses Enumerators to iterate given set of data and to return the value. This is more complex than just iterating indices in a simple For loop.

    So in our project whenever it was possible we changed Foreach loops to For loops like this:

    //Bad code
    foreach (GameObject obstacle in obstacles)
    
    //Better code
    var count = obstacles.Count;
    for (int i = 0; i < count; i++) {
        obstacles[i];
    }

     

    In our case of the large for loop, this change was really significant. The simple for loop resulted in a 2 times faster code.

     

    5th Rule: Arrays over List

    In our code, we found out that most of the Lists either have fixed length or we can calculate the maximum number of items. So we reimplemented these using arrays which in some cases led to even 2x faster iteration of the data.

    In some cases, you can not avoid using Lists or any other complex data structures. The common situation is if you need to add or remove elements often if this case it is better to use Lists. Anyway in general always use Arrays for fixed-size lists.

     

    6th Rule: Float Operations over Vector Operations

    This difference is barely noticeable unless you do thousands of operations like this, which was exactly our case, so this performance increase became significant for us.

    We did changes like this:

    Vector3 pos1 = new Vector3(1,2,3);
    Vector3 pos2 = new Vector3(4,5,6);
    
    //Bad code
    var pos3 = pos1 + pos2;
    
    //Better code
    var pos3 = new Vector3(pos1.x + pos2.x, pos1.y + pos2.y, ......);
    Vector3 pos1 = new Vector3(1,2,3);
    
    
    //Bad code
    var pos2 = pos1 * 2f;
    
    //Better code
    var pos2 = new Vector3(pos1.x * 2f, pos1.y * 2f, ......);

     

    7th Rule: Finding Objects Properly

    Always consider if you really need to use GameObject.Find() method. This method is a beast and takes an insane amount of time. You should never have such a method in any of the Update methods. We found out that most of our Find calls could be replaced by direct reference association in the Editor, which is, of course, the better way to go.

    //Bad Code
    GameObject player;
    
    void Start()
    {
        player = GameObject.Find("PLAYER");
    }
    
    //Better Code
    
    //Assign the reference to the player object in editor
    [SerializeField]
    GameObject player;
    void Start()
    {
    }

     

    In case you can not do it like this you should at least consider using Tags and finding the object by its Tag using GameObject.FindWithTag.

    So in general: Direct Reference > GameObject.FindWithTag() > GameObject.Find()

     

    8th Rule: Work Only with Relevant Objects

    In our case, this was significant for collision detection using RayCasts (CircleCasts etc). Instead of detecting all collisions and making the decision which ones are relevant in code, we moved our game objects to proper layers so we can calculate collisions only on relevant objects.

    Here is an example

    //Bad Code
    void DetectCollision()
    {
        var count = Physics2D.CircleCastNonAlloc(
           position, radius, direction, results, distance);
        for (int i = 0; i < count; i++) {
           var obj = results[i].collider.transform.gameObject;
           if(obj.CompareTag("FOO")) {
               ProcessCollision(results[i]);
           }
        }
    }
                                  
    //Better Code
    //We added all objects with tag FOO into the same layer
    void DetectCollision()
    {
        //8 is number of the desired layer
        var mask = 1 << 8;
        var count = Physics2D.CircleCastNonAlloc(
           position, radius, direction, results, distance, mask);
        for (int i = 0; i < count; i++) {
           ProcessCollision(results[i]);
        }
    }

     

    9th Rule: Use Tags Properly

    There are no doubts that tags are very useful and can improve performance of your code, but keep in mind that there is only one correct way of comparing object tags!

    //Bad Code
    gameObject.Tag == "MyTag";
    
    //Better Code
    gameObject.CompareTag("MyTag");

     

    10th Rule: Beware of Tricky Camera!

    It is so simple to use Camera.main, but the performance of such action is really bad. The reason is that behind each Camera.main call Unity actually does FindGameObjectsWithTag() to get the result, so we already know that it is not a good idea to call this frequently and the best way to solve this is to cache the reference in the Start or Awake method.

    //Bad code
    void Update()
    {
        Camera.main.orthographicSize //Some operation with camera
    }
    
    //Better Code
    private Camera cam;
    void Start()
    {
        cam = Camera.main;
    }
    void Update()
    {
        cam.orthographicSize //Some operation with camera
    }

     

    11th Rule: LocalPosition over Position

    Use Transform.LocalPosition everywhere you can instead of Transform.Position for both getters and setters. The reason is that there are much more operations executed behind each Transform.Position call, specifically calculating the global position in case of calling a getter or calculating local position from the global one in case of calling a setter. In our case, we found out that we could use LocalPositions in 99 percents of occurrences of Transform.Position with no other changes needed to be done in the code.

     

    12th Rule: Don’t Use LINQ

    Already discussed in the first part. Just don’t use it, that’s it.

     

    13th Rule: Don’t Be Afraid to Break Best Practices (sometimes)

    Sometimes even a simple function call can be too expensive. In this case, you should always consider Code Inlining. What does that mean? Basically, it means you just take the code from the function and copy the code directly to the place where you wanted to use the function to avoid any additional method calls.

    In most of the cases, this will not make any difference because the code inlining is done automatically at compile-time, but there are certain rules by which the compiler decided whether the code will be inlined or not (for example Virtual methods are never inlined, for more details check https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity8.html). So just open profiler, run the game on the actual device and see if there is any space for improvement.

    In our case, we found a few functions that we decided to inline for better performance, especially in the large for-loop that we have in the game.

     

    Conclusion

    By applying the rules mentioned in the article we easily managed to get a stable 60 fps in an iOS game even on an iPhone 5S. Some of these rules might be too specific for our use case, but I still think that most of them should be in your mind while coding or doing code reviews to avoid any problems in later stages. It is always easier to continuously write code with performance aspects in mind than refactoring large chunks of the code later.

     

    ---

    We are Lonely Vertex, a small indie game studio located in Prague, Czech Republic. Currently getting ready to release our first game, Sine. You can subscribe for our newsletter, read our development blog posts or follow our progress on Twitter or Facebook.

    ---

     

    Note: This article was originally published on Medium, and is republished here with kind permission of the author.



      Report Article


    User Feedback


    This is very valuable. As an engine developer, I already use a lot of these patterns in unity, but it's awesome to see production code confirm and teach me new paradigms! Thank You 

    Share this comment


    Link to comment
    Share on other sites
    gameObject.CompareTag("MyTag");

    I was not aware of this, I always did the other way around with "==" operator. Similar thing with the Input.touches[0] and Input.GetTouch(0). Nice tips ;)

    Also be sure to compile with IL2CPP, compiler may optimize most of the things ;)

    Thanks, nice article!

    Share this comment


    Link to comment
    Share on other sites

    6th Rule (Float Operations over Vector Operations)  is outdated: 

    Use the new Unity.Mathematics package and it's float3 and float4 types instead of Vector3 (Vector3 and float3 are interoperable btw so it's easy to just convert the critical code)

    Even if you are not using jobs and burst, you will gain from it, it was written for performance (including all the utility methods in the lowercase math class) and it's even more convenient :  you can do f.x = 5 or even f.yzx = p.xyz

    But if you use burst and jobs (even if you just run a job on the main thread with .Run() ), you will get the benefits of optimized low level code with intrinsics for the math functions.

    And the craziest, is that if you use float4, even if you just need xyz and always have w set to 0, you will get vectorized code ( = multiple operations at once in the cpu).

     

    Share this comment


    Link to comment
    Share on other sites


    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

  • Advertisement
  • Advertisement
  • Latest Featured Articles

  • Featured Blogs

  • Advertisement
  • Popular Now

  • Similar Content

    • By Ruben Torres
      Last week, I wrote a post to show you how your unity scene hierarchy is reducing the performance of your game. That post arose awareness across many of you developers. And so, many of you asked great questions that I'll answer in today's entry.
      [The original post can be found here]

      Yes... I confess.
      The examples I often show you in my blog posts are not real-life projects. This is one of the points you have been commenting on.
      Ruben, this is an artificial scenario.
      Ruben, this doesn't happen in games.
      You see, I understand. It's easy to doubt the information I provide you when the only cases you see are extreme. I'd even rub some salt in the wound on the Reddit threads if the author wasn't me (see how I got owned).
      But here's the thing: all the information I give you is based on the pain and gains of real-life projects I worked on. I do my research before I write here too.
      Doing research is great. But that takes time. A lot.
      So I won't set up a new game for every weekly post that I publish. What I do instead is to create a small project to make a point...
      ...A very real point that I experienced in production games.
      You're never going to come across the exact dummy project in your games. But you're likely to suffer from the issues these points reveal.
      And that's what matters.
      So I took some of the feedback you provided for today's post. I'll elaborate on some of the problematic hierarchy patterns you'll commonly see in production games. We will address unity scene hierarchy bottlenecks based on the tools I gave you in the last article:
      The FAP Hierarchy Tool The DetachGameObject simple performance boostingg component Quick Navigation (opens in a new tab)
      The golden rules for an efficient scene hierarchy
          The Gamedev Guru's Golden Rules of a Clean Unity Scene Hierarchy
      Flattening a unity scene hierarchy: an artificial case-study
          The hierarchy structure
          Profiling the unoptimized scene
          Flattening our scene hierarchy
      So what?
      The golden rules for an efficient scene hierarchy
      In the previous post we established a guideline for diagnosing and optimizing unity scene hierarchies.
      Let's quickly recap The Gamedev Guru's golden rules for an efficient hierarchy:

      These apply especially to hierarchy trees that have dynamic objects. And by dynamic I mean, game objects whose transforms are altered. It can be a position, a rotation, a scale or any of the attributes you find in a RectTransform.
      If an entire tree is static, i.e. no periodical transform changes, then don't worry about that tree.
      You see, propagating these transform changes takes time. And it takes more time when you have more game objects in the same tree.
      But it's not the total CPU time that concerns me the most. The issue that I see is that it is pretty hard for Unity to do these transform operations in parallel when they happen to be in the same hierarchy tree.
      So changes in complex trees take a higher CPU time for two main reasons:
      The absolute CPU time required to do the math increases These calculations cannot be spread across different threads Flattening a unity scene hierarchy: an artificial case-study
      I'm a pragmatic and practical professional developer. So let's see all this theory in action.
      The hierarchy structure
      What I have here for you is a scene full of props, particle systems and characters. This is how the unity scene hierarchy looks like:

      Unity Scene Hierarchy: Original Structure
      That's it. No magic. 4-5 levels of depth, plus all the bones required for the characters. Nothing too crazy apart from the 300 simple characters, which probably accounts for all the missing pieces that a real game has.
      Have a look at World. It's a single root game object containing way too many children. This fact violates the first golden rule of an efficient unity scene hierarchy.
      Is that bad? I don't know. The way to find out is by measuring its relative cost.
      I'm using free assets I found in the asset store. That means, I can't upload this project to GitHub or I'll risk ending up in jail. Yeah, thanks for that restrictive license, Unity.
      Profiling the unoptimized scene
      And so I start the scene. Not much happening, just a slight amount of movement. I cannot say this is the funniest game I ever played.

      Sample project "gameplay"
      Well, I'm not an artist or designer. I'm excused for the looks but not for the performance. Point which brings me to using the profiler now.
      I captured a 300-frame profile and here's what I got:

      Unity Scene Hierarchy: Pre-Optimization
      Was that useful?
      Nah, don't even bother to look at the image.
      We don't know whether that's good or bad, because we don't have a reference point.
      But we can compare... We can compare against an equivalent, flattened hierarchy.
      Let's try that out.
      Flattening our scene hierarchy
      Analyzing the previous hierarchy, we can notice something of interest.
      Most of the introduced game objects are there for organization purposes in this setup. That means, some added hierarchy levels are useful for developers to structure content around them. We incorporate these objects to make development easier.
      Game objects such as World, City, Props don't serve any other purpose than organizing. Our characters, particles, UI and props do not really depend on those to accomplish their goals.
      I see a potential gain here.
      But on the other side, we don't want to break the only organizational tool we have. I don't want to deal with a flat hierarchy during development. That sucks.
      We want to keep things structured on the editor and yet we want our game to be performant on run-time.
      Ehmm... Is this possible?
      You bet it is. That we can do by making use of the script you downloaded in the first part of the blog series: DetachGameObject. This script will let you maintain the original hierarchy when you develop your game but will unparent the gameobject of your choice to squeeze all its performance juice on run-time.
      So I'll add right now our DetachGameObject component to the Character prefab, to all particle systems and to the dynamic canvas we have in place. I'll ask our component to unparent the game object after a delay of 15 seconds so I can take two profiles: one before detaching and another after it.
      Below you find the DetachGameObject asset applied to an example particle effect.

      DetachGameObject Performance Booster
      Now that I have set up all DetachGameObject components, there's only one thing remaining. That's right, press the play button!
      So I run the game and after 15 seconds...

      Scene Hierarchy: Flatter Optimized Version
      Boom.
      All my characters, particles and UI have been detached. Now the hierarchy is much flatter.
      So I wonder...
      How do both profiles compare? Let's use the neat profile analyzer to get some fresh numbers.
      *drums*

      Profile Comparison: Deeper vs. Boosted Flatter CPU Performance
      I'll translate for you what this chart means...
      This comparison says that there're significant differences between the deeper and flatter hierarchies.
      The flatter hierarchy improves performance significantly over the deeper one.
      Yes, you might not have 300 characters, but you will surely have many over 100 times more complexity in real gameplay elements, scripts, networking and such.
      So what?
      The conclusion is simple: there's a performance penalty you're paying if you don't have a flat hierarchy. Can you afford it? That's a question only you can answer. And you better answer that with data and metrics. My metrics in my previous games always reach the same conclusion: I don't want to pay that expensive bill. I let instead DetachGameObject pay it for me.
      That's my advice to you. Measure your game through the profiler and FAP Hierarchy Tool. Both tools will immensely help you finding scene hierarchy bottlenecks in your Unity game.
      Always remember...
      Flattening your hierarchy will improve your Unity CPU performance.
      What were your scores before and after flattening your hierarchy? Share below.
    • By logicandchaos
      If you are making a clicker or hyper casual game and need to manipulate and display extremely large numbers then my asset might be just what you need. You can work with numbers upto 10^123 decimal places! It's on the asset store here: https://assetstore.unity.com/packages/tools/integration/largenumberclass-141445 

    • By zwolya
      Trade your way around the Bakersville Baking Convention in your quest to claim THE GOLDEN MUFFIN!
       
      CLICK on the different BOOTHS to see the muffins they have available, and what they are willing to TRADE them for. Make your INVENTORY match Chef Jacques’ as quickly as possible to claim THE GOLDEN MUFFIN!
       
      Read more and play for free at https://www.zwolya.com/the-golden-muffin
       
       

    • By zwolya
      Trade your way around the Bakersville Baking Convention in your quest to claim THE GOLDEN MUFFIN!
       
      CLICK on the different BOOTHS to see the muffins they have available, and what they are willing to TRADE them for. Make your INVENTORY match Chef Jacques’ as quickly as possible to claim THE GOLDEN MUFFIN!
       
      Read more and play for free at https://www.zwolya.com/the-golden-muffin

    • By joe123
      I'm a software engineer and i have entered game development field (Unity) for the last couple of months . I have made a couple of games on android using Unity that are only at concept stage now. And i mean by concept stage , that i'm focusing only currently on game play , not graphics and levels and anything else now. The games i have made are similar to (Color Switch) on mobile (The game has no levels , but gets harder with higher score or time). I'm making games with least resources now then decide which game i should focus on after getting feedback from users. I need guidance for the next steps that i should be doing. Like for example should i focus on one game and expand it ? Should i publish and market all of these games with their current state then get feedback with the best game then focus on it ? Should i use ASO (app store optimization) now  ....etc What i think i need is a game product manager , or an experienced indie game developer that can guide me what should i do for the next step. For anyone who has experienced a state similar to mine , can you share with us some knowledge about how to manage a game in means of marketing and everything else (not code related) ? To confirm my question , I'm not asking for example how to market my games , but i'm asking at which stage i should start marketing my games...etc I know answering this question could be big , but i need just some guide lines for now.
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!