Cocos Creator 是当前非常流行的手游和微信小游戏的开发工具,Cocos Creator 的引擎部分包括 JavaScript 和 C++两个部分,我们团队主要用的是 JS-engine,因此有必要对 JS-engine 进行深入的分析与学习,而分析其源码是一种有效的学习手段。
从入口文件 index.js 来看,引擎的核心还是 cocos2d,源码位于 cocos2d 文件夹,启动引擎为 CCBoot.js,整个引擎的命名空间位于 cc module 之下。而 cocos2d 的重要部分有 game, director, action, node, scene, sprite, render-texture 几个部分。
CCgame 定义了一个核心对象 game,包含了整个游戏的主要状态属性。以下是较为重要的部分:
- EVENTHIDE:"game_on_hide"
- EVENTSHOW:"game_on_show"
- EVENT_GAME_INITED: "game_inited"
- EVENT_RENDERER_INITED: "renderer_inited"
对应于前两个事件要特别注意,源码中给出了重要说明。
事件"game_on_hide": 请注意,在 WEB 平台,这个事件不一定会 100% 触发,这完全取决于浏览器的回调行为。在原生平台,它对应的是应用被切换到后台事件,下拉菜单和上拉状态栏等不一定会触发这个事件,这取决于系统行为。
事件“game_on_show”: 请注意,在 WEB 平台,这个事件不一定会 100% 触发,这完全取决于浏览器的回调行为。在原生平台,它对应的是应用被切换到前台事件。
- RENDER_TYPE_CANVAS: 0
- RENDER_TYPE_WEBGL: 1
- RENDER_TYPE_OPENGL: 2
- _persistRootNodes: {}
- _ignoreRemovePersistNode: null
CONFIG_KEY: {
width: "width",
height: "height",
// engineDir: "engineDir",
debugMode: "debugMode",
exposeClassName: "exposeClassName",
showFPS: "showFPS",
frameRate: "frameRate",
id: "id",
renderMode: "renderMode",
registerSystemEvent: "registerSystemEvent",
jsList: "jsList",
scenes: "scenes"
}- _paused: true, //whether the game is paused
- _configLoaded: false, //whether config loaded
- _isCloning: false, // deserializing or instantiating
- _prepareCalled: false, //whether the prepare function has been called
- _prepared: false, //whether the engine has prepared
- _rendererInitialized: false,
- _renderContext: null,
- _intervalId: null, //interval target of main
- _lastTime: null,
- _frameTime: null,
- _sceneInfos: []
setFrameRate (frameRate) {...}
step () {
cc.director.mainLoop();
}step()函数用于执行一帧游戏循环,实际上是调用执行 cc.director.mainLoop 函数,cc.director 是一个管理游戏的逻辑流程的单例对象。它创建和处理主窗口并且管理什么时候执行场景。mainLoop 函数我们在以后的 director 部分再做详解。
pause () {...}源码里的注释说的很清楚:暂停游戏主循环。包含:游戏逻辑,渲染,事件处理,背景音乐和所有音效。这点和只暂停游戏逻辑的 cc.director.pause 不同。
resume () {...}恢复游戏主循环。包含:游戏逻辑,渲染,事件处理,背景音乐和所有音效。
run () {...}运行游戏,并且指定引擎配置和 onStart 的回调。
addPersistRootNode: function (node) {
if (!cc.Node.isNode(node) || !node.uuid) {
cc.warnID(3800);
return;
}
var id = node.uuid;
if (!this._persistRootNodes[id]) {
var scene = cc.director._scene;
if (cc.isValid(scene)) {
if (!node.parent) {
node.parent = scene;
}
else if ( !(node.parent instanceof cc.Scene) ) {
cc.warnID(3801);
return;
}
else if (node.parent !== scene) {
cc.warnID(3802);
return;
}
this._persistRootNodes[id] = node;
node._persistNode = true;
}
}
}添加常驻根节点,该节点在场景切换时不会被销毁。看源码会检查该 node 的 parent,如果为空则将当前 scene 赋给 node.parent,如果不为空且不等于当前 scene 则会告警退出。还有一些其它的公共方法,就不做详解了,有兴趣的可以参看源代码。
_setAnimFrame () {...}这个函数的主要作用是根据运行环境设置 window.requestAnimFrame,在 web 环境下,应该就是标准的 window.requestAnimationFrame;关于 requestAnimationFrame 在游戏开发中的应用,可以参考这篇文章Anatomy of a video game
_runMainLoop: function () {
var self = this, callback, config = self.config, CONFIG_KEY = self.CONFIG_KEY,
director = cc.director,
skip = true, frameRate = config[CONFIG_KEY.frameRate];
director.setDisplayStats(config[CONFIG_KEY.showFPS]);
callback = function () {
if (!self._paused) {
self._intervalId = window.requestAnimFrame(callback);
if (frameRate === 30) {
if (skip = !skip) {
return;
}
}
director.mainLoop();
}
};
self._intervalId = window.requestAnimFrame(callback);
self._paused = false;
}运行游戏,通过前面设置的 window.requestAnimFrame 函数形成游戏主循环,每帧中运行 director.mainLoop; 关于 mainLoop 函数,会在 director 一章介绍。剩余几个私有方法都是初始化配置信息、初始化渲染器 renderer、初始化事件的函数。
cc.director 是一个 singleton 对象,包含了一些标准方法,这些方法主要用于创建和处理主窗口并且管理场景的执行。cc.director 是由 cc.Director._getInstance 生成的,而这个函数是用 cc.DisplayLinkDirector 这个类构造出 director 的。
cc.DisplayLinkDirector 是继承于 cc.Director,cc.DisplayLinkDirector 定义了 mainLoop 函数和几个控制动画的方法。我们应该重点关注 mainLoop 函数,因为这是游戏运行的主循环函数,下面看看 mainLoop 的定义
/**
* Run main loop of director
*/
mainLoop: CC_EDITOR
? function(deltaTime, updateAnimate) {
if (!this._paused) {
this.emit(cc.Director.EVENT_BEFORE_UPDATE);
this._compScheduler.startPhase();
this._compScheduler.updatePhase(deltaTime);
if (updateAnimate) {
this._scheduler.update(deltaTime);
}
this._compScheduler.lateUpdatePhase(deltaTime);
this.emit(cc.Director.EVENT_AFTER_UPDATE);
}
this.emit(cc.Director.EVENT_BEFORE_VISIT);
// update the scene
this._visitScene();
this.emit(cc.Director.EVENT_AFTER_VISIT);
// Render
cc.g_NumberOfDraws = 0;
cc.renderer.clear();
cc.renderer.rendering(cc._renderContext);
this._totalFrames++;
this.emit(cc.Director.EVENT_AFTER_DRAW);
}
: function() {
if (this._purgeDirectorInNextLoop) {
this._purgeDirectorInNextLoop = false;
this.purgeDirector();
} else if (!this.invalid) {
// calculate "global" dt
this.calculateDeltaTime();
if (!this._paused) {
this.emit(cc.Director.EVENT_BEFORE_UPDATE);
// Call start for new added components
this._compScheduler.startPhase();
// Update for components
this._compScheduler.updatePhase(this._deltaTime);
// Engine update with scheduler
this._scheduler.update(this._deltaTime);
// Late update for components
this._compScheduler.lateUpdatePhase(this._deltaTime);
// User can use this event to do things after update
this.emit(cc.Director.EVENT_AFTER_UPDATE);
// Destroy entities that have been removed recently
cc.Object._deferredDestroy();
}
/* to avoid flickr, nextScene MUST be here: after tick and before draw.
XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
if (this._nextScene) {
this.setNextScene();
}
this.emit(cc.Director.EVENT_BEFORE_VISIT);
// update the scene
this._visitScene();
this.emit(cc.Director.EVENT_AFTER_VISIT);
// Render
cc.g_NumberOfDraws = 0;
cc.renderer.clear();
cc.renderer.rendering(cc._renderContext);
this._totalFrames++;
this.emit(cc.Director.EVENT_AFTER_DRAW);
eventManager.frameUpdateListeners();
}
};从源码来看,主循环主要的步骤为:
- 发送即将更新事件 EVENT_BEFORE_UPDATE;
- 调用组件调度器_compScheduler 运行所有新注册组件的 start 方法,_compScheduler 是在初始化 director 时 new ComponentScheduler()生成的对象实例,ComponentScheduler 定义在 component-scheduler.js 文件;
- 调用_compScheduler 运行所有注册组件的 update 方法;
- 运行所有有效的定时器 update,定时器_scheduler 是在初始化时 new cc.Scheduler()生成的对象实例,cc.Scheduler 的定义在 CCScheduler.js 文件中;
- 调用_compScheduler 运行所有注册组件的 lateUpdate 方法;
- 发送更新完成事件 EVENT_AFTER_UPDATE;
- 发送场景即将更新事件 EVENT_BEFORE_VISIT;
- 更新场景;
- 发送场景更新完成事件 EVENT_AFTER_VISIT;
- 渲染下一帧;
- 发送渲染完成事件 EVENT_AFTER_DRAW
DisplayLinkDirector 继承于 Director,Director 提供了一些对场景操作的方法
// 暂停正在运行的场景,该暂停只会停止游戏逻辑执行,但是不会停止渲染和 UI 响应。如果想要更彻底得暂停游戏,包含渲染,音频和事件,请使用cc.game.pause
pause() {...}
// 恢复暂停场景的游戏逻辑,如果当前场景没有暂停将没任何事情发生。
resume() {...}
// 暂停当前运行的场景,压入到暂停的场景栈中。
pushScene(scene) {...}
// 立刻切换指定场景。
runSceneImmediate(scene, onBeforeLoadScene, onLaunched) {...}
// 运行指定场景
runScene(scene, onBeforeLoadScene, onLaunched) {...}
// 通过场景名称进行加载场景。
loadScene(sceneName, onLaunched, _onUnloaded) {...}
// 预加载场景,你可以在任何时候调用这个方法。
// 调用完后,你仍然需要通过 `cc.director.loadScene` 来启动场景.
// 就算预加载还没完成,你也可以直接调用 `cc.director.loadScene`,加载完成后场景就会启动。
preloadScene(sceneName, onLoaded) {...}