FINAL STAGE OF CODING!
///////////////////////////////
So, I ran into an unexpected problem in the UNITY editor =
When activating 6+ atoms, the program suddenly and randomly freezes. It doesn't return an error! The background music continues playing, but the editor freezes in runtime mode...
This ERROR doesn't work for me ))) The mega-tracker will eventually have many atoms = it was precisely for such an UNLIMITED structure that everything was planned.
I found a good article on project optimization... I only followed the first two rules...
and the ERROR DISAPPEARED!
I'll save this article just in case )))
///////////////////////////////
First rule: no new objects in Update methods
Ideally, the Update, FixedUpdate, and LateUpdate methods shouldn't contain the "new" keyword. You should always use what you already have.
Sometimes creating a new object is hidden in some internal Unity methods, making it less obvious. We'll cover this later.
Second rule: create once and use again!
Essentially, this means that memory for everything possible should be allocated in the Start and Awake methods. This rule is very similar to the first one. In fact, it's just another way of eliminating the "new" keywords from the Update methods.
Code that:
creates new instances and searches for some game objects
You should always try to move it from Update to Start or Awake methods.
Here are examples of the changes we made:
Allocating memory for lists in the Start method, clearing them (Clear) and reusing them if necessary.
//Bad codeprivate List objectsList;void Update(){
objectsList = new List(); objectsList.Add(......)
}
//Better Codeprivate List objectsList;void Start(){
objectsList = new List();}
void Update(){
objectsList.Clear();
objectsList.Add(......)
}
Storing links and reusing them like this:
//Bad codevoid Update(){
var levelObstacles = FindObjectsOfType(); foreach(var obstacle in levelObstacles) { ....... }}
//Better codeprivate Object[] levelObstacles;void Start(){
levelObstacles = FindObjectsOfType();
}
void Update(){
foreach(var obstacle in levelObstacles) { ....... }}
The same applies to the FindGameObjectsWithTag method or any other method that returns a new array.
Rule 3: Beware of strings and avoid concatenating them
When it comes to garbage production, strings are terrible. Even the simplest string operations can generate a lot of garbage. Why? Strings are just arrays, and these arrays are immutable. This means that every time you concatenate two strings, a new array is created and the old one becomes garbage. Luckily, you can use StringBuilder to avoid or minimize this garbage production.
Here's an example of how the situation can be improved:
//Bad codevoid Start(){
text = GetComponent();
}
void Update(){
text.text = "Player " + name + " has score " + score.toString();}
//Better codevoid Start(){
text = GetComponent();
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 above is fine, but there's still plenty of room for improvement. As you can see, almost the entire string can be considered static. We split the string into two parts, each for two UI.Text objects. One part contains just the static text "Player " + name + " has score ", which can be assigned in the Start method, and the other contains the score value, which is updated every frame. Always make static strings truly static and generate them in the Start or Awake method. After this improvement, almost everything is fine, but some garbage is still generated when calling Int.ToString(), Float.ToString(), etc.
We solved this problem by generating and pre-allocating memory for all possible strings. This may seem like a silly waste of memory, but it perfectly suits our needs and completely solves the problem. So, in the end, we have a static array that can be accessed directly using indexes to select the desired string representing the number:
public static readonly string[] NUMBERS_THREE_DECIMAL = { "000", "001", "002", "003", "004", "005", "006",.........
Rule 4: Cache values returned by access methods
This can be very tricky because even a simple accessor like the one below generates garbage:
//Bad Codevoid Update(){
gameObject.tag;
//or gameObject.name;
}
Try to avoid using access methods in the Update method. Call the access method only once in the Start method and cache the return value.
In general, I recommend NOT calling any row access methods or array access methods in the Update method. In most cases, getting a reference once in the Start method is sufficient.
Here are two more common examples of more unoptimized accessor code:
//Bad Codevoid Update(){
//Allocates new array containing all touches Input.touches[0];}
//Better Codevoid Update(){
Input.GetTouch(0);}
//Bad Codevoid Update(){
//Returns new string(garbage) and compare the two strings gameObject.Tag == "MyTag";}
//Better Codevoid Update(){
gameObject.CompareTag("MyTag");}
Rule 5: Use functions that don't allocate memory
Some Unity functions can be found as alternatives that don't allocate memory. In our case, all of these functions are physics-related. Our collision detection is based on
Physics2D.CircleCast();
For this particular case, you can find a non-allocating function called
Physics2D. CircleCastNonAlloc();
Many other functions also have similar alternatives, so always check the documentation for NonAlloc functions.
Rule 6: Don't use LINQ
Just don't do it. I mean, don't use it in any code that runs frequently. I know LINQ makes code easier to read, but in many cases, the performance and memory allocation of such code are terrible. Of course, it can be used occasionally, but frankly, we don't use LINQ at all in our game.
Rule 7: Build Once, Reuse Again, Part 2
This time we are talking about object pooling.
In our case, we use the following object pooling scenario. We have a generated level filled with obstacles that exist for a certain period of time until the player completes that section of the level. Instances of these obstacles are created from prefabs when certain conditions are met. The code is located in the Update method. This code is completely inefficient in terms of memory and execution time. We solved this problem by generating a pool of 40 obstacles: we retrieve obstacles from the pool when needed and return the object back to the pool when it is no longer needed.
Rule number eight: be careful with packaging and transformation (Boxing)!
Boxing generates garbage! But what is boxing? Most often, boxing occurs when you pass a value type (int, float, bool, etc.) to a function that expects an Object parameter.
Here's an example of boxing that we need to fix in our project:
We implemented our own messaging system in the project. Each message can contain an unlimited amount of data. The data is stored in a dictionary defined as follows:
Dictionary data;
We also have a setter that sets values in this dictionary:
public Action SetAttribute(string attribute, object value){
data[attribute] = value;}
Boxing here is pretty obvious. You can call the function like this:
SetAttribute("my_int_value", 12);
Then the value "12" is boxed and this generates garbage.
We solved the problem by creating separate data containers for each primitive type, and the previous Object container is used only for reference types.
Dictionary data;Dictionary dataBool;Dictionary dataInt;.......
We also have separate setters for each data type:
SetBoolAttribute(string attribute, bool value)SetIntAttribute(string attribute, int value)
And all these setters are implemented in such a way that they call the same generic function:
SetAttribute(ref Dictionary dict, string attribute, T value)
The boxing problem is solved!
Rule number nine: cycles are always suspect
This rule is very similar to the first and second. Simply try to remove all unnecessary code from loops for performance and memory allocation reasons.
In general, we want to avoid loops in Update methods, but if they're unavoidable, we at least want to avoid any memory allocations in such loops. So, follow rules 1–8 and apply them to loops in general, not just in Update methods.
Rule 10: No garbage in external libraries
If it turns out that some of the garbage is generated by code downloaded from the Asset Store, there are many possible solutions. But before reverse engineering and debugging, simply visit the Asset Store again and update the library. In our case, all the assets we were using were still supported by their authors, who continued to release performance-improving updates, so this solved all our problems. Dependencies must be up-to-date! I'd rather get rid of a library than keep an unsupported one.
Part 2: Minimizing Execution Time
Some of the rules presented above make a subtle difference if the code is called infrequently. Our code has one large loop that runs every frame, so even these small changes made a huge difference.
Some of these changes, if used incorrectly or in the wrong situation, can lead to even worse execution times. Always check the profiler after making any optimizations to ensure you're moving in the right direction.
Frankly, some of these rules result in much less readable code, and sometimes even violate best practices, such as the code inlining mentioned in one of the rules below.
Many of these rules overlap with those presented in the first part of the article. Typically, garbage-producing code performs worse than non-garbage-producing code.
The first rule: the correct order of execution
Move code from the FixedUpdate, Update, and LateUpdate methods to the Start and Awake methods. I know it sounds crazy, but trust me, if you dig into your code, you'll find hundreds of lines of code that can be moved to methods that are executed only once.
In our case, such code is usually associated with
Calls to GetComponent Calculations that actually return the same result every frame Multiple instantiations of the same objects, usually lists Finding GameObjects Getting references to Transforms and using other access methods
Here's a list of code examples we moved from Update methods to Start methods:
//There must be a good reason to keep GetComponent in UpdategameObject.GetComponent();
gameObject.GetComponent();
//Examples of calculations returning the same result every frameMathf.FloorToInt(Screen.width / 2);var width = 2f * mainCamera.orthographicSize * mainCamera.aspect;var castRadius = circleCollider.radius * transform.lossyScale.x;var halfSize = GetComponent().bounds.size.x / 2f;//Finding objectsvar levelObstacles = FindObjectsOfType();var levelCollectibles = FindGameObjectsWithTag("COLLECTIBLE");//ReferencesobjectTransform = gameObject.transform;
mainCamera = Camera.main;
Rule 2: Only execute code when necessary.
In our case, this mostly concerned UI update scripts. Here's an example of how we modified the implementation of the code displaying the current state of collectibles in a level.
//Bad codeText text;
GameState gameState;
void Start(){
gameState = StoreProvider.Get();
text = GetComponent();
}
void Update(){
text.text = gameState.CollectedCollectibles.ToString();
}
Since each level only has a few collectibles, it doesn't make sense to change the UI text every frame. Therefore, we only update the text when the number changes.
//Better codeText text;
GameState gameState;
int collectiblesCount;void Start(){
gameState = StoreProvider.Get();
text = GetComponent();
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();
}
}
This code is much better, especially if the actions are much more complex than simply changing the UI.
However, this still wasn't enough for us, and we wanted to implement a completely general solution, so we created a library implementing Flux in Unity. This resulted in a very simple solution in which the entire game state is stored in a "Store" object, and all UI elements and other components are notified when the state changes and react to this change without requiring code in the Update method.
Rule number three: cycles are always suspect
This is exactly the same rule I mentioned in the first part of the article. If your code contains a loop that iterates over a large number of elements, use both rules from both parts of the article to improve the loop's performance.
Rule 4: For is better than Foreach
The Foreach loop is very easy to write, but "very difficult" to execute. Within the Foreach loop, Enumerators are used to iterate through a data set and return a value. This is more complex than iterating through indices in a simple For loop.
Therefore, in our project, we replaced Foreach loops with For loops whenever possible:
//Bad codeforeach (GameObject obstacle in obstacles)//Better codevar count = obstacles.Count;for (int i = 0; i < count; i++) { obstacles;
}
In our case with a large for loop, this change is very significant. A simple for loop sped up the code by a factor of two.
Rule 5: Arrays are better than lists
In our code, we discovered that most lists have a constant length, or we can calculate the maximum number of elements. So, we reimplemented them using arrays, and in some cases, this resulted in a two-fold increase in data iteration speed.
In some cases, lists or other complex data structures are unavoidable. Sometimes you need to frequently add or remove elements, in which case lists are a better choice. But in general, for fixed-length lists, arrays should always be used.
Rule 6: Float operations are better than vector operations
This difference is barely noticeable unless you're doing thousands of these operations, as we were, so the performance improvement was significant for us.
We made changes like these:
Vector3 pos1 = new Vector3(1,2,3);Vector3 pos2 = new Vector3(4,5,6);//Bad codevar pos3 = pos1 + pos2;//Better codevar pos3 = new Vector3(pos1.x + pos2.x, pos1.y + pos2.y, ......);Vector3 pos1 = new Vector3(1,2,3);//Bad codevar pos2 = pos1 * 2f;//Better codevar pos2 = new Vector3(pos1.x * 2f, pos1.y * 2f, ......);
Rule 7: Look for objects correctly
Always consider whether you really need to use the GameObject.Find() method. This method is heavy and takes an insane amount of time. It should never be used in Update methods. We've found that most of our Find calls can be replaced with direct links in the editor, which is, of course, much better.
//Bad CodeGameObject player;
void Start(){
player = GameObject.Find("PLAYER");}
//Better Code//Assign the reference to the player object in editor[SerializeField]GameObject player;
void Start(){
}
If this is not possible, then at least consider using tags and searching for an object by its tag using GameObject.FindWithTag.
So, in general: Direct link > GameObject.FindWithTag() > GameObject.Find()
Rule 8: Work only with relevant objects
In our case, this was important for collision detection using RayCasts (CircleCast, etc.). Instead of detecting collisions and deciding which ones are important in code, we moved game objects to the appropriate layers so that collisions could be calculated only for the relevant objects.
Here is an example
//Bad Codevoid DetectCollision(){
var count = Physics2D.CircleCastNonAlloc( position, radius, direction, results, distance);
for (int i = 0; i < count; i++) { var obj = results.collider.transform.gameObject; if(obj.CompareTag("FOO")) { ProcessCollision(results);
}
}
}
//Better Code//We added all objects with tag FOO into the same layervoid DetectCollision(){
//8 is number of the desired layer var mask = 1
►PERFECT RMT PLAYER Loading… ███████[][][] 70%