微信号:Gad-GameDev

介绍:专业的游戏人社区(GAD.QQ.COM):为游戏开发者提供学习指导、问题解决、队员招募、游戏孵化等服务.免费组件下载、专业知识学习、组队游戏制作、项目孵化渠道.

【程序猿】游戏引擎Unity中的单线程与多线程

2017-01-12 17:41 Harry何朝阳


  因为工作的原因,第一次接触到Unity3D游戏引擎,开拓了眼界,学到不少新知识。


  今天与大家聊一聊我觉得在Unity3D中最基础也是最重要的概念:Unity3D的单线程与多线程。


  最近项目中,有一个网络操作的小需求,我按照其他移动端的研发经验,使用Unity的网络库UnityEngine.WWW或者UnityEngine.Networking,然后new Thread放入子线程运行网络,结果非常尴尬的遇到了can only be called from themain thread.的异常,百思不得其解。


  为什么不能在子线程中操作UnityEngineSDK?


  我带着这个疑问,开始了探索。发现如下规律:


  1. UnityEngine的API与对象都不能在子线程中运行。


  2. UnityEngine定义的基本结构(int,float,Struct定义的数据类型)可以在子线程中运行。例如Vector3(Struct)可以,但Texture2d对象则不可以,因为其根父类为UnityEngine.Object)。


  概括起来,Unity3D中的子线程无法运行Unity SDK API。


  其实,当Unity对其SDK做了这个限制之后,我们可以非常肯定的说Unity是单线程的游戏引擎。


  为什么要做这个限制?因为游戏中逻辑更新和画面更新的时间点要求有确定性,必须按照帧序列严格保持同步,否则就会出现游戏中的对象不同步的现象。多线程也能保证这个效果,但如果引入多线程,会加大同步处理的难度与游戏的不稳定性。脑补一下这张图中的场景,如果卡顿了是什么感受:-)



  明白了Unity3D是单线程设计之后,我继续带着疑问探索。


Unity3D的线程运行机制是怎样的呢?


  最近写了一点游戏脚本,一般都要求继承MonoBehavior对象,会在OnStart()中做地图初始化,在OnUpdate()中进行帧刷新,然后MonoBehavior脚本可以作为一个Component挂载到某个GameObject中。


  这一点点入门级的理解大致能在游戏中把地图引擎跑起来,但是并没有深入的去理解这些框架函数的执行原理。


  先上一张Unity3D运行机制的图:



  初看起来可能不太理解,但是随着项目深入,相信会逐步理解其中的含义。


  第一次学**Unity3D,我认为可以简单一点理解:这是一个单线程的帧循环,每一次绘制都会重新走一遍生命周期。


  展开来说,主要包含以下部分:


  1. 初始化。对于常用的OnStart()框架方法只会被调用一次。


  2. 物理。比如游戏中的碰撞处理。


  3. 事件输入。例如手势


  4. Update(), 协程(Coroutine),LastUpdate()


  5. 渲染Rendering。包含常用的OnPreRender(),onPostRender()


  6. 销毁。

  掌握这张图的精髓或许是理解Unity3D的关键,需要时间沉淀。目前我所掌握的有:


  OnPreRender:在相机开始渲染场景之前调用此函数。


  OnPostRender: 在相机完成场景渲染后调用此函数。


  Update: 在每帧上调用一次 Update() 函数。


