CocosCreator 主循环源码浅析 标签(空格分隔): CocosCreator
MainLoop 游戏有好看的界面、有趣的动画,能响应用户的操作,所以才好玩。基本动画的本质是在于随着时间不断地绘制图片,这跟电影放映原理是有些类似的。但不同之处在于,游戏的界面是根据时间、用户输入、定时器等信息动态生成并绘制的,也就是说需要有逻辑控制游戏内容。我们可以将游戏抽象成以下几个内容:
处理用户事件
处理定时器事件
绘图
应用在运行的过程中,会不断地重复上面的逻辑。我们可以用下面的代码来表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 function onTick ( ) { handleUserEvents(); handleSchedules(); draw(); ... } while (true ) { onTick(); }
上面的代码显然不合理,这会使 CPU 的使用达到 100%,所以需要能每隔一段时间就能执行一次 onTick
的那种。比如定时器:
1 2 setInterval(onTick, 33 )
这样就好多了,但其实我们有更好的选择。屏幕在亮起工作的时候,会不断地刷新,一般手机上是每秒 60 次,在每次屏幕刷新前都执行任务,这会带来几个好处:
平台或浏览器会自动优化刷新时机
窗口没激活时,任务不会工作,能节省 CPU 资源。
接下来,我们看看不同三大平台(浏览器、iOS 原生、Android 原生)如何实现这个功能。
浏览器中的 requestAnimationFrame 在浏览器中,window.requestAnimationFrame()
方法方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。我们利用这个方法来重新实现上面的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var intervalId, callback;callback = function ( ) { intervalId = window .requestAnimationFrame(callback); onTick(); } intervalId = window .requestAnimationFrame(callback);
如上面代码所示,我们通过回调成功地让浏览器在每次重绘前都调用 callback
。
iOS 下的 CADisplayLink iOS 原生的 Core Animation
框架中,有一个 CADisplayLink
类。这是一个计时器对象,它允许应用程序将其绘图与显示的刷新速率同步。它可以绑定一个方法,该方法会被屏幕刷新时调用。我们看一下它的实现方式:
1 2 3 4 5 6 7 let displaylink = CADisplayLink (target: self , selector: #selector(onTick))displaylink.add(to: .current, forMode: .defaultRunLoopMode)
Cocos Creator 在 CCDirectorCaller-ios.mm
文件中使用了 CADisplayLink
。
Android 下的 GLSurfaceView Android 原生有一个类 android.opengl.GLSurfaceView
,它内部定义了 Renderer
接口,该接口声明了 void onDrawFrame(GL10 gl)
方法。该方法负责绘制当前帧。我们可以继承这个类并实现该方法:
1 2 3 4 5 6 7 8 9 10 11 public class Renderer implements GLSurfaceView .Renderer { @Override public void onDrawFrame (final GL10 gl) { onTick(); } public void onTick () { } }
Cocos Creator 在 org.cocos2dx.lib.Cocos2dxRenderer
中实现了 onDrawFrame
方法。
本文以 Web 平台为主,各平台原理基本相同。接下来不会再涉及其它到原生代码。
MainLoop 简化源码 在游戏引擎中,上面的循环中执行的方法 onTick
实际上就是 CCDirector
中的 mainLoop
方法。也就是主循环了。下一节中,我们通过源码来了解其中的基本流程。
源码解析 MainLoop 在引擎中被调用的流程 Cocos Creator 游戏引擎的入口在 main.js
中,它在 cocos2d-js-min.js
加载完后会开始运行游戏:cc.game.run()
。源码如下:
1 2 3 4 5 6 7 8 function boot ( ) { cc.game.run(); } boot();
在 run
方法中,会根据配置进行引擎的准备工作,包括初始化渲染器、全局视图对象 cc.view
、导演 cc.director
还有注册系统各类事件等工作。简化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 var game = { prepare: function ( ) { this ._initRenderer(); cc.view = View ? View._getInstance() : null ; cc.director = cc.Director._getInstance(); cc.director.setOpenGLView(cc.view); this ._initEvents(); this ._runMainLoop(); }, run: function ( ) { this .prepare(); }, } cc.game = game;
从上面的代码中可以看到,引擎在完成初始化工作之后,就会开始运行主循环了:this._runMainLoop();
我们再看一下这个方法的工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var game = { _runMainLoop: function ( ) { var callback, director = cc.director; callback = function ( ) { self._intervalId = window .requestAnimationFrame(callback); director.mainLoop(); } }; self._intervalId = window .requestAnimationFrame(callback); } } cc.game = game;
这里在上文介绍过,利用 window.requestAnimationFrame
及其回调函数来实现循环。循环里调用的是 director.mainLoop()
;
流程图如下:
1 2 3 4 5 6 7 8 boot=>start: Boot() gamerun=>inputoutput: cc.game.run() gameprepare=>inputoutput: cc.game.prepare() gamerunloop=>inputoutput: cc.game._runMainLoop(); requestAnimationFrame=>inputoutput: window.requestAnimationFrame() directormainloop=>inputoutput: director.mainLoop(); boot->gamerun->gameprepare->gamerunloop->requestAnimationFrame->directormainloop->requestAnimationFrame
从上面的流程中,可以看到,director.mainLoop
是引擎的逻辑控制的中枢。接下来,我们看看这个方法内部。
CCDirector cc.director 是一个单例对象,管理你的游戏逻辑流程,它还负责同步定时器与显示器的刷新速率。这个对象在引擎做初始化工作时,就被创建了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var game = { prepare: function ( ) { cc.director = cc.Director._getInstance(); } } cc.Director._getInstance = function ( ) { if (cc.Director.firstUseDirector) { cc.Director.firstUseDirector = false ; cc.Director.sharedDirector = new cc.DisplayLinkDirector(); cc.Director.sharedDirector.init(); } return cc.Director.sharedDirector; };
可以看到,实际上的 cc.director
是 DisplayLinkDirector
类的实例,这个类继承自 Director
:
1 2 3 4 5 6 7 8 cc.DisplayLinkDirector = cc.Director.extend({ mainLoop: function ( ) { } });
mainLoop
方法也是在这个类中实现的。在深入这个方法之前,我们先回顾一下游戏逻辑中最常用的类:cc.Component
,下面列出我们常继承或使用的逻辑流程方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class MyComponent extends cc .Component { onLoad() {} start() {} update(dt) {} lastUpdate() {} onEnable() {} onDisable() {} onDestroy() {} schedule(callback, interval, repeat, delay) { var scheduler = cc.director.getScheduler(); var paused = scheduler.isTargetPaused(this ); scheduler.schedule(callback, this , interval, repeat, delay, paused); } scheduleOnce (callback, delay) { this .schedule(callback, 0 , 0 , delay); } unschedule(callback) { cc.director.getScheduler().unschedule(callback_fn, this ); } unscheduleAllCallbacks () { cc.director.getScheduler().unscheduleAllForTarget(this ); } }
再回到 mainLoop
,简化后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 mainLoop() { this .calculateDeltaTime(); this .emit(cc.Director.EVENT_BEFORE_UPDATE); this ._compScheduler.startPhase(); this ._compScheduler.updatePhase(this ._deltaTime); this ._scheduler.update(this ._deltaTime); this ._compScheduler.lateUpdatePhase(this ._deltaTime); this .emit(cc.Director.EVENT_AFTER_UPDATE); cc.Object._deferredDestroy(); if (this ._nextScene) { this .setNextScene(); } this .emit(cc.Director.EVENT_BEFORE_VISIT); this ._visitScene(); this .emit(cc.Director.EVENT_AFTER_VISIT); cc.g_NumberOfDraws = 0 ; cc.renderer.clear(); cc.renderer.rendering(cc._renderContext); this ._totalFrames++; this .emit(cc.Director.EVENT_AFTER_DRAW); eventManager.frameUpdateListeners(); }
上面的代码展示了 mainLoop
所做的事情。可以看到,在各阶段的前后,都有相应的事件发出:EVENT_BEFORE_XXX
和 EVENT_AFTER_XXX
。接下来,我们分析除绘图渲染外的逻辑部分。
calculateDeltaTime calculateDeltaTime
用于计算本次 mainLoop
调用与上一次调用之间的时间间隔。就是计算 this._deltaTime
计算的值:
1 2 3 4 5 6 7 8 9 10 11 init() { this ._lastUpdate = Date .now(); } calculateDeltaTime() { var now = Date .now(); this ._deltaTime = (now - this ._lastUpdate) / 1000 ; this ._lastUpdate = now; }
计算非常简单,将当前时间与上次计算时的时间相减即可。
组件的生命周期方法 compScheduler
属性是 ComponentScheduler
类的实例。该类定义在 component-scheduler.js
类中。 在进入这个方法的代码之前,我们先看一下添加组件逻辑。
CCObject 在 CocosCreator 中的,cc.Node
、cc.Component
等大部分类的基类是 CCObject
。来看一下它的定义:
1 2 3 4 5 6 class CCObject { constructor () { this ._name = '' ; this ._objFlags = 0 ; } }
可以看到,CCObject
有两个属性。其中值得注意的是 _objFlags
,它被引擎用来标识对象的部分状态,这需要用到标志位、与操作、或操作、位移操作符。以下是引擎定义的部分标志位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var Destroyed = 1 << 0 ;var RealDestroyed = 1 << 1 ;var ToDestroy = 1 << 2 ;var DontSave = 1 << 3 ;var EditorOnly = 1 << 4 ;var DontDestroy = 1 << 6 ;var Destroying = 1 << 7 ;var Deactivating = 1 << 8 ;var IsOnEnableCalled = 1 << 11 ;var IsEditorOnEnableCalled = 1 << 12 ;var IsPreloadStarted = 1 << 13 ;var IsOnLoadCalled = 1 << 14 ;var IsOnLoadStarted = 1 << 15 ;var IsStartCalled = 1 << 16 ;
从上面的定义的变量名,可以得知标志位的作用。比如,判断一个组件的 onLoad
方法是否调用过:
1 2 3 4 5 6 _isOnLoadCalled: { get () { return this ._objFlags & IsOnLoadCalled; } },
addComponent CCNode
和 CCComponent
都有方法:addComponent
。实际上组件的这个方法调用的就是节点的方法:
1 2 3 4 addComponent (typeOrClassName) { return this .node.addComponent(typeOrClassName); },
所以只需要关注 CCNode
里的实现即可,这个方法是定义在其父类 base-node
中的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 addComponent (typeOrClassName) { var constructor = typeOrClassName; if (typeof typeOrClassName === 'string') { constructor = JS.getClassByName(typeOrClassName); } // 新建组件并添加到 _components 数组中 var component = new constructor (); component.node = this; this._components.push(component); // 如果 node 及其父组件链是 active 状态,则调用组件的 onLoad 方法。 if (this._activeInHierarchy) { cc.director._nodeActivator.activateComp(component); } return component; }
添加组件的逻辑非常简单,先找到组件的类,再创建组件对象,并保存对象。值得一提的是 _nodeActivator
,这是对象是 NodeActivator
类的实例,在 cc.director
初始化时创建。NodeActivator
是用于执行节点和组件的 activating
和 deactivating
操作的类。下面是该类的 activateComp
方法的简化版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 activateComp: function (comp ) { if (!(comp._objFlags & IsPreloadStarted)) { comp._objFlags |= IsPreloadStarted; comp.__preload(); } if (!(comp._objFlags & IsOnLoadStarted)) { comp._objFlags |= IsOnLoadStarted; if (comp.onLoad) { comp.onLoad(); } comp._objFlags |= IsOnLoadCalled; } if (comp._enabled) { cc.director._compScheduler.enableComp(comp); } }
activateComp
的工作就是调用组件的 onLoad
方法,并且保证只调用一次。再将组件添加到 _compScheduler
中并启用组件。
ComponentScheduler ComponentScheduler
用于统一管理所有组件的生命周期方法,方法包括:start
、upate
、lateUpdate
。_compScheduler
在 cc.director
初始化时创建。从上面的代码中,可以看出通过 enableComp
方法将组件管理起来的。下面是该类代码的简化版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 class ComponentScheduler { constructor () { this .scheduleInNextFrame = []; this ._updating = false ; this .startInvoker = []; this .updateInvoker = [];\ this .lateUpdateInvoker =[]; } enableComp (comp) { if (!(comp._objFlags & IsOnEnableCalled)) { if (comp.onEnable) { comp.onEnable(); } this ._onEnabled(comp); } } _onEnabled (comp) { cc.director.getScheduler().resumeTarget(comp); comp._objFlags |= IsOnEnableCalled; if (this ._updating) { this .scheduleInNextFrame.push(comp); } else { this ._scheduleImmediate(comp); } } _scheduleImmediate (comp) { if (comp.start && !(comp._objFlags & IsStartCalled)) { this .startInvoker.add(comp); } if (comp.update) { this .updateInvoker.add(comp); } if (comp.lateUpdate) { this .lateUpdateInvoker.add(comp); } } _deferredSchedule () { var comps = this .scheduleInNextFrame; for (var i = 0 , len = comps.length; i < len; i++) { var comp = comps[i]; this ._scheduleImmediate(comp); } comps.length = 0 ; } startPhase () { this ._updating = true ; if (this .scheduleInNextFrame.length > 0 ) { this ._deferredSchedule(); } this .startInvoker.forEach(comp => { if (!(comp._objFlags & IsStartCalled)) { comp._objFlags |= IsStartCalled; comp.start(); } }); } updatePhase (dt) { this .updateInvoker.forEach(comp => { comp.update(dt); }); } lateUpdatePhase (dt) { this .lateUpdateInvoker.forEach(comp => { comp.lateUpdate(dt); }); this ._updating = false ; } }
可以看到,该类提供了三个方法给外部,用于统一管理组件生命周期方法。它们在前文提到的主循环里有调用:
1 2 3 4 5 6 7 8 mainLoop() { this ._compScheduler.startPhase(); this ._compScheduler.updatePhase(this ._deltaTime); this ._scheduler.update(this ._deltaTime); this ._compScheduler.lateUpdatePhase(this ._deltaTime); }
组件生命周期流程 从上面的代码可以分析出,当添加组件到节点后,组件被托管到 _compScheduler
统一管理。再由 mainLoop
通过 _compScheduler
调用到组件的相应方法。
参考 1、cocos2d-x游戏引擎核心之三——主循环和定时器 2、JavaScript 技术文档 3、CADisplayLink 4、cocos-creator/engine