Unity3d 游戏优化 mono等

#Unity3D optimization tips

Some code allocates memory when running in the editor and not on the device. This is due to Unity doing some extra error handling so possible error messages can be output in the console. Much of this code is stripped during build. It's therefore important to profile on device, and do it regularly.

Optimizations often makes code more rigid and harder to reason. Don't do it until you really need to.

When profiling, wrap methods or portions of a method in Profiler.BeginSample(string name) and Profiler.EndSample() to make them easier to find in the profiler.

public class SomeClass {
	
	public void Update() {
		// Misc. code here ...

		Profiler.BeginSample("Heavy calculations");
		// Heavy calculations
		Profiler.EndSample();

		// More misc.code here
	}
}

##Enum as key in a dictionary

Enums can be used as keys in a dictionary, but they require some setup to avoid littering the heap.

Mono will do boxing on the enum when comparing dictionary keys. Looking up an item in a dictionary via ContainsKey, TryGetValue or using array notation on the dictionary will allocate objects on the heap.

The boxing happens because of Mono's default equal comparer, and can be fixed with a custom IEqualityComparer. The downside is that you need to implement an equal comparer for each of your enums. See the discussion on Stack Overflow and the blog post from Somasim for a more in-depth explanation.

Enums are backed by integers and can therefore be cast to and from ints. You can make a utility method that handles the casting for you, like this:

public Something GetSomething(SomeEnum e) {
	return someDictionary[(int)e];
}

With a method like this, you can safely redefine your dictionary to use an int as key, avoid the allocation problems and still have the readability of using an Enum.

###References

  • http://stackoverflow.com/a/26281533
  • http://www.somasim.com/blog/2015/08/c-performance-tips-for-unity-part-2-structs-and-enums/

##Struct as key in a dictionary

Just like with enums, structs will get boxed and put on the heap when used as keys in a dictionary. The solution is to have the struct implement IEquatable, override GetHashCode(), Equals() and the == and != operators to use strongly typed equality comparisons.

using System;
using UnityEngine;

public struct MyStruct : IEquatable {
	public readonly int x;
    public readonly int y;
    public readonly int z;

    public MyStruct(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public override bool Equals(System.Object obj) {
        if (!(obj is MyStruct)) {
            return false;
        }

        MyStruct i = (MyStruct)obj;
        return (x == i.x) && (y == i.y) && (z == i.z);
    }

    public bool Equals(MyStruct i) {
        return (x == i.x) && (y == i.y) && (z == i.z);
    }

    public override int GetHashCode() {
        return x ^ y ^ z;
    }

    public static bool operator == (MyStruct a, MyStruct b) {
        return a.x == b.x && a.y == b.y && a.z == b.z;
    }

    public static bool operator != (MyStruct a, MyStruct b) {
        return !(a == b);
    }
}

###References

  • http://www.somasim.com/blog/2015/08/c-performance-tips-for-unity-part-2-structs-and-enums/

##Lists

Iterating a list using foreach creates a new instance of an Enumerator. Mono discards the enumerator when done iterating, thus causing unnecessary memory allocations. Tests shows that some cases don't cause allocations, such as using foreach on a generic list and dictionary. See the link to Jackson Dunstan's blog.

When developing, default to using foreach because it is easier to read and work with. Replace with a for loop only when you see the need for it, which you'll discover during optimization. If you do replace foreach with for, cache the length of the array before starting the iteration.

Any object references you make during iteration should use objects defined outside the loop to prevent allocations, like this:

SomeClass someInstance = null;
for (int i=0; i<10; i0++) {
	someInstance = someList[i];
}

In any case, preallocate slots for arrays, lists and dictionarys when instantiating them. Pick the maximum number of items you expect to fill the list with.

When instantiating a list without preallocating slots, you allocate only space for the list itself. When you call list.Add(), the application will find new memory space to hold the entire list -including the newly added item, copy the old list to the new space, add the item, and point the list reference to the new memory address. You can easily avoid this by preallocating multiple slots.

private List list;

void Start() {
	list = new List(25);
}

###Fun facts

Did you know that Dictionary.Keys and Dictionary.Values are not lists but classes? Simply accessing a dictionary's Keys property allocates some bytes on the heap.

###References

