Cocos 引擎启动与主循环 源码解读 Creator

前言

本文基于 Cocos Creator 2.4.3 撰写。

不知道你有没有想过,假设把游戏环球比作一辆汽车,那么这辆“汽车”是如何启动,又是如何继续运转的呢?

如题,本文的内容关键为 Cocos Creator 引擎的启动流程和主循环。

而在主循环的内容中还会触及到:组件的生命周期和计时器、缓动系统、动画系统和物理系统等...

本文会在微观上为大家解读主循环与各个模块之间的相关,关于各个模块也会繁难引见,但不会深化到模块的详细成功。

毕竟假设要把每个模块都“摸”一遍,那这篇文章怕是写不完了。

宿愿大家看完这篇文章之后能够愈加了解 Cocos Creator 引擎。

同时也宿愿本文可以起到「领进门」的作用,大家一同加油呀~

另外《源码解读》系列(应该)会继续降级,假设你想要皮皮来解读解读引擎的某个模块,也欢迎留言通知我,我...我思考下哈哈哈~

注释

启动流程

index.html

关于 Web 平台来说 index.html 文件就是程序的终点。

在自动的 index.html 文件中,定义了游戏启动页面的规划,加载了 main.js 文件,并且还有一段立刻口头的代码。

这里截取 main.js 文件中一部分比拟关键的代码:

//加载物理系统脚本并启动引擎

上方这段代码关键用于加载引擎脚本和物理系统脚本,脚本加载成功之后就会调用 main.js 中定义的 window.boot 函数。

原生平台

关于原生平台,会在 {名目目录}build\jsb-link\frameworks\runtime-src\Classes\AppDelegate.cpp文件的 applicationDidFinishLaunching 函数中加载 main.js 文件。

——来自渡鸦大佬的补充

代码紧缩

脚本文件名中带有 -min 字样(如 index.min.js)普通代表着这个文件内的代码是被紧缩过的。

紧缩代码可以节俭代码文件所占用的空间,放慢文件加载速度,缩小流量消耗,但同时也让代码失去了可阅读性,不利于调试。

所以开启调试形式后会间接经常使用未经过紧缩的代码文件,便于开发调试和定位失误。

关于不同平台 main.js 的内容也有些许差异,这里咱们疏忽差异部分,只关注其中关键的独特行为。

关于 main.js 文件的内容基本上就是定义了 window.boot 函数。

关于非 Web 平台,会在定义完之后间接就调用 window.boot 函数,所以 main.js 就是他们的终点。

而 window.boot 函数外部有以下关键行为:

这部分的代码就不贴了,小同伴们可以看看自己的名目构建后的 main.js 文件。

cc.game 对象是 cc.Game 类的一个实例,cc.game 蕴含了游戏主体消息并担任驱动游戏。

说人话,cc.game 对象就是治理引擎生命周期的模块,启动、暂停和重启等操作都须要用到它。

[源码] CCGame.js:

cc.game.run 函数内指定了引擎性能和 onStart 回调并触发 cc.game.prepare() 函数。

源码节选:

函数:cc.game.run

[源码] CCGame.js#L491:

cc.game.prepare 函数内关键在名目预览时极速编译名目代码并调用 _prepareFinished 函数。

源码节选:

函数:cc.game.prepare

[源码] CCGame.js#L472:

极速编译

关于极速编译的细节,可以在名目预览时关上阅读器的开发者工具,在 Sources 栏中搜查(Ctrl + P)__quick_compile_project__ 即可找到 __quick_compile_project__.js 文件。

_prepareFinished

cc.game._prepareFinished() 函数的作用关键为初始化引擎、设置帧率计时器和初始化内建资源(effect 资源和 material资源)。

当内建资源加载成功后就会调用 cc.game._runMainLoop() 启动引擎主循环。

源码节选:

函数:cc.game._prepareFinished

//初始化内建资源(加载内置的effect和material资源)//打印引擎版本到控制台//发射‘game_inited’事情(代表引擎已初始化成功)

[源码] CCGame.js#L387:

_setAnimFrame

关于 _prepareFinished 函数内调用的 _setAnimFrame 函数这里必定提一下。

cc.game._setAnimFrame 函数外部对不同的游戏帧率做了适配。

另外还对 window.requestAnimationFrame 函数做了兼容性封装,用于兼容不同的阅读器环境,详细的咱们上方再说。

这里就不贴 _setAnimFrame 函数的代码了,有须要的小同伴可自行查阅。

[源码] CCGame.js#L564:

_runMainLoop

cc.game._runMainLoop 函数的名字取得很繁难间接,摊牌了它就是用来运转 mainLoop 函数的。

源码节选:

函数:cc.game._runMainLoop

//将在下一帧开局循环回调

[源码] CCGame.js#L612:

经过以上代码咱们可以得悉,_runMainLoop 函数关键经过 window.requestAnimFrame 函数来成功循环调用 mainLoop函数。

requestAnimFrame

window.requestAnimFrame 函数就是咱们上方说到的 _setAnimFrame 函数外部关于window.requestAnimationFrame 函数的兼容性封装。