Unity3D中协程(Coroutine)究竟是什么?


  介绍完了Unity3D的生命周期,来说说协程(Coroutine)。


  协程是什么呢?总体来说,对与Unity,它是单线程的设计,它更倾向使用time slicing(时间分片)的协程(coroutine)去完成异步任务,融合到了刚刚提到的生命周期中。


  要理解协程,先回顾下线程:线程是操作系统级别的概念,现代操作系统都实现并且支持线程,线程的调度对应用开发者是透明的,开发者无法预期某线程在何时被调度执行。基于此,一般那种随机出现的BUG,多与线程调度相关。


  而协程Coroutine是编译器级的,本质还是一个线程时间分片去执行代码段。它通过**相关的代码使得代码段能够实现分段式的执行,显式调用yield函数后才被挂起,重新开始的地方是yield关键字指定的,一次一定会跑到一个yield对应的地方。因为协程本质上还是在主线程里执行的,需要内部有一个类似栈的数据结构,当该coroutine被挂起时要保存该coroutine的数据现场以便恢复执行。


  在Unity3D中,协程是可自行停止运行 (yield),直到给定的 YieldInstruction 结束再继续运行的函数。 协程 (Coroutines) 的不同用途:


  • yield; 在下一帧上调用所有 Update 函数后,协同程序将继续运行。


  • yield WaitForSeconds(2); 在指定的时间延迟之后,为此帧调用所有 Update 函数之后继续运行


  • yield WaitForFixedUpdate(); 在所有脚本上调用所有 FixedUpdate 后继续运行


  • yield WWW 完成 WWW 下载后继续运行。


  • yield StartCoroutine(MyFunc); 连接协同程序,并等待 MyFunc coroutine 首先结束。


  也就是说,将代码段分散在不同的帧中,每次执行一段,下一帧再执行yield挂起的地方。


  举个例子: 在OnStart()框架函数中调用startCoroutine(GetHttpData)执行以下代码端,其实是第一次发起网络请求,下一次执行时则走入yield之后的代码段继续执行,从而实现了一个时间分片的”异步”效果,而不是像线程那样在操作系统层面分CPU时间片去执行。



Unity中无法使用子线程了吗?


  先回顾下前面提到的内容:


  1、Unity是单线程设计的游戏引擎,子线程中无法运行UnitySDK


  2、Unity主循环是单线程,游戏脚本MonoBehavior有着严格的生命周期


  3、倾向使用time slicing(时间分片)的协程(coroutine)去完成异步任务


  这三点是Unity3D最为基础也是最为重要的概念,熟练掌握才算入门了Unity3D开发。


  但这些并不意味着无法在Unity中使用多线程,只是需要注意使用的场景。


  试想一下,如果在帧序列的主循环单线程中处理大量耗时操作,势必会带来游戏画面的卡顿,帧率的下降。


  因此,对于不是画面更新,也不是常规的逻辑更新(指包括AI、物理碰撞、角色控制这些),而是一些其他后台任务,则可以将这个独立出来开辟一个子线程。


  所以,在不使用Unity SDK的前提下,确保做好主子线程的同步(采用C#中的delegate等机制),那么是可以合理使用子线程的。


  概括起来,结合过往移动端的研发经验,我认为有以下几点可以在子线程中处理:

  大量耗时的数据计算

  网络请求

  复杂密集的I/O操作

  Unity3D的NativePlugin中可以新建子线程。通过NativePlugin可以接入移动端iOS与Android中的成熟库,可以是Objective C, Java, C++三种语言交叉混合的方式组成NativePlugin,然后使用Android或者iOS的SDK开辟子线程。


  (这也是Unity3D中比较关键的技术,后续有空单独开篇谈谈我的理解。)


  关于这个话题,先聊到这里,或许遇到新问题又会有更为深入的发现。




点击一下
立即阅读近期热文


手把手教你做3D扫雷:完结篇

深度学习热潮下,2017 年最受欢迎的编程语言是哪些

腾讯开源手游热更新方案:Unity3D下的XLua技术内幕(一)

……


添加小编微信,发送“程序

即可直接加入GAD程序猿交流基地

获取行业干货资讯,观看大牛分享直播

↓长按添加小编GAD-沫沫


 
Gad-腾讯游戏开发者平台 更多文章 【越策划】不写代码——教你如何快速制作h5独立小游戏(下篇) 【美术圈】如何绘画一副有着赛博朋克风的营地 【程序猿】手把手教你做3D扫雷:完结篇 策划资料库 | 17G精选文章\/课程视频\/行业报告全套资料下载 策划资料库 | 17G精选文章\/课程视频\/行业报告全套资料下载
猜您喜欢 互联网公司年会上的老板与女优 冰山一角,管窥中国互联网的地下世界 基于深度学习的目标检测研究进展 CDH大数据平台搭建 信息安全怎么强调都不为过,BoCloud体系化保证云平台安全可靠