  • http://jacksondunstan.com/articles/3250

##Branching

A modern CPU tries to stay ahead of the script's execution point and precalculates as many lines ahead as possible. When reaching an if statement it uses branch prediction to either decide early on what branch to take, or calculate both branches to be safe. The more complex and harder to predict your branch is, the more likely the CPU will precalculate both branches.

If your code features lots of branches, and is hard to rewrite, consider using a behaviour tree instead. Make sure the behaviour tree implementation doesn't use any if statements, and that none of the behaviour tree's nodes use any.

Most branches can be avoided using simple arithmetics and lists. I managed to reduce the runtime of a function from 22ms to 2ms by avoiding a few branches. It was a busy function, getting called around 2.900 times per update.

Avoiding branches is also a good practice because it often results in code that's easier to read and maintain.

###Example

Call one function when a is equal to b, and another when they're not.

public class Example01 {
	
	public void Run(int a, int b) {
		if (a == b) {
			Oneway();
		} else {
			TheOther();
		}
	}
	
	private void OneWay() {}
	
	private void TheOther() {}
}

We can avoid the branch using an array of functions, and some simple arithmetics to figure out which branch to chose.

public class Example01 {
	
	private Action[] actions;
	
	public Example01() {
		actions = new Action[2] { OneWay, TheOther };
	}
	
	public void Run(int a, int b) {
		int branch = Min(Math.Abs(b - a), 1);
		actions[branch]();
	}
	
	private void OneWay() {}
	
	private void TheOther() {}
	
	private int Min(int a, int b) {
		return (a + b - Mathf.Abs(a-b)) * 0.5f;
	}
	
	private int Max(int a, int b) {
		return (a + b + Mathf.Abs(a-b)) * 0.5f;
	}
}

To calculate the array index of the correct function, we first get the absolute value of the difference between a and b. This limits the range from 0 to the maximum value of Int32. Lastly we clamp that value between 0 and 1 using the Min method. I left the Max method in there just so you can see how it compares to Min.

When a equals b, the difference is 0, and we use the first function in the actions array. For all other values the second function is used.

##Strings

Don't use strings for anything but displaying text in the UI or logging.

It's better to cache strings in a list and pass a pointer to the string to the UI element, than passing a raw string.

public class StringCaching {
	
	private UIElement uiElm;
	private string[] labels;
	
	public StringCaching() {
		labels = new string[2] {
			"Hello world",
			"Hello there"
		};
	}
	
	// This is bad
	public void RawString() {
		uiElm.label = "Hello world";
	}
	
	// This is good
	public void CachedString() {
		uiElm.label = labels[0];
	}
}

When doing string concatenation, use a StringBuilder. A StringBuilder can be reused, but be aware that emptying a StringBuilder causes memory allocations. In .net 2.5 you don't have access to StringBuilder's Clear method. Your only options are using Replace or setting the Length to 0. These methods causes memory allocations that seem to be proportional to the size of the string.

If you have a string with a constant length, like a timer that shows minutes and seconds (MM:SS), you should try using a character array instead.

public class Timer {

	private char[] digits;
	private UIElement uiElm;
	
	public Timer() {
		digits = new char[5];
		digits[2] = ':';
	}
	
	public void Update() {
		digits[0] = minutesTen;
		digits[1] = minutes;
		digits[3] = secondsTen;
		digits[4] = seconds;
		uiElm = new string(char);
	}
}

Depending on the length of your string, and how much you need to change, using a character array might not be the solution for you. Test using the profiler to see what causes the least amount of allocations in your case.

Don't use strings for comparisons. If you're searching for an object named something, see if you can use an Enum, or an integer ID instead.

##Recycling

Use object pools so that you can reuse frequently used objects without the cost of a new allocation.

Make objects reusable by implementing a Reset method. A good practice is to use the Reset method in the constructor. That way you can be sure that as long as the object is in the right state when initialized, it will also be when resetting.

public class Poolable {
	
	private int value;
	
	public Poolable() {
		Reset();
	}
	
	public void Reset() {
		value = 0;
	}
}

Objects like game entities, sound effects, even messaging objects are good candidates for object pools.

##Meshes

When changing vertex colors of a mesh, use MeshRenderer's additionalVertexStreams to add a per mesh instance override. This way you'll prevent creating new instances of each mesh.

##Delegates

When declaring delegates, default to an empty function, or cache an empty function to avoid the extra null check. Simplifes code and is better for branch prediction.

public class DelegateCaching {
	
	private Action nullAction = () => {};
	private Action callback;
	
	public DelegateCaching() {
		callback = nullAction;
	}
	