对前端不太相熟的小同伴或者会有不懂,window.requestAnimationFrame 又是啥,是用来干嘛的,又是如何运转的?

requestAnimationFrame

繁难来说,window.requestAnimationFrame函数用于向阅读器恳求启动一次性重绘(repaint),并在重绘之前调用指定的回调函数。

window.requestAnimationFrame函数接纳一个回调作为参数并前往一个整数作为惟一标识,阅读器将会在下一个重绘之前口头这个回调;并且口头回调时会传入一个参数,参数的值与performance.now() 前往的值相等。

Performance.now()

[MDN] Performance.now():

回调函数的口头次数通常与阅读器屏幕刷新次数相婚配,也就是说,关于刷新率为 60Hz 的显示器,阅读器会在一秒外口头 60 次回调函数。

关于 window.requestAnimationFrame 函数的说明到此为止,假构想要了解更多消息请自行检索。

[MDN] window.requestAnimationFrame:

小结

画一张图来对引擎的启动流程做一个小小的总结叭~

主循环

教训了一番挫折后,终于到来了最等候的引擎主循环部分,话不多说,咱们继续!

cc.director

cc.director 对象是导演类 cc.Director 的实例,引擎通关键过 cc.director 对象来治理游戏的逻辑流程。

[源码] CCDirector.js:

cc.director.mainLoop 函数或者是引擎中最关键的逻辑之一了,蕴含的内容很多也很关键。

如今让咱们进入 mainLoop 函数外部来一探求竟吧!

源码节选:

函数:cc.Director.prototype.mainLoop

这里我选用性剔除掉了函数中的一些代码,还加了点注释。

//也就是距离上一次性调用mainLoop的时时期隔//游戏没有暂停则启动降级//调用新增的组件(已启用)的start函数//调用一切组件(已启用)的//调用一切组件(已启用)的lateUpdate函数//销毁最近被移除的实体(节点)//降级事情治理器的事情监听(cc.eventManager已被废除)//累加游戏运转的总帧数

[源码] CCDirector.js#L843:

接上去咱们来对主循环中的关键点逐一启动合成。

ComponentScheduler

cc.director 对象中的 _compScheduler属性 是 ComponentScheduler 类的实例。

大少数小同伴或者关于 ComponentScheduler 这个类没有什么印象,我来繁难解释一下。

将 ComponentScheduler 的名字直译上来就是“组件调度器”,从名字上就可以看出,这个类是用来调度组件的。

说人话,ComponentScheduler 类是用来集中调度(治理)游戏场景中一切组件(cc.Component)的生命周期的。

文字不够直观,看完上方这张图大略就懂了:

[源码] component-scheduler.js:

startPhase

mainLoop 函数代码片段:

//调用上一帧新增(且已启用)的组件的start函数

组件的 start 回调函数会在组件第一次性激活前,也就是第一次性口头 update 之前触发。

在组件的永世中 start 回调只会被触发一次性。

而 start 则会等到下一次性主循环 mainLoop() 时才触发。

常识补充

生命周期中的 onLoad 和 onEnable 是由 NodeActivator 类的来治理的:

NodeActivator

NodeActivator 类关键用于激活和反激活节点以及节点身上的组件。

cc.director 对象中就领有一个实例 _nodeActivator。如激活节点时会调用cc.director._nodeActivator.activateNode(this, value);。

[源码] node-activator.js:

updatePhase

mainLoop 函数代码片段:

//调用一切(已启用)组件的

组件的 update 回调每一帧都会被触发一次性。

lateUpdatePhase

mainLoop 函数代码片段:

//调用一切组件(已启用)的lateUpdate函数

组件的 lateUpdate 回调会在「组件 update 回调口头后、调度器(cc.Scheduler)降级后」被触发。

调度器(cc.Scheduler)的降级内容包括缓动、动画和物理等,这一点上方会倒退。

ParticleSystem

BTW,粒子系统组件(cc.ParticleSystem)就是在 lateUpdate 回调函数中启动降级的。

[源码] CCParticleSystem.js#L923:

一个倡导

请审慎经常使用 update 和 lateUpdate 回调,由于它们每一帧都会被触发,假设 update 或 lateUpdate内的逻辑过多,就会使得每一帧的口头时期(即帧时期 Frame time)都变长,造成游戏运转帧数降落或出现不稳固的状况。

留意:这个倡导不是不让你用,该用还得用,只是不要滥用,不要啥玩意都往里边塞~

cc.director 对象的 _scheduler 属性是 cc.Scheduler 类的一个实例。

cc.Scheduler 是担任触发回调函数的类。

[源码] Scheduler.js:

mainLoop 函数代码片段:

你相对猜不到上方这一行看起来如此平平无奇的代码口头之后会出现什么。

cc.director.mainLoop 函数中经常使用调度器 _scheduler 的 update 函数来散发update,在调度器外部会依据优先级先后触发各个系统模块和组件计时器的降级。

系统模块

调度器的降级会先触发以下系统模块的降级:

