[Unity教程] Unity性能优化—数据集合的最佳使用

发表于2016-10-02
评论0 3.8k浏览
目标
  这篇文章的主要目标是:过详细的理解Array、List、和Dictionaries,让你的游戏运行速度快十倍。
  我应该使用Array吗?我应该使用List吗?噢 ,等等,或者使用Dictionaries?
  为什么我的循环花了那么长时间呢? ?
  在我代码中看不到任何的错误,那么为什么我的代码执行那么慢?
  为什么查找我想要的对象话费那么长时间?
  我看不到任何的GC(垃圾回收器)处理,然而我的游戏为什么那么的延迟?
  “这些是我们在开发一个游戏的时候经常遇到的一些常见问题。”
  但正是这些常见问题最让游戏开发者苦恼了!!
  几毫秒的延迟就可以让游戏开发者失去理智!!
  最终,我们总是推卸责任喊道:“这个是引擎的错误,我的代码是完美的!!”
  那好吧,这并非总是如此,只是有的时候我们不正确使用数据结构中的集合造成的,且心里咒骂着:“集合是如此的慢!”

  在应用程序中,我们一般通过以下两种方式去管理相邻对象组:
1、通过创建对象数组(Array)
2、通过创建对象的集合
  我们应该记得每一种集合的具体用法,以及它的优点和缺点,并且知道在什么情况下使用它是最佳的。在文章中,我将列举在Unity中所有常用的数组,这样有利于你更好的理解集合。

一、什么是集合?
  集合是特殊的类用于数据的存储和检索,集合类通常是用来为元素动态的分配内存,并且通过下标索引来访问列表里的每一个元素等等。
  这些类创建Object类的对象的集合,在C#中所有数据类型的基类是Object类。集合可以根据应用程序的请求动态的扩展和缩减,这就是集合的主要优势。
  集合使得内存管理和数据管理的过程变得相当简单。那么,在Unity中常用的集合有哪几个呢?
  在Unity中Dictionary(字典) 和 List(列表)是最常用的集合,让我给初学者对于Dictionary(字典) 和 List(列表)的一些基本概念。如果知道的朋友可以跳过。

1、List
  C#List< T >类代表一个强大的List类型(其实就是泛型List类)可以通过索引访问的列表对象,它可以存储没有指定类型的对象集合。
  它和其他集合一样都有以下功能:添加(Add),插入(Insert),移除(Remove),查找(Search)等等。
  List的索引表示方式和Array一样,然后它的主要优势是动态的指定容器的大小。
例如,我们可以这样定义一个Object的List<>:
1
List myListOfGameObjects = new List();

2、Dictionary
  Dictionary实际上是一个哈希表类型的替代品,Dictionary代表一个键值对
  例如,如果5代表Red,10代表Green,我们便在Dictionary中通过5键(Key)找到Red这个值(Value)。
  因此,我们如果想要找到Red这个值(Value),只要记住5这个键(Key)即可,那么,Dictionary是怎么查找数据的呢?例如,我们可以这样定义一个Dictionary对象:
//Dictionary: //in this example ‘int’ is key and ‘String’ is value.(翻译:在这个例子中,“int”是键,“String”是值)
1
Dictionary<int,string> myDictionary = new Dictionary<int, string="">();int,>int,string>

  现在,这篇文章的主要目标是关于优化使用集合,而不是学习集合,因此我们将忽略集合的学习。

二、集合是怎么影响游戏的呢?
让我们一起看一个例子,并且理解集合石怎么影响我们的游戏的。
1、在Unity中依照下面方式来设置场景
· 创建一个空的游戏物体(Empty Game),并且更改名字(你随意,在这里我命名为Test)
2、创建一个脚本,并且命名为你喜欢的名字
· 在这里我把命名为GenericCollectionsTest.cs
· 我使用的是C#作为我的脚本语言,你也可以使用Javascript,如果你愿意的话。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
   
public class GenericCollectionsTest : MonoBehaviour
{
   
    #region PUBLIC_DECLARATIONS
   
    public int numberOfIterations = 10000000;
   
    #endregion
   
    #region PRIVATE_DECLARATIONS
   
    private Stopwatch stopWatch;
   
    private List<int> intList;
    private Dictionary<int, int=""> intDictionary;
    private int[] intArray;
   
