Books
Chris Dickinson

Unity 5 Game Optimization

  • Timur Ahmetovhas quoted7 years ago
    In fact, creating Poolable Components that are not self-contained, and tend to tinker with other Components like this, is one of the biggest dangers of implementing a pooling system. We should minimize such implementations, and routinely verify them when we're trying to debug strange issues in our game.
  • Timur Ahmetovhas quoted7 years ago
    Prefab pooling
    The previous pooling solution is useful for typical classes, but it won't work for special Unity objects, such as GameObject and MonoBehaviour. These objects tend to consume a large chunk of our runtime memory, can cost us a great deal of CPU usage when they're created and destroyed, and tend to risk a large amount of garbage collection at runtime. In other words, the main goal of Prefab pooling is to push the overwhelming majority of object instantiation to Scene initialization, rather than letting them get created at runtime. This can provide some big runtime CPU savings, and avoids a lot of spikes caused by object creation/destruction and garbage collection, at the expense of Scene loading times, and runtime memory consumption. As a result, there are quite a few pooling solutions available on the Asset Store for handling this task, with varying degrees of simplicity, quality, and feature sets.
  • Timur Ahmetovhas quoted7 years ago
    Note
    It is often recommended that pooling should be implemented in any game that intends to deploy on mobile devices, due to the greater overhead costs involved in the allocation and deallocation of memory compared to desktop applications.
    However, creating a pooling solution is an interesting topic, and building one from scratch is a great way of getting to grips with a lot of important internal Unity Engine behavior. Also, knowing how such a system is built makes it easier to extend if we wish it to meet the needs of our particular game, rather than relying on a prebuilt solution.
    The general idea of Prefab pooling is to create a system that contains lists of active and inactive GameObjects that were all instantiated from the same Prefab reference. The following diagram shows how the system might look after several spawns, despawns, and respawns of various objects derived from four different Prefabs (Orc, Troll, Ogre, and Dragon):
  • Timur Ahmetovhas quoted7 years ago
    There are also two big features in the .NET library that often become big performance hogs whenever they're used. This tends to be because they are only included as a quick-and-hacky solution to a given problem without much effort put into optimization. These features are LINQ and Regular Expressions.
    LINQ provides a way to treat arrays of data as miniature databases and perform queries against them using SQL-like syntax. The simplicity of its coding style, and complexity of the underlying system (through its usage of Closures), implies that it has a fairly large overhead cost. LINQ is a handy tool, but is not really intended for high-performance, real-time applications such as games, and does not even function on platforms that do not support JIT compilation, such as iOS.
    Meanwhile, Regular Expressions, using the
    Regex
    class, allow us to perform complex string parsing to find substrings that match a particular format, replace pieces of a string, or construct strings from various inputs. Regular Expression is another very useful tool, but tends to be overused in places where it is largely unnecessary, or in seemingly "clever" ways to implement a feature such as text localization, when straightforward string replacement would be far more efficient.
  • Timur Ahmetovhas quoted7 years ago
    There are also two big features in the .NET library that often become big performance hogs whenever they're used. This tends to be because they are only included as a quick-and-hacky solution to a given problem without much effort put into optimization. These features are LINQ and Regular Expressions.
    LINQ provides a way to treat arrays of data as miniature databases and perform queries against them using SQL-like syntax. The simplicity of its coding style, and complexity of the underlying system (through its usage of Closures), implies that it has a fairly large overhead cost. LINQ is a handy tool, but is not really intended for high-performance, real-time applications such as games, and does not even function on platforms that do not support JIT compilation, such as iOS.
    Meanwhile, Regular Expressions, using the
    Regex
    class, allow us to perform complex string parsing to find substrings that match a particular format, replace pieces of a string, or construct strings from various inputs. Regular Expression is another very useful tool, but tends to be overused in places where it is largely unnecessary, or in seemingly "clever" ways to implement a feature such as text localization, when straightforward string replacement would be far more efficient.
  • Timur Ahmetovhas quoted7 years ago
    Coroutines
    Starting a Coroutine costs a small amount of memory to begin with, but note that no further costs are incurred when the method yields. If memory consumption and garbage collection are significant concerns, we should try to avoid having too many short-lived Coroutines, and avoid calling
    StartCoroutine()
    too much during runtime.
  • Timur Ahmetovhas quoted7 years ago
    Value types and Reference types
  • Timur Ahmetovhas quoted7 years ago
    There are several instructions within the Unity API which result in heap memory allocations, which we should be aware of. This essentially includes everything that returns an array of data. For example, the following methods allocate memory on the heap:
    GetComponents<T>(); // (T[])
    Mesh.vertices; // (Vector3[])
    Camera.allCameras; // (Camera[])
    Such methods should be avoided whenever possible, or at the very least called once and cached so that we don't cause memory allocations more often than necessary.
  • Timur Ahmetovhas quoted7 years ago
    Because strings are essentially arrays of characters (chars), they are Reference types, and follow all of the same rules as other Reference types; the value that is actually copied and passed between functions is merely a pointer, and they will be allocated on the heap.
    The confusing part really begins when we discover that strings are immutable, meaning they cannot be changed after they've been allocated. Being an array implies that the entire list of characters must be contiguous in memory, which cannot be true if we allowed strings to expand or contract at-will within a dynamic memory space (how could we quickly and safely expand the string if we've allocated something else immediately after it?).
    This means that, if a string is modified, then a new string must be allocated to replace it, where the contents of the original will be copied and modified as-needed into a whole new character array. In which case, the old version will no longer be referenced anywhere, will not be marked during mark-and-sweep and will therefore eventually be garbage-collected. As a result, lazy string programming can result in a lot of unnecessary heap allocations and garbage collection.
  • Timur Ahmetovhas quoted7 years ago
    Value types are normally allocated on the stack. Primitive data types such as bools, ints, and floats are examples of Value types, but only if they're standalone and not a member of a Reference type. As soon as a primitive data types is contained within a Reference type, such as a class or an array, then it is implied that it is either too large for the stack or will need to survive longer than the current scope and must be allocated on the heap instead.
    All of this can be best explained through examples. The following code will create an integer as a Value type that exists on the stack only temporarily:
    public class TestComponent : MonoBehaviour {
    void Start() {
    int data = 5; // allocated on the stack
    DoSomething(data);
    } // integer is deallocated from the stack here
    }
    As soon as the
    Start()
    method ends, then the integer is deallocated from the stack. This is essentially a free operation since, as mentioned previously, it doesn't bother doing any cleanup; it just moves the stack pointer back to the previous memory location in the call stack. Any future stack allocations simply overwrite the old data. Most importantly, no heap allocation took place to create the data, and so, the Garbage Collector would be completely unaware of its existence.
    But, if we created an integer as a member variable of the
    MonoBehaviour
    class definition, then it is now contained within a Reference type (a class) and must be allocated on the heap along with its container:
    public class TestComponent : MonoBehaviour {
    private int _data = 5;
    void Start() {
    DoSomething(_data);
    }
    }
    Similarly, if we put the integer into an independent class, then the rules for Reference types still apply, and the object is allocated on the heap:
    public class TestData {
    public int data = 5;
    }
    public class TestComponent : MonoBehaviour {
    void Start() {
    TestData dataObj = new TestData(); // allocated on the heap
    DoSomething(dataObj.data);
    } // 'dataObj' is not deallocated here, but it will become a candidate during the next garbage collection
    }
    So, there is a big difference between temporarily allocating memory within a class method and storing long-term data in a class' member data. In both cases, we're using a Reference type (a class) to store the data, which means it can be referenced elsewhere.
fb2epub
Drag & drop your files (not more than 5 at once)