1.Avoid Find() and SendMessage() at runtime
SendMessage() 方法和 GameObject.Find() 相关的一系列方法都是开销非常大的。SendMessage()函数调用的耗时大约是一个普通函数调用的2000倍,GameObject.Find() 则和场景的复杂度相关,场景越复杂,调用自然越慢。有时候在场景的初始化时,比如Awake和Start函数中调用Find函数是可以理解的,但是即使是这样,也应该是用来获取一定已经存在于这个场景的objects并且场景内的物体最好越少越好。在runtime运行时调用这些函数,都将会带来很大的开销,从而有可能会引起帧率下降。
依赖Find()和SendMessage()函数是架构设计代码设计中一个典型不给力的表现, 这通常是初学者经常犯的错,尽管Unity已经在文档中一遍又一遍的建议大家避免使用这些函数。
尽可能的不使用这些函数甚至打破了我们之前的原则:只有需要优化的时候再优化,不要过度的提前优化。为了避免使用这些函数,我们应该在代码的原型阶段就设计好。
让我们举个栗子,下边的是一个简单的EnemyManagerComponent类,它的作用是存储一个GameObject的List,用来表示游戏中的敌人,并且提供一个KillAll()方法在需要的时候去销毁他们
之后我们会在Scene中放置一个GameObject并且加入这个脚本,把这个物体叫做EnemyManager。
下边的代码会通过Prefab生成一些敌人,并且通知EnemyManager它们的存在
在循环体内初始化数据和调用函数是一个很危险的行为,这会有可能带来很差的性能表现,但我们调用的是开销很大的函数例如Find()时,我们应该尽可能的寻找方法减少调用次数。因此,一个优化点就是用一个本地变量用于保存,将函数调用提到循环外边。另一个很重要的优化是用GetComponet()代替SendMessage(),这开销会小很多。优化后的代码如下:
有很多种方法都能给这个小节提出的问题带来优化,每一种有各自的优缺点:
1.用已经存在的Object保存引用
2.Static 类
3.单例
4.全局的消息系统
2.Assigning references to preexisting objects
一个简单的解决 interobject communication 问题的方法是使用Unity内置的序列化系统可以解决。也就是俗称的在Hierarchy里直接拖拽GameObject或者Prefab到面板对应变量上。但是public属性违反了类的封装原则,这是很危险的,因此可以使用[SerializeField]这个attribute(特性),这样可以使得private 和 protect的属性也能在Inspector中序列化。比如下边的例子:
但是要注意的是拖拽的操作有可能拖拽不合适类型的物体或者忘记处理而变成null。还有就是Unity不能序列化static 和 readonly修饰的变量和属性等。
3.Static Classes
虽然Static Classes有不方便调试,不便于修改和扩展功能(在系统中到处直接引用)等等缺点,但是却是目前非常简便的一种解决方案。单例模式是一种非常普遍常用的设计模式,它保证内存中同一类型只有一个实例。单例模式在处理重度的数据传输,比如读取文件下载解析等时非常适合。单例未必需要是全局的,它们最重要的特性是只有一个实例。单例最简单的一种实现方法就是通过C#的Static Class(静态类)。
把上一小节中的例子改成用Static Class实现,代码如下:
Static Class中所有的方法,属性等都必须是static类型,Static Class中的字段可以直接初始化,也可以通过构造函数初始化。
Static Class的缺点是没办法和Unity中的Inspector window结合,也就是没法像Monobehaviours一样使用,有时候就得写一个匹配的辅助类来帮忙:
尽管有这些缺点,但是使用Static Classes这个方法也要比使用Find()和SendMessage()强的多。
4.Singleton Components
前一节提到过,Static Class无法和Unity一起顺利工作,不能使用MonoBehaviour的各种特性也不能在运行时在Inspector window中看到,从而难于调试。因此实现一个基于MonoBehaviour的单例是一个不错的解决方案:
最简单的使用方式如下边代码所示:
这个方案的缺点是要注意有可能DontDestroyOnLoad()永远不会被调用到,最好是在子类的Awake防范重调用下。当然在有些时候在切换场景时销毁再重新创建也是不错的选择,一切都根据使用场景决定。
另外要注意的是OnDestroy的危险,比如观察者模式,很多物体的取消注册时机都写在了OnDestroy函数里,但是Unity并不保证OnDestroy的时序,因此有可能当某个物体调用自己的OnDestroy时,调用到了已经销毁的单例,这有可能带来致命的错误。
最好的解决方案就是永远不要在OnDestroy函数里调用单例,但是如果非要使用,解决方案如下:
第一步:我们需要加个标志,用来跟踪单例是否存活
第二步:要提供一个途径来获取单例是否存活的状态
最后,任何在Destroy函数里调用单例的地方都要先去验证
这个单例方案也使用到了Find方法,但是只是在初始化时调用一次,因此还可以接受。但是它的初始化时机可能并不是在场景的初始化时,而是在第一次使用时,因此有可能在那个时机会给性能带来影响。因此也可以在场景的初始化时调用单例来保证其在场景初始化时就初始化好。
另外一个缺点就是如果以后我们想改掉这个单例模式,变成可能有多个实例或者想把它变得更模块化,代码的改动量将会非常大。