    #endregion
   
    #region UNITY_CALLBACKS
   
    void Start()
    {
        stopWatch = new Stopwatch();
        intArray = new int[numberOfIterations];
        intList = new List<int>();
        intDictionary = new Dictionary<int, int="">();
   
        AddFakeValuesInArray(numberOfIterations);
        AddFakeValuesInList(numberOfIterations);
        AddFakeValuesInDictionay(numberOfIterations);
    }
   
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            PerformTest();
        }
   
        if (Input.GetKeyDown(KeyCode.S))
        {
            SearchInList(111);
            SearchInDictionary(numberOfIterations - 1);
            UnityEngine.Debug.Log("SearchComplete");
        }
    }
   
    #endregion
   
    #region PRIVATE_METHODS
   
    private void AddFakeValuesInArray(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            intArray[i] = Random.Range(0, 100);
        }
    }
   
    private void AddFakeValuesInList(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            intList.Add(Random.Range(0, 100));
        }
        intList[iterations - 1] = 111;
    }
   
   
    private void AddFakeValuesInDictionay(int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            intDictionary.Add(i, Random.Range(0, 100));
        }
        intDictionary[iterations - 1] = 111;
    }
   
    private void SearchInList(int value)
    {
        #region FIND_IN_LIST
        stopWatch.Start();
        int index = intList.FindIndex(item => item == value);
   
        stopWatch.Stop();
        UnityEngine.Debug.Log("Index " + index);
        UnityEngine.Debug.Log("Time Taken to Find in List  "+stopWatch.ElapsedMilliseconds +" ms");
        stopWatch.Reset();
        #endregion
   
        #region CHECK_IF_CONTAINS_VALUE_IN_LIST
        stopWatch.Start();
        bool containsValue = intList.Contains(value);
   
        stopWatch.Stop();
        UnityEngine.Debug.Log(containsValue);
        UnityEngine.Debug.Log("Time Taken To Check in List "+stopWatch.ElapsedMilliseconds +" ms");
        stopWatch.Reset();
        #endregion
    }
   
    private void SearchInDictionary(int key)
    {
        #region FIND_IN_DICTIONARY_USING_REQUIRED_KEY
        stopWatch.Start();
        int value = intDictionary[key];
        stopWatch.Stop();
        UnityEngine.Debug.Log(("Time Taken to Find in Dictionary   " + stopWatch.ElapsedMilliseconds + " ms"));
        stopWatch.Reset();
        #endregion
   
        #region CHECK_IF_DICTIONARY_CONTAINS_VALUE
        stopWatch.Start();
        bool containsKey = intDictionary.ContainsKey(key);
   
        stopWatch.Stop();
        UnityEngine.Debug.Log(containsKey);
        UnityEngine.Debug.Log("Time taken to check if it contains key in Dictionary" + stopWatch.ElapsedMilliseconds + " ms");
        stopWatch.Reset();
        #endregion
    }
   
    private void PerformTest()
    {
   
        #region ARRAY_ITERATION
        stopWatch.Start();
   
        for (int i = 0; i < intArray.Length; i++)
        {
   
        }
   
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By Array "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
   
        #endregion
   
        #region LIST_ITERATION
        stopWatch.Start();
        for (int i = 0; i < intList.Count; i++)
        {
   
        }
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By List "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
        #endregion
   
        #region LIST_ITERATION_BY_FOREACH_LOOP
        stopWatch.Start();
        foreach (var item in intList)
        {
   
        }
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By List Using foreach  "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
        #endregion
   
        #region DICTIONARY_ITERATIOn_LOOP
        stopWatch.Start();
   
        foreach (var key in intDictionary.Keys)
        {
   
        }
        stopWatch.Stop();
        UnityEngine.Debug.Log("Time Taken By Dictionary "+stopWatch.ElapsedMilliseconds + "ms");
        stopWatch.Reset();
        #endregion
    }
   
    #endregion
}int,>int>int,>int>
  • 3、分解代码和理解代码
· 让我们来分解这个代码,并且一步一步的理解代码
· 在这里显示了一些数据,如下表所示:
intArray一个int类型的数组(Array)

intList一个int类型的列表(List)
intDictionary一个键值都是int类型的字典(Dictionary)
  • · 现在,让我们来看看Start()方法
