在使用Unity3d开发游戏的时,我们总会涉及Unity内置的生命周期函数。弄清这些函数的调用顺序和特性十分重要,因为这影响到逻辑的执行顺序,例如:初始化要在使用之前,注册回调要在响应之前等等。
通常说来,在大型商业项目中,我们需要自制一套游戏循环,来满足多变的游戏玩法。哪些现有的生命周期函数可以使用?哪些需要被替换掉?这些取舍贯穿了整个游戏开发过程。
Unity3d的函数调用有固定的顺序,对于脚本生命周期(Script Lifecycle),官方文档中给出一张时序图如下:
深入理解这张图,可以应对大多数的时序问题。在Initialization、Disable/enable、Decommissioning区域的函数只会调用有限次数,而其他部分都是受控重复或循环调用。其中:
主循环分为物理模拟、游戏逻辑、渲染绘制三个子循环。从Unity实现的方式上看,这三个循环放是在同一个线程中。不过自从Unity3d的5.x版本后,引擎加入了多线程渲染的选项,即将第三步循环放在另一个线程中进行。这样做就可以在固定帧率下降低逻辑循环的压力,降低掉帧现象的发生。至于物理模拟循环,我个人觉得没必要拆出到另一个线程。毕竟线程间通信也需要性能,而物理模拟循环运行到速度一般要快于游戏逻辑,如果出现数据访问的冲突还需要加锁,就得不偿失了。本文内容出自《游戏架构:核心技术与面试精粹》,感兴趣的读者可以在主流电商网站上查到。
在实际项目中,我会通过更改代码的模板文件,来优化生命周期函数的使用方式。在Unity的安装目录中,有创建代码的模版文件。通过自定义这个文件,可以更改创建默认文件的内容。文件目录为:
根据自己团队开发人员的编程习惯,我自定义了一个代码模版。大家可以根据自己的需要做相应的更改。
using UnityEngine;
using System.Collections;
public class #SCRIPTNAME# : MonoBehaviour
{
#region Public Attributes
#endregion
#region Private Attributes
#endregion
#region Unity Messages
// void Awake()
// {
//
// }
// void OnEnable()
// {
//
// }
//
// void Start()
// {
//
// }
//
// void Update()
// {
//
// }
//
// void OnDisable()
// {
//
// }
//
// void OnDestroy()
// {
//
// }
#endregion
#region Public Methods
#endregion
#region Override Methods
#endregion
#region Private Methods
#endregion
#region Inner
#endregion
}
代码模版中添加了常用的生命周期函数,并按照顺序进行排列。由于空函数也会产生性能消耗,这里采用注释的方式规避这个弊端。另外,我按用途添加了几个#region分隔函数区域。#region是C#的功能,可以标定折叠区域,方便查找对应函数。
了解Unity自带的生命周期函数之后,再看看游戏循环应该如何设计。游戏归根结底是由交互序列组成的。因此它必然会有一个基础的结构:
while (true)
{
Input();
Update();
Render();
}
从这个层面看,游戏循环由三部分组成,分别是
每次循环完成后,会更新一次画面的绘制,这个过程也被称为帧(Frame)。使用帧率(FPS,Frame Per Second)可以标定游戏循环的速率与真实时间的映射关系。帧率值越小,意味着游戏越“卡”。游戏在电脑或家用主机上,通常为60帧/秒,手机上为30帧/秒。另一方面,帧率的倒数即为每帧所占用的时长,单位通常为毫秒。影响帧率的主要因素是每帧需要做的工作。例如复杂的物理计算,游戏逻辑的处理,图形细节控制等。这些都会占据CPU与GPU。如果处理操作的时长超过帧率的倒数,那么就会拖慢帧率。这种现象被成为“掉帧”。
一般说来,我们有个期望的帧率,如果每帧的运行时长短,那么帧率就会超过预定的标准,因此我们通常会在循环的末尾加入延期等待。假定我有个Sleep函数可以阻塞线程执行,那么这个模式的代码结构如下:
while (true)
{
double start = getCurrentTime();
Input();
Update();
Render();
sleep(start + 1/FPS - getCurrentTime());
}
在这种结构中,帧率不会超过预定数值。在Unity3d中可以通过下面的代码设置:
Application.targetFrameRate = FPS;
这种模式可以更好的处理掉帧引发的逻辑问题。大体思路是,当出现掉帧时,只运行逻辑,不绘制画面,用节省下来的时间追赶落后的帧。这种策略会降低图形绘制的频率,但可以保证逻辑的执行。
具体说来,每次Render执行之前,要保证累计运行时长到达阈值。如果出现卡顿,后面的帧会多次执行Update,直到赶上之前的帧为止。代码结构如下:
double preFrameTime = getCurrentTime();
double lag = 0.0;
while (true)
{
double current = getCurrentTime();
double elapsed = current - preFrameTime;
preFrameTime = current;
lag += elapsed;
Input();
while (lag >= 1/FPS)
{
Update();
lag -= 1/FPS;
}
Render();
}
使用这种模式时,要注意不要将FPS设置的太大,否则最慢的机器将永远赶不上时间,它将卡在死循环中。对于较差的机器,Render在这逻辑循环之外,所以总体上看还是会节省一些时间。虽然看起来会比较卡,但还是能够正确的运行游戏。
在Unity3d中,对应这个模式的循环是FixUpdate。Unity中设置Fixed Timestep
可以控制FixedUpdate速率,其数值为时长周期。如果FixedUpdate在限定的时间执行不完,图形绘制频率会降低,以保证物理的执行。另一方面,设置Maximum Allowed Timestep
可以防止逻辑执行时间过长,卡死线程。
Unity3d作为完整的引擎,常见的生命周期函数与游戏循环模式都已具备。但作为特定的游戏,通常有自己的特点。例如,竞速类游戏与MMO网游在游戏循环的设计上就有很大的差别。竞速类型对实时反馈的要求很高,如果采用追赶模式,抽帧造成的体验就会很差。在掉帧方面,MMO网游面临的则是,角色在场景中漫游时,其他玩家的模型加载与位置同步造成的卡顿。这种情况下,可能会使用分帧加载、AOI ( Area Of Interest ) 等处理方法保障游戏的流畅。
因此,游戏循环通常是每个项目根据自己的特点“独家定制”。在深入理解Unity3d的生命周期函数后,我们就可以在其基础上,自主独立的搭建个性化的游戏循环框架。
本文出自我的编写的书:
主流电商网站有售: