Unity3d 制定游戏循环的策略

版权声明:本文为 松阳 (blog.csdn.net/fansongy) 原创文章,转载必须注明出处: https://blog.csdn.net/fansongy/article/details/82313518

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

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

生命周期

Unity3d的函数调用有固定的顺序,对于脚本生命周期(Script Lifecycle),官方文档中给出一张时序图如下:

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

  • Awake 函数在构造脚本对象时调用
  • OnEnable/OnDisable 在每次激活对象时调用
  • Start 在第一次触发脚本时调用
  • OnDestory 在销毁对象时调用

主循环分为物理模拟、游戏逻辑、渲染绘制三个子循环。从Unity实现的方式上看,这三个循环放是在同一个线程中。不过自从Unity3d的5.x版本后,引擎加入了多线程渲染的选项,即将第三步循环放在另一个线程中进行。这样做就可以在固定帧率下降低逻辑循环的压力,降低掉帧现象的发生。至于物理模拟循环,我个人觉得没必要拆出到另一个线程。毕竟线程间通信也需要性能,而物理模拟循环运行到速度一般要快于游戏逻辑,如果出现数据访问的冲突还需要加锁,就得不偿失了。本文内容出自《游戏架构:核心技术与面试精粹》,感兴趣的读者可以在主流电商网站上查到。

重写模版脚本

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

  • Windows: Unity安装目录/Editor/Data/Resources/ScriptTemplates/81-C# Script-NewBehaviourScript.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#的功能,可以标定折叠区域,方便查找对应函数。

游戏循环

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

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

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

  1. 非阻塞的用户输入
  2. 更新游戏逻辑状态
  3. 渲染游戏画面

每次循环完成后,会更新一次画面的绘制,这个过程也被称为帧(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可以防止逻辑执行时间过长,卡死线程。

fixedupdate

总结

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

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

本文出自我的编写的书:

这里写图片描述

主流电商网站有售:

猜你喜欢

转载自blog.csdn.net/fansongy/article/details/82313518