在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言预备不知道你有没有想过,假如把游戏世界比作一辆汽车,那么这辆“汽车”是如何启动,又是如何持续运转的呢? 如题,本文的内容主要为 Cocos Creator 引擎的启动流程和主循环。 而在主循环的内容中还会涉及到:组件的生命周期和计时器、缓动系统、动画系统和物理系统等... 本文会在宏观上为大家解读主循环与各个模块之间的关系,对于各个模块也会简单介绍,但不会深入到模块的具体实现。 因为如果把每个模块都“摸”一遍,那这篇文章怕是写不完了。 Go!希望大家看完这篇文章之后能够更加了解 Cocos Creator 引擎。 同时也希望本文可以起到“师傅领进门”的作用,大家一起加油修行鸭~ 另外《源码解读》系列(应该)会持续更新,如果你想要皮皮来解读解读引擎的某个模块,也欢迎留言告诉我,我...我考虑下哈哈哈~ 本文以 Cocos Creator 2.4.3 版本为参考。 正文启动流程index.html 对于 Web 平台 index.html 文件就是绝对的起点。 在默认的 index.html 文件中,定义了游戏启动页面的布局,加载了 main.js 文件,并且还有一段立即执行的代码。 这里截取文件中一部分比较关键的代码: // 加载引擎脚本 loadScript(debug ? 'cocos2d-js.js' : 'cocos2d-js-min.ec334.js', function () { // 是否开启了物理系统? if (CC_PHYSICS_BUILTIN || CC_PHYSICS_CANNON) { // 加载物理系统脚本并启动引擎 loadScript(debug ? 'physics.js' : 'physics-min.js', window.boot); } else { // 启动引擎 window.boot(); } }); 上面这段代码主要用于加载引擎脚本和物理系统脚本,脚本加载完成之后就会调用 main.js 中定义的 💡 对于原生平台,会在 🧵 代码压缩 脚本文件名中带有 压缩代码可以节省代码文件所占用的空间,加快文件加载速度,减少流量消耗,但同时也让代码失去了可阅读性,不利于调试。 所以开启调试模式后会直接使用未经过压缩的代码文件,便于开发调试和定位错误。 main.js window.boot() 对于不同平台 main.js 的内容也有些许差异,这里我们忽略差异部分,只关注其中关键的共同行为。 关于 main.js 文件的内容基本上就是定义了 💡 对于非 Web 平台,会在定义完之后直接就调用 而
这部分的代码就不贴了,小伙伴们可以看看自己的项目构建后的 main.js 文件。 cc.game
说人话, CCGame.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCGame.js run()
run: function (config, onStart) { // 指定引擎配置 this._initConfig(config); this.onStart = onStart; this.prepare(game.onStart && game.onStart.bind(game)); } 传送门:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCGame.js#L491 prepare()
prepare(cb) { // 已经准备过则跳过 if (this._prepared) { if (cb) cb(); return; } // 加载预览项目代码 this._loadPreviewScript(() => { this._prepareFinished(cb); }); } 传送门:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCGame.js#L472 对于快速编译的细节,可以在项目预览时打开浏览器的开发者工具,在 Sources 栏中搜索(Ctrl + P) _prepareFinished()
当内建资源加载完成后就会调用 _prepareFinished(cb) { // 初始化引擎 this._initEngine(); // 设置帧率计时器 this._setAnimFrame(); // 初始化内建资源(加载内置的 effect 和 material 资源) cc.assetManager.builtins.init(() => { // 打印引擎版本到控制台 console.log('Cocos Creator v' + cc.ENGINE_VERSION); this._prepared = true; // 启动 mainLoop this._runMainLoop(); // 发射 ‘game_inited' 事件(即引擎已初始化完成) this.emit(this.EVENT_GAME_INITED); // 调用 main.js 中定义的 onStart 函数 if (cb) cb(); }); } 传送门:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCGame.js#L387 💡 对于 _setAnimFrame()
另外还对 这里就不贴 传送门:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCGame.js#L564 _runMainLoop()
让我们瞧瞧代码: _runMainLoop: function () { if (CC_EDITOR) return; if (!this._prepared) return; // 定义局部变量 var self = this, callback, config = self.config, director = cc.director, skip = true, frameRate = config.frameRate; // 展示或隐藏性能统计 debug.setDisplayStats(config.showFPS); // 设置帧回调 callback = function (now) { if (!self._paused) { // 循环调用回调 self._intervalId = window.requestAnimFrame(callback); if (!CC_JSB && !CC_RUNTIME && frameRate === 30) { if (skip = !skip) return; } // 调用 mainLoop director.mainLoop(now); } }; // 将在下一帧开始循环回调 self._intervalId = window.requestAnimFrame(callback); self._paused = false; } 传送门:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCGame.js#L612 通过以上代码我们可以得知, window.requestAnimFrame()
对前端不太熟悉的小伙伴可能会有疑问, window.requestAnimationFrame() 简单来说,
MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/now 回调函数的执行次数通常与浏览器屏幕刷新次数相匹配,也就是说,对于刷新率为 60Hz 的显示器,浏览器会在一秒内执行 60 次回调函数。 对于 MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame 小结 画一张图来对引擎的启动流程做一个小小的总结叭~ 主循环经历了一番波折后,终于来到了最期待的引擎主循环部分,话不多说,我们继续! cc.director
CCDirector.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCDirector.js mainLoop() 🍖 现在让我们进入 这里我选择性剔除掉了函数中一些的代码,还搞了点注释: mainLoop: function(now) { // 计算“全局”增量时间(DeltaTime) // 也就是距离上一次调用 mainLoop 的时间间隔 this.calculateDeltaTime(now); // 游戏没有暂停则进行更新 if (!this._paused) { // 发射 'director_before_update' 事件 this.emit(cc.Director.EVENT_BEFORE_UPDATE); // 调用新增的组件(已启用)的 start 函数 this._compScheduler.startPhase(); // 调用所有组件(已启用)的 update 函数 this._compScheduler.updatePhase(this._deltaTime); // 更新调度器(cc.Scheduler) this._scheduler.update(this._deltaTime); // 调用所有组件(已启用)的 lateUpdate 函数 this._compScheduler.lateUpdatePhase(this._deltaTime); // 发射 'director_after_update' 事件 this.emit(cc.Director.EVENT_AFTER_UPDATE); // 销毁最近被移除的实体(节点) Obj._deferredDestroy(); } // 发射 'director_before_draw' 事件 this.emit(cc.Director.EVENT_BEFORE_DRAW); // 渲染游戏场景 renderer.render(this._scene, this._deltaTime); // 发射 'director_after_draw' 事件 this.emit(cc.Director.EVENT_AFTER_DRAW); // 更新事件管理器的事件监听(cc.eventManager 已被废弃) eventManager.frameUpdateListeners(); // 累加游戏运行的总帧数 this._totalFrames++; } 传送门:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/CCDirector.js#L843 接下来我们来对主循环中的关键点一一进行分解。 ComponentScheduler
大多数小伙伴可能对于 将 说人话, 文字不够直观,看完下面这张图大概就懂了: component-scheduler.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/component-scheduler.js startPhase // 调用新增的组件(已启用)的 start 函数 this._compScheduler.startPhase(); 组件的 在组件的一生中 只不过
而 🥁 NodeActivator
像这样: node-activator.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/node-activator.js updatePhase // 调用所有组件(已启用)的 update 函数 this._compScheduler.updatePhase(deltaTime); 组件的 lateUpdatePhase // 调用所有组件(已启用)的 lateUpdate 函数 this._compScheduler.lateUpdatePhase(deltaTime); 组件的 ParticleSystem BTW,粒子系统组件( CCParticleSystem.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/particle/CCParticleSystem.js Tips 请谨慎使用 📢 注意这不是不让你用,该用还得用,只是不要滥用,不要啥玩意都往里边赛~ Scheduler
Scheduler.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/Scheduler.js 💣 你绝对猜不到下面这一行看起来如此平平无奇的代码执行之后会发生什么。 // 更新调度器(cc.Scheduler 类实例) this._scheduler.update(this._deltaTime);
系统模块 调度器的更新会先触发以下系统模块的更新:
以上这些模块都以 除了 ActionManager
CCActionManager.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/actions/CCActionManager.js AnimationManager
animation-manager.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/animation/animation-manager.js CollisionManager
CCCollisionManager.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/collider/CCCollisionManager.js PhysicsManager
CCPhysicsManager.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/physics/CCPhysicsManager.js Physics3DManager
physics-manager.ts:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/3d/physics/framework/physics-manager.ts InputManager
CCInputManager.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d\core\platform\CCInputManager.js 组件计时器 相信大多数小伙伴都使用过组件的 实际上, 组件的 而 CCComponent.js:https://github.com/cocos-creator/engine/blob/2.4.3/cocos2d/core/components/CCComponent.js [文档] 使用计时器:http://docs.cocos.com/creator/manual/zh/scripting/scheduler.html 另外我还注意到,有不少小伙伴还不是很清楚组件的计时器和 setTimeout & setInterval
💡 再补充一个小知识: 在浏览器中 如果是未激活(后台)的标签页(tab),最小延时(间隔)则加长到 1000ms。 🌰 举个栗子 假如我在当前标签页设置了一个每 500ms 输出一个 log 的定时器,当我切换到别的标签页之后,那么这个定时器就会变成每 1000ms 才输出一个 log。 像这样,感兴趣的话可以自己去试试: setInterval(() => { console.log(new Date().getTime()); }, 500); // 模拟输出 // 标签页在前台 // 1604373521000 // 1604373521500 // 1604373522000 // 切换到别的标签页后 // 1604373523000 // 1604373524000 // 1604373525000 区别 & 用法 组件的计时器依赖于引擎的
当你需要在组件内部定时或重复执行某一函数或操作某个节点,那么可以使用组件的计时器。 💬 让我们想象一个场景: 在当前场景中的某个脚本内使用 当定时器再次调用回调尝试移动节点的时候,会无法找到目标节点而报错,因为节点已经跟着之前的场景一起被销毁了,而定时器还在继续执行。 这种情况下使用组件的计时器就不会有这种问题,因为计时器会随着组件的销毁而被清除。 而当我们需要执行一些与游戏场景没有关联的事情的时候,就可以考虑使用 🎃 当然能用组件计时器的话最好还是用组件计时器啦~ 小结依然还是画一张图来小小总结一下 总结关于引擎的启动流程和主循环就解读到这里啦。 最后的最后,还是画张图来做一个最后的总结~ 以上就是解读CocosCreator源码之引擎启动与主循环的详细内容,更多关于CocosCreator源码解读的资料请关注极客世界其它相关文章! |
请发表评论