以上这些模块都以 cc.director._scheduler.scheduleUpdate()的形式注册到调度器上,由于这些模块每一帧都须要启动降级。

除了 InputManager 以外的模块的优先级都为cc.Scheduler.PRIORITY_SYSTEM,也就是「系统优先级」,会优先被触发。

ActionManager

ActionManager 即举措治理器,用于治理游戏中的一切举措,也就是缓动系统 Action 和 Tween(其实它们实质上是同一种物品)。

[源码] CCActionManager.js:

AnimationManager

AnimationManager 即动画治理器,用于治理游戏中的一切动画,驱动节点上的 Animation 组件播放动画。

[源码] animation-manager.js:

CollisionManager

CollisionManager 即碰撞组件治理器,用于处置节点之间的碰撞组件能否发生了碰撞,并调用相应回调函数。

[源码] CCCollisionManager.js:

PhysicsManager

PhysicsManager 即物理系统治理器,外部以 Box2D 作为 2D 物理引擎,加以封装并放开部分罕用的接口。同时 PhysicsManager还担任治理碰撞消息的散发。

[源码] CCPhysicsManager.js:

Physics3DManager

Physics3DManager 即3D 物理系统治理器,Cocos Creator 中的 3D 物理引擎有 Cannon.js 和 Builtin可选,Physics3DManager 给它们封装了一致的罕用接口。

[源码] physics-manager.ts:

InputManager

InputManager 即输入事情治理器,用于治理一切输入事情。开发者被动启用减速度计(Accelerometer)之后,引擎会定时经过InputManager 发送 cc.SystemEvent.EventType.DEVICEMOTION 事情(自动距离为 0.2 秒)。

[源码] CCInputManager.js:

组件计时器

置信大少数小同伴都经常使用过组件的 schedule 和 scheduleOnce 函数,关键用来提前或重复口头指定的函数。

实践上,cc.Component 的 schedule 函数依赖的也是 cc.Scheduler 类,详细经常使用的也是 cc.director 对象中的_scheduler 实例。

组件的 schedule 函数在 cc.director._scheduler.schedule 函数之外加了一层封装,「以组件自身作为target,这样一来组件内的定时义务就与组件生命周期绑定,当组件被销毁时定时义务也会被移除。」

而 scheduleOnce 函数则是在组件的 schedule 接口之外又加了一层繁难的封装,写死只会在指定时期后口头一次性。

[源码] CCComponent.js#L555:

[文档] 经常使用计时器:

setTimeout & setInterval

另外我还留意到,有不少小同伴还不是很分明组件的计时器和 setTimeout、setInterval之间的区别和用法,那就趁这个时机繁难讲一下吧~

首先,setTimeout 和 setInterval 函数都是由阅读器或 Node.js 这类 runtime 所提供的接口。

setTimeout 函数用于设置一个定时器,该定时器在定时器到期后口头一个函数或指定的一段代码。

setInterval 函数用于重复调用一个函数或口头一个代码段,在每次调用之间具备固定的时期提前。

常识补充

在阅读器中 setTimeout 和 setInterval 函数的最小延时(距离)是 4ms。

假设是未激活(处于后盾)的标签页(tab),最小延时(距离)则延长到 1000ms。

举个栗子

假设我在标签页设置了一个每 500ms 输入一个 log 的定时器,当我切换到别的标签页之后,那么这个定时器就会变成每 1000ms 才输入一个log。

像这样(可以自己跑跑看):

区别 & 用法

组件的计时器依赖于引擎的 mainLoop()和组件自身,假设引擎被暂停,那么组件的计时器也会被暂停,假设组件或组件所在的节点被销毁了,那么计时器也会失效。

setTimeout() 和 setInterval() 都依赖于所处的 window对象,也就是说只需阅读器标签页不封锁,setTimeout() 和 setInterval() 都还是会口头的。

当你须要在组件外部定时或重复口头某一函数或操作某个节点,那么可以经常使用组件的计时器。

让咱们构想一个场景:

在场景中的某个脚本内经常使用 setInterval() 来重复移动场景中的某个节点,当咱们切换场景后会出现什么?

当定时器再次调用回调尝试移动节点的时刻,会不可找到指标节点而报错,由于节点曾经跟着之前的场景一同被销毁了,而定时器还在继续口头。

这种状况下经常使用组件的计时器就不会有这种疑问,由于计时器会随着组件的销毁而被肃清。

而当咱们须要口头一些与游戏场景没无关联的事情的时刻,就可以思考经常使用 setTimeout() 或 setInterval()。

当然能用组件计时器的话最好还是用组件计时器啦~

小结

雷同也画一张图来小小总结一下 Scheduler。

总结

关于引擎的启动流程和主循环就解读到这里啦。

假设有遗漏或失误的中央,也欢迎大家提进去,毕竟熬夜写文章精气恍惚漏了也是情有可原的对吧哈哈哈~

最后的最后,再画一张图来做一个最后的总结~

逐渐爱上画图~

您可能还会对下面的文章感兴趣: