在一位Cocos资深开发者的Creator 之旅(上)这篇文章里,我们的老朋友SuperSuRaccoon深入浅出地把Creator 分析了透透底底,一点小秘密都没给我们留。今天,这个精彩的旅程将继续走下去。
Part.4 逻辑树 vs 渲染树
初看 CCC 的文档,一个比较新的概念,就是 逻辑树 vs 渲染树,当然这也是比较让人迷惑的概念。但是在我们有了上面这些知识的积累之后,就可以比较好的来理解这个意义了。
渲染树
在过去的 cocos2d-js 中,我们在创建一个游戏场景的时候,通常是这样一个步骤(就拿最简单的,自带的 HelloWorld 举例):
// 首先要有一个 Scene
var HelloWorldScene = cc.Scene.extend({
onEnter:function () {
this._super();
var layer = new HelloWorldLayer();
this.addChild(layer);
}
});
// 其次是这个 Scene 中的一个 Layer
var HelloWorldLayer = cc.Layer.extend({
ctor:function () {
this._super();
// ...
var helloLabel = new cc.LabelTTF(/*...*/);
this.addChild(helloLabel);
//
this.sprite = new cc.Sprite(/*...*/);
this.addChild(this.sprite);
}
});
// 最后,是在启动的时候,运行这个 Scene
cc.game.onStart = function(){
// ...
cc.director.runScene(new HelloWorldScene());
};
cc.game.run();
这是一个非常经典,简单的,启动流程。 对于这样一个例子,在实际生成的场景中,渲染树的内容是这样的 :
+ Canvas (游戏渲染在浏览器的一个 Canvas 标签中)
+ CCScene
+ CCLayer
- CCSprite :
HelloWorld.png
- CCLabelTTF:
"HelloWorld"
再来看官方的解释就很好理解了 :
在cocos2d-js中,渲染器会遍历场景节点树来生成渲染队列,所以开发者构建的节点树实际上就是渲染树。
我们创建的 cc.Scene,cc.Layer,cc.Sprite,cc.LabelTTF 这些节点构成的树,直接就是渲染树。
逻辑树
在 CCC,中 逻辑树 的概念被添加了进来,官方的解释中,有这几点很重要 :
开发者在编辑器中搭建的节点树和挂载的组件共同组成了逻辑树 节点构成实体单位,组件负责逻辑 逻辑树关注的是游戏逻辑而不是渲染关系, 逻辑树会生成场景的渲染树,决定渲染顺序,开发者并不需要关心这些。
第1,2点,编辑器中添加到节点,其实都是 cc.Node 对象,我们已经知道,它只是一个空壳,一个容器,虽然持有了过去 cc.Node 的基础属性,和方法,它并不负责任何渲染的任务。 同样的,在它身上挂载的这些组件,都继承自 cc.Component ,即使是继承自 cc.SGComponent 的组件,也并不是直接负责渲染的,负责渲染的,是背后对应的 _ccsg.Xxx 对象们。
第3,4点,逻辑树关注的是游戏逻辑,不是渲染关系,因为 CCC 已经将渲染的细节进行了隐藏,我们在编辑器中只是负责将一些节点的顺序进行组织,对于那些需要渲染的节点中的组件,CCC 会将这些逻辑树上的节点,翻译成一颗渲染树,在幕后,就像过去一样,为我们渲染出一个游戏世界来。
渲染树的生成
既然 CCC 会为我们自动生成想过去一样的渲染树,那么它究竟是怎么做到的呢。 同样的,拿一个简单的 CCC 的 HelloWorld,来做例子。 注意,这里为了简化例子,删除了原本 cc.Scene 中自带的 cc.Canvas 节点,以及一个背景节点。
首先它的起点,就和 cocos2d-js 中一样,在编辑器中需要一个 cc.Scene,接着就是将需要的图片和文本,以组件挂载节点的形式,被添加到场景中,它的结构基本上是这样的 :
+ Canvas (游戏渲染在浏览器的一个 Canvas 标签中)
+ cc.Scene
+ cc.Node (cocos)
- Sprite Component
- HelloWorld.png
+ cc.Node (label)
- Label Component
- "HelloWorld"
这里所谓的 渲染树的自动生成 结合前面,对于 cc.Node,cc._BaseNode 等一些基础类的分析,其实可以很容易的理解。 在编辑器中,我们只是不停的重复 新建节点,挂载组件,新建节点,挂载组件,…… 的操作步骤,以此来构建我们的游戏场景内容。
每当我们添加一个 cc.Node ,在它的幕后,都会有一个 _ccsg.Node 被同样的添加到了场景中,当我们添加一个 cc.Sprite 组件,同样的,一个用于渲染的节点 cc.Scale9Sprite 被创建,并添加到了之前的 _ccsg.Node 中,这一点在源码中也有体现 :
var RendererUnderSG = cc.Class({
extends: require('./CCSGComponent'),
ctor: function () {
var sgNode = this._sgNode = this._createSgNode();
// ...
},
__preload: function () {
// ...
this._appendSgNode(this._sgNode);
},
// ...
_appendSgNode: function (sgNode) {
var node = this.node;
// ...
var sgParent = node._sgNode;
sgParent.addChild(sgNode);
}
});
换句话说,上面的逻辑树的示意图,其实,在它的幕后,一颗渲染树,已经自动的被生成了 :
Part.5 谁动了我的 cocos2d-js
对于有 cocos2d-js 经验的朋友来说,就像本人,需要适应 CCC 中这种编程理念的转变,同时,也经常会碰到一些自己认为这么写理所当然正确,但是在 CCC 中却行不通的情况。 现在,我们有了上面这些知识的积累,现在该是时候来彻底的理解这些过去一知半解的情况了。
cc.DrawNode
cc.DrawNode 作为 cocos2d-js 中的一个绘图类,使用还是极其广泛的,尤其对于本人来说,做一些游戏,比起使用 cc.Sprite,更喜欢使用 cc.DrawNode 来绘制各种图形内容,作为游戏中的元素。此外,利用 cc.DrawNode 来做游戏中的一些调试性绘图,也是非常有用的一种情况。 但是刚接触 CCC 的时候,一个比较困惑的错误来自于下面 :
// helloworld.js
cc.Class({
extends: cc.Component,
properties: {
},
onLoad: function () {
var draw = new cc.DrawNode();
draw.drawDot(cc.p(0, 0), 20, cc.Color.RED);
this.addChild(draw);
},
});
添加一个 cc.DrawNode,绘制一个圆,非常简单的代码,却会报错 :
Uncaught TypeError: this.addChild is not a function
接着,尝试着 :
this.node.addChild(draw);
但是同样会报错 :
addChild: The child to add must be instance of cc.Node, not _Class.
从报错,可以看出,cc.DrawNode 被成功的创建了,并没有提示说不认识这个类,只是在添加到场景的时候,出现了各种错误。 如果在不了解新的 CCC 的设计理念的话,这个问题是无解的, 但是现在我们可以轻松的解决这个问题。
cc.DrawNode Hack
这是一种最最快速的,最最直接的方法,CCC 的论坛也有提到,那就是直接将创建好的 cc.DrawNode 实例,添加到渲染树对应的节点上 :
this.node._sgNode.addChild(draw);
这就可以正常的在游戏中使用 cc.DrawNode 了 。
但是对于这种方式,官方的说明是,这是一种 hack 的方式,并不推荐,因为后期官方可能会禁止 ._sgNode 这种访问方式。
cc.DrawNode Component
好吧,既然这是一种 hack 的方式,不推荐,那我们就自然而然的需要寻找一种合理的方式,迎合 CCC 的设计模式,创建一个 cc.DrawNode 的组件,应该是最好的解决方案。 首先,这是一个需要渲染的组件,我们需要它继承自 cc.SGComponent 中的 cc.RenderUnderSG :
// DrawNodeComponent.js
cc.Class({
extends: cc._RendererUnderSG,
});
紧接着,自然是实现 cc.SGComponent 中的两个重要的接口 :
// DrawNodeComponent.js
cc.Class({
extends: cc._RendererUnderSG,
//
_createSgNode: function () {
return new cc.DrawNode();
},
_initSgNode: function () {
},
});
这里的幕后渲染工作的执行者,自然是我们的 cc.DrawNode。 有了实际的渲染对象,接下来,就是提供一些方法,给外界调用,否则外接就需要直接访问 ._sgNode 了 :
// DrawNodeComponent.js
cc.Class({
extends: cc._RendererUnderSG,
//
_createSgNode: function () {
return new cc.DrawNode();
},
_initSgNode: function () {
},
//
drawDot: function(pos, radius, color) {
this._sgNode.drawDot(pos, radius, color);
}
});
最后,自然就是使用了,在 CCC 编辑器中将这个组件挂在到节点上,就可以在脚本中使用了 :
this.getComponent("DrawNodeComponent").drawDot(
cc.visibleRect.center, 20, cc.Color.RED
);
同样的结果,不一样的实现 :
当然,这里的实现才是正统的,符合 CCC 的设计理念的做法。
cc.Menu && cc.MenuItem
除了上面提到的 cc.DrawNode 外,个人觉得 cocos2d-js 中另一个很有用的类就是 cc.Menu,配合 cc.MenuItemFont 之类的使用,可以非常快速的创建一个菜单。 cc.Menu 在 CCC 使用比起上面的 cc.DrawNode 要更复杂一些,一个原因在于,它并没有被包含到标准生成的引擎文件中 :
cc.log(cc.Menu); // 显示 undefined
换句话说,想要启用 cc.Menu 就需要将它添加到引擎中。 引擎的定制,可以通过修改 CCC 中的一个文件实现 : CocosCreator.app/Contents/Resources/engine/gulp/tasks/modular.js
var modules = {
'Core': [
// ...
'./cocos2d/core/base-nodes/CCSGNode.js',
'./cocos2d/core/scenes/CCSGScene.js',
'./cocos2d/core/layers/CCLayer.js',
],
'Sprite': [
// ...
'./cocos2d/core/sprites/CCSGSprite.js',
],
'Label': [
'./cocos2d/core/label/CCSGLabel.js',
'./cocos2d/core/label/CCSGLabelCanvasRenderCmd.js',
'./cocos2d/core/label/CCSGLabelWebGLRenderCmd.js',
],
// ...
};
可以看到这里列出了所有会被打包到引擎中的模块,而我们需要的 cc.Menu 并不在其中,因此只要把需要的模块,加入即可,当然这里必须注意加入文件的顺序的先后,不然还是会报错的,其实可以参考 cocos2d-js 中的模块导入顺序,例如 cocos2d-x v3.10 引擎中的文件 : cocos2d-x-3.10/web/moduleConfig.json 参考上面的文件,添加模块 cc.Menu 和它需要依赖的 cc.LabelTTF 等模块 :
// ...
'Label': [
// ...
'./cocos2d/core/labelttf/LabelTTFPropertyDefine.js',
'./cocos2d/core/labelttf/CCLabelTTF.js',
'./cocos2d/core/labelttf/CCLabelTTFCanvasRenderCmd.js',
'./cocos2d/core/labelttf/CCLabelTTFWebGLRenderCmd.js'
],
'Menu' : [
'./cocos2d/menus/CCMenu.js',
'./cocos2d/menus/CCMenuItem.js'
],
// ...
然后使用 gulp 打包引擎 :
cd CocosCreator.app/Contents/Resources/engine
gulp build
刷新工程后,就可以在 CCC 中使用 cc.Menu 了 :
// helloworld.js
cc.Class({
extends: cc.Component,
onLoad: function () {
cc.MenuItemFont.setFontSize(20);
cc.MenuItemFont.setFontName("Verdana");
var menuItem1 = new cc.MenuItemFont("Item1", this.item1Callback, this);
var menuItem2 = new cc.MenuItemFont("Item2", this.item2Callback, this);
var menuItem3 = new cc.MenuItemFont("Item3", this.item3Callback, this);
var menu = cc.Menu.create(menuItem1, menuItem2, menuItem3);
this.node._sgNode.addChild(menu);
},
item1Callback: function() {
cc.log("item1Callback");
},
item2Callback: function() {
cc.log("item2Callback");
},
item3Callback: function() {
cc.log("item3Callback");
}
});
这里为了测试方便,使用了 hack 的方式将 ccMenu 添加到了场景中,我们当然也可以像 cc.DrawNode 一样,为其设计对应组件,来迎合 CCC 的工作方式。
看到这里,其实我们就可以了解到,过去的 cocos 里的那些模块,并没有被废弃,它们还是在那里,只是官方并没有将其集成到 CCC 中,或是因为时间不够,或是因为认为没有必要。
但是不管怎样,个人的理解是,只要 CCC 提拱了一个完善的框架,那么即使部分模块的缺失,也是完全可以通过自己来解决弥补的,毕竟这就是开源的好处,放着好好的源码不看,这不是暴残天物吗...
(作者还计划有生命周期、常见设计模式、最佳实践、编辑器扩展、Gizmo、Native等多个章节。期待SuperSuRaccoon继续完成这系列教程!)