1
2
3
4
5
6
7
8
9
10
11
void Start()
   {
       stopWatch = new Stopwatch();
       intArray = new int[numberOfIterations];
       intList = new List<int>();
       intDictionary = new Dictionary<int, int="">();
  
       AddFakeValuesInArray(numberOfIterations);
       AddFakeValuesInList(numberOfIterations);
       AddFakeValuesInDictionay(numberOfIterations);
   }int,>int>

· 在这里,我初始化了数组(Array),列表(List),字典(Dictionary),并且为他们添加了一些随机值。
· 正如你在代码中看到的,我创建了一个Private(私有的)方法为那些集合添加一些随机数。
· 在这里我也使用了Stopwatch对象用来时间和性能测试,并且也在Start()里进行了初始化
· 如果你还不知道Stopwatch是怎么运作的,在往后学习之前,先去了解它,这样有利于你接下来的理解。
· 现在让我们看一看PerformTest()方法
· 我把该方法分为了4块,这样更有利于理解:
ARRAY_ITERATION这里我们只遍历该数组
LIST_ITERATION这里我们只使用简单的for循环遍历列表
LIST_ITERATION_BY_FOREACH_LOOP这里我们使用foreach循环遍历列表
DICTIONARY_ITERATION_LOOP这里我们遍历字典
  • · 正如你在Update()中看到,我们在按下“Space”键(空格键)时调用PerformTest()。
· 现在,我们将执行项目去测试一番,为了获得性能的确切数据,我们让每一个种类型的数据集合迭代10万次。
· 那么,这就意味着Array(数组)是最好的吗?我们应该只使用Array(数组)就行了嘛?不,不是这样的。正如我们之前说的那样,我们要有计划的使用集合。
· 只是我们要明确我们的需要,按自己的需求指定需用的集合类型。
· 让我们一起去了解一些,我们应该在什么情况下使用什么样的集合类型。
情况1:在整个游戏中,对象的数量保持不变
· 在这种情况下使用List(列表)和Dictionary(字典)是不合适的,很显然对象的数量没有改变。然而使用一个集合为什么会给内存和CPU造成额外的消耗呢?
· 在这里,Array(数组)的效率是List(列表)的两倍。

情况2:在游戏中对象的数量在不断的变化
· 我们从上面中了解到Array(数组)不是动态分配的,显然,我在这种情况下应该使用List(列表)。因为对象在持续的改变,所以List(列表)比Dictionary要快很多。
· List(列表)常用来管理对象池
· List(列表)比Dictionary(字典)快将近8倍左右
· 使用foreach循环来遍历List比使用for循环多消耗将3倍的时间(这个在《关于Foreach你不知道的事儿》中有详细说明)。
  所以这意味着我们应该完全停止使用字典吗?不是的,让我们通过下面的例子更好的理解它。在代码中有两个方法SearchInList() 和 SearchInDictionary()。
通过下面表格进行了解:
SearchInList()方法的第一部分是传递一个值给列表,然后在列表中去查找这个值,第二部分是判断这个列表是否存在该值,最后根据判断条件返回相应的布尔值
SeatchInDictionary()方法的第一部分是根据这个传入的键去找到这个键对应的值,第二部分通过使用ContainsKey()方法判断这个方法里是否有指定的键
  • 让我们在一次运行项目进行测试,且在运行中按下“S”键后看输出日志的显示。
输出将是这样的:
file:///C:UsersADMINI~1AppDataLocalTempksohtmlwpsB87B.tmp.png
从上图中便能得知,使用Dictionary(字典)进行搜索几乎不消耗任何时间
因此,如果在整个游戏运行的时候需要不断的寻找一些对象时,明智的选择就是选择使用Dictionary(字典)。
接受它吧,你的游戏不能没有集合!是的,这是正确的。我们只需要知道在什么情况下使用什么类型的集合。

结论很简单,有三个基本原则:
1、当一个对象的数量保持不变时和需要频繁的查找对象时不要使用List(列表)。
2、如果是动态的对象,且不需要频繁查找对象时,使用List(列表)是最佳的选择。
3、需要快速查找,并且对象的改变很小时,使用Dictionary(字典)是最佳的选择。
4、当一个对象的数量保持不变时,使用Array(数组)是最佳的选择(自己添加的)

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引