	public void Update() {
		callback();
	}
}

In this example we create an empty function using a lambda expression and point callback to it. NullAction could also be private method as longs as it has the correct signature. In Update() we don't need to check if callback is null.

Be however careful with the lambdas. Mono will cache your lambdas, which makes them safe to use. But can easily create a closure by making a reference to an external variable or function, and that breaks caching. Mono will in those cases generate a new class on the fly, each time the lambda is called. The newly generated class pollutes the heap and stresses the garbage collector.

Don't do like this:

public class LambdaCaching {
	
	private List list;
	
	public LambdaCaching() {
		list = new List(10);
		list.Add(new MyObject());
		// etc.
	}
	
	public void Update() {
		int l = list.Count;
		for (int i=0; i {
				myObj.DoSomething(i);
			});
		}
	}
	
	private void Callback(int i, Action callback) {
		callback(list[i]);
	}
}

The bad happening in this example is the referencing of the i int inside the lambda expression. Mono will create a new class with the i int as a private field, ten times each update.

##Prefabs loaded via Resources

Unload prefabs from memory after loading them from Resources.

public class Loader {

	public void Load(string asset) {
		GameObject prefab = Resources.Load(asset);
		GameObject instance = GameObject.Instantiate(prefab);
		Resources.UnloadAsset(prefab);
	}
}

When loading prefabs from Resources you always instantiate them, otherwise you'll start to make changes to the prefab itself. Since instantiating duplicates the object there's no reason to keep the prefab around.

##Caching array length

Array.Length doesn't need caching, but List.Count does. Check this out: http://jacksondunstan.com/articles/3577

##Data structures

All though not directly a tips for optimization, the data structure will largely affect how much of your code you actually can optimize. It's a task well worth spending time on to get right.

TL;DR: Keep it simple!

Data should be easy to work with. The API should help you get the right data with the least amount of input, at the shortest time possible. Keep the API simple to make it easy to learn, but add as many function overrides as you need to make it flexible.

Decide on a set of basic data blocks and design the API around them. In a game of pool a ball could be a basic data block. The table holds a list of balls, and the pockets are functions that accepts a ball as parameter.

You should be able to pass the data blocks you get from the API back to it without the need to transform them. In an adventure game, when a character picks up an item from the ground, it shouldn't need to create a new instance of an object just to be able to add it to the inventory.

When deciding on a data structure, prioritize the simplest first. If you don't need a resizable list, choose an Array instead. If you need a more complex structure, add helper methods to make it easier for yourself and your peers to work with the data.

Keep scalability in mind when designing the data structure, but don't overdo it. The more scalable the data is, the harder it is to work with. Scalable data means the possibility to add more data without breaking the API or the structure.

Whenever you want to add more properties to your data, think it through. Can you get the information you're looking for from other properties? Can you merge the new property with another to keep the API simple? In a platformer game the characters probably have a health property. Instead of adding a boolean called isDead, can you instead check for health == 0? Always go for the least amount of data that you need, and pick the data that gives the most meaning.

##Good practice

Gain control over the garbage collector by doing large cleanups when the game is not doing anything important, like in a menu scene, and avoid creating allocations when the game is running.

Keep classes small and simple. It'll make it easier to reason the code and fix bugs. When class gets larger than 300 lines, split it. In cases where you need to make a large class, split it using a partial class.

Splitting code into smaller functions makes it easier to reuse code and identify bottlenecks during profiling. Reusable functions also reduces the need to write more code, which results in less time spent hunting bugs.

Write code in decoupled, self contained classes with methods that accepts as few parameters as possible and have no hard dependencies to other classes to make it easier to run isolated tests. Your development cycle will be slow if you always need to compile the entire project and play up to a specific point in the game to test a certain feature. Make it easy to either mock data or load data from disk.

##More stuff on Unity optimizations

  • http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php
  • http://www.gamasutra.com/blogs/WendelinReich/20131119/203842/C_Memory_Management_for_Unity_Developers_part_2_of_3.php
  • http://www.gamasutra.com/blogs/WendelinReich/20131127/203843/C_Memory_Management_for_Unity_Developers_part_3_of_3.php
  • http://www.somasim.com/blog/2015/04/csharp-memory-and-performance-tips-for-unity/
  • http://www.somasim.com/blog/2015/08/c-performance-tips-for-unity-part-2-structs-and-enums/
  • https://andrewfray.wordpress.com/2013/02/04/reducing-memory-usage-in-unity-c-and-netmono/

你可能感兴趣的:(Unity3d优化相关)