游戏架构:核心技术与面试精粹
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1 游戏循环

请简述Unity 3D中常用的生命周期函数,并以此为基础简述游戏循环的设计要点。

问题分析

在使用Unity 3D开发游戏时,总会涉及Unity内置的生命周期函数。弄清这些函数的调用顺序和特性十分重要,因为这影响到逻辑的执行顺序。例如,初始化要在使用之前,注册回调要在响应之前,等等。

一般来说,在大型商业项目中,我们需要自制一套游戏循环,来满足多变的游戏玩法。哪些现有的生命周期函数可以使用?哪些需要被替换掉?这些取舍贯穿了整个游戏开发过程。

生命周期

Unity 3D的函数调用有固定的顺序,对于脚本生命周期(Script Lifecycle),官方文档中给出了一张时序图,如图2.1所示。([:lifecycle])。

图2.1

深入理解这张图,可以应对大多数的时序问题。在Initialization、Disable/enable、Decommissioning区域的函数只会调用有限次数,而其他部分都是受控重复或循环调用,其中:

◎ Awake函数在构造脚本对象时调用;

◎ OnEnable/OnDisable在每次激活对象时调用;

◎ Start在第一次触发脚本时调用;

◎ OnDestory在销毁对象时调用。

主循环分为物理模拟、游戏逻辑、渲染绘制三个子循环。从Unity实现的方式来看,这三个循环是在同一个线程中的。不过自Unity 3D的5.x版本后,引擎加入了多线程渲染的选项,即把第三步循环放在另一个线程中进行,这样就可以在固定帧率下降低逻辑循环的压力,减少掉帧现象的发生。至于物理模拟循环,笔者觉得没必要拆出到另一个线程。毕竟线程间通信也需要性能,而物理模拟循环运行的速度一般要快于游戏逻辑,如果出现数据访问冲突时还需要加锁,就得不偿失了。

重写模板脚本

在实际项目中,可以通过更改代码的模板文件,来优化生命周期函数的使用方式。在Unity的安装目录中,有创建代码的模板文件。通过自定义这个文件,可以更改创建默认文件的内容。文件目录如下。

◎ Windows:Unity安装目录/Editor/Data/Resources/ScriptTemplates/81-C# Script- NewBe haviourScript. cs.txt。

◎ Mac:Unity安装目录/Contents/Resources/ScriptTemplates/81-C# Script-NewBehaviourScript. cs.txt。

根据团队开发人员的编程习惯,笔者自定义了一个代码模板,大家可以根据自己的需要做相应更改:

    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#的功能,可以标定折叠区域,方便查找对应函数。

[注]本节后续内容部分参考(参考书目6)

游戏循环

了解了Unity自带的生命周期函数之后,再看看游戏循环应该如何设计。游戏归根结底是由交互序列组成的,因此它必然会有一个基础的结构:

    while (true)
    {
      Input();
      Update();
      Render();
    }

从这个层面看,游戏循环由三部分组成,分别是

(1)非阻塞的用户输入;

(2)更新游戏逻辑状态;

(3)渲染游戏画面。

每次循环完成后,会更新一次画面的绘制,这个过程也被称为帧(Frame)。帧率(FPS, Frame Per Second)可以标定游戏循环的速率与真实时间的映射关系。帧率值越小,意味着游戏越“卡”。游戏在PC上,通常为60帧/秒,在手机上为30帧/秒。另一方面,帧率的倒数即为每帧所占用的时长,单位通常为毫秒。影响帧率的主要因素是每帧需要做的工作。例如,复杂的物理计算、游戏逻辑的处理、图形细节控制等,这些都会占据CPU与GPU。如果处理操作的时长超过帧率的倒数,那么就会拖慢帧率,这种现象称为“掉帧”。

固定帧率模式

一般来说,我们有个期望的帧率,如果每帧的运行时间短,那么帧率就会超过预定的标准,因此我们通常会在循环的末尾加入延期等待。假定有个Sleep函数可以阻塞线程执行,即这个模式的代码结构如下:

    while (true)
    {
      double start=getCurrentTime();

      Input();
      Update();
      Render();

      sleep(start + 1/FPS - getCurrentTime());
    }

在这种结构中,帧率不会超过预定数值。在Unity 3D中可以通过下面的代码设置:

    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在逻辑循环之外,所以总体来看还是会节省一些时间。虽然看起来会比较卡,但基本能够正常运行游戏。

在Unity 3D中,对应这个模式的循环是FixedUpdate。在Unity中设置Fixed Timestep可以控制FixedUpdate速率,其数值为时长周期。如果FixedUpdate在限定的时间内执行不完,则图形绘制频率就会降低,以保证物理的执行。另一方面,设置Maximum Allowed Timestep可以防止逻辑执行时间过长,卡死线程,如图2.2所示。

图2.2

总结

Unity 3D作为完整的引擎,常见的生命周期函数与游戏循环模式都已具备。但作为特定的游戏,通常有自己的特点。例如,竞速类游戏与MMO网游在游戏循环的设计上就有很大的差别。竞速类游戏对实时反馈的要求很高,如果采用追赶模式,则抽帧造成的体验就会很差。在掉帧方面,MMO网游面临的则是角色在场景中漫游时,其他玩家的模型加载与位置同步造成的卡顿。在这种情况下,可能会使用分帧加载、AOI(Area Of Interest)等处理方法来保障游戏的流畅。

因此,游戏循环通常是每个项目根据自己的特点“独家定制”的。在深入理解Unity 3D的生命周期函数后,我们就可以在其基础上,自主独立地搭建个性化的游戏循环框架了。

扩展问题

请尝试分析多线程渲染的实现原理([::mutirender])。