Scritping Strategies
Scritping Strategies
Obtaining components using the fastest mehod
最好的方式获得组件:
GetComponent<T>()
Removing empty callback definitions
移除MonoBehaviour中空的生命周期函数:
void Start();
void Update();
void OnGUI();
void LateUpdate();
void FixedUpdate();
可以使用正则表达式查找:
void\s*Update\s*?\(s*?\)\s*?\n*?\{\n*?\s*?\}
Caching component referemces
使用少量的内存换取CPU的开销。
Shaing calculation output
存放一些可能会公用的计算结果,这样我们就不需要重复计算,如AI中的寻路。
Update,coroutines,and InvokeRepeating
我们在计算一些数据的次数比使用到这些数据的次数多的时候,就需要对update做一定的优化。
比如:
void Update()
{
ProcessAI();
}
我们可能不需要每一帧去调用这个方法,所以可以使用隔几秒调用。
float _delayTime = 0.3f;
float _curTime;
void Update()
{
_curTime += Time.deltaTime;
if(_curTime > _delayTime)
{
_curTime = 0;
ProcessAI();
}
}
这种做法,就会出现空的Update
回调。我们可以使用到另外两种做法:
- Corotines
- InvokeRepeating
有一个性能测试对比空的Update
,Coroutines
,InvokeRepeating
:
- 1000个空的
Update
的耗时1.1
毫秒 - 1000个协成使用的
WaitForEndOfFrame
的耗时是2.9
毫秒 - 1000个
InvokeRepeating
的耗时是2.6
毫秒
协成还有很多问题:
- 内存开销会大一些
- 运行会受到GameObject的激活
Faster GameObject null reference checks
一些不必要的GameObject
判空会造成性能的消耗。因为GameObject
和Monobehaviours
这两个属于是两个特殊的对象,他们在内存中有两种表示形式:一个存在于管理我们编写的C#代码的统一系统所管理的内存中(托管代码),而另一个则单独的操作于不同的内存空间(本地代码)。数据可以在这两个空间之间移动,但是每一次移动会造成额外的CPU消耗和内存的分配。
我们可以改使用:
if (gameObject != null){
// do stuff with gameObject
}
if (!System.Object.ReferenceEquals(gameObject,null)){
// do stuff with gameObject
}
这个也可以应该到其他的Unity Objects
,这个的影响其实不太大。
Avoid retrieving string properties from GameObjects
不要使用tag
和name
来判断一个游戏物体这会造成多余的内存分配。我们可以通过组件来区分游戏物体。
如果一定要是使用tag
我们可以使用CompareTag
这个方法会好一些。
Using appropriate data structures
字典与列表的区别:
- 字典在取和插入时,可以很快。遍历可能就不是那么快,取决于内存分配
- 列表恰恰相反
Avoiding re-parenting transforms at runtime
每个Transform
都会有一个层级结构,使用一个列表来存储子物体。若果当前Transform
的这个列表长度达到上限了,我们就需要新申请空间。所以说我们可以在游戏物体实例化时就对这个游戏物体的父物体做指定。
GameObject.Instantiate(....,parent);
同样我们也可以早早的定义好没有子物体的游戏物体的Transform.hierarchyCapacity
Considering caching transform changes
有两种优化这里:
- 能直接修改
local
本地的数据就修改本地的数据,这样省去了矩阵的转换,世界坐标到本地坐标 - 缓存本地
transform
的变化,在FixedUpdate
中修改。因为在修改transform
属性时,会通知其他的组件,这样就会一直通知。
Avoiding Find() and SendMessage() at runtime
消耗非常之大,我们可以使用一下方案来解决:
- Assign references to preexisting objets:在组件中直接引用其他对象,并在编辑器下对其赋值
- Static classes:构造方法是在第一次调用该类时调用
- Singleton components:file access,downloads,data parsing, and messaging.
- A global messaging system
Disabling unused scripts and objects
场景足够大时,物体多就会导致性能拖拉,我们可以把远处或者不必要的物体或脚本关闭。
Disabling objects by visibility
可以使用两个回调来知道我们的游戏物体是否被相机看到:
void OnBecameVisible() {gameObject.SetActive(true);}
void OnBecameInvisible() {gameObject.SetActive(false);}
但是触发这个回调必须要有Render
相关的组件才行,如:Skinned Mesh Renderer
,Mesh Renderer
等。
Disabling objects by distance
距离远的物体可以关闭
Using distance-squared over distance
使用开方消耗极大,我们使用sqrMagnitude
来代替,例如:
float distanceSqrd = (transform.position - other.transform.position).sqrMagnitude;
if(distanceSqrd < (targetDistance * targetDistance))
{
// do studff
}
Minimizing deserialization behavior
Unity的序列化系统主要是场景,预制件,ScriptableObjects和各种资源类型。它们是以文本文件的形式存在磁盘上的,使用一种标记语言的格式存储的(Yet Another Markup Language - YAML),可以反序列化回对象。
其实反序列化这个过程对性能有一个较大的开销,过程也是相对比较慢。比如说一个预制件拥有很深的层级,预制件有很多空的GameObject,并且每一个GameObject至少有一个Transform的组件。这种情况最多出现在UI的预制件中。
加载一个大预制件时,可能会造成CPU的峰值,同时也增加了加载时间。更重要的是,它会引起帧率下降。
Reducing serialized object size
我们的目标应该是使序列化的对象尽可能小,或者将他们分割成更小的数据块,后面使用时,可以把他们组合起来。
Loading serialized objects asynchronously
这对于游戏开始就需要使用到的物体,不太适用于异步加载。
Keeping previously loaded serialized objects in memory
Moving common data into ScriptableObjects
直接使用这个可以节省反序列化后的赋值。
Loading scenes additively and async hronously
Creating a custom Update() layer
使用一个Update方法代替多个Update方法,编写一个OnUpdate的接口,然后把所有继承该接口的对象,都放在唯一的一个Update中去调用。