原文作者:SupserSuRaccoon
原文出处:SuperSuRaccoon的个人博客
经过作者同意,我们得到授权转载。
Part.0 前言
本文基于 Cocos Creator v1.2 所写。
操作系统平台: Mac OS 10.11。浏览器使用 Chrome 。
如果有问题,欢迎到本人博客留言。
目的
这篇文章的目的十分简单明确,通过将 Cocos Creator 与 Cocos2d 引擎进行比较,分析 Cocos Creator 在原有的已经十分成熟的引擎上,做出了那些修改,以及为什么要做这些修改,从而达更进一步了解这一款新的产品,并且更好的使用它,来开发游戏。
现状
Cocos Creator 目前还处于不是很稳定的时期,虽然大部分的基础模块已经到位,但是仍然有不少的功能并没有对应到 Cocos Creator 中,同时,因为 Cocos Creator 需要对应各种浏览器以及移动,桌面的原生平台,各种 BUG 的数量必然也是很多的。
因此本文所写的一些东西可能在新版本中会不再适用,但是对于 Cocos Creator 一些底层解析应该是同样适用于今后的版本的。
下文中 Cocos Creator 将简称为 CCC。
感受
作为一个 cocos2d 引擎的老用户,从最早的 cocos2d-iphone v0.9x 开始,用 objective-c 写游戏,慢慢的转移到 cocos2d-x v0.8x,用 C++,在脚本的分支出现后,又转战 cocos2d-html5,后升级到 cocos2d-js,一直至今。cocos 的编辑器也用过不少, CocosBuilder,SpriteBuilder,CocosStudio 等等。在接触了 CCC 这不到一个月的时间里,最大的感触是,CCC 在设计理念是,是对于过去一直以来的 cocos 游戏开发方式的一种革新。
这不到一个月的时间,本人利用业余时间,写了不少的东西,这些东西基本都是和渲染密切相关的,为的就是测试一下现在的 CCC 能够做到什么程度,当然测试下来的效果还是很让人满意的 :




下面是本人这一个月不到的使用感受 :
- 现在的 CCC ,从某种程度上来讲,就如同当年的 cocos2d-x 0.xx 版一样,想要在网上搜索一些教程之类的,其实是很困难的,有些遇到的问题,几乎都要靠自己想办法解决
- 文档的缺失,是现在 CCC 最大的问题,但是这也是没有办法的,毕竟这是个时间精力的问题,把有限的精力,放在最重要的工作上,才是正确的选择
- 其实对于一个好的程序员来说,学会看源码是极其基础的能力,你想要的大部分功能,在 CCC自带的源码中,其实已经都有,只是你需要自己去寻找
- 从另一方面来说,现在的 CCC 比过去的 cocos2d-x 0.xx 版相比,要成熟的太多,毕竟它的幕后,就是我们所熟悉的 cocos2d-x 和 cocos2d-js 引擎在支撑
- 如果你觉得 CCC 削减了很多过去的功能模块,那只能说你根本就没有看源码,没有去深入的学习一下 CCC,你想要的所有东西,其实都在,只是官方还没有时间, 将它们集成到 CCC 中
- 接着上面一点,CCC 最大的优点,就是提供了基于实体组件的开发框架,如果你了解了这一点,过去你在 cocos2d-x 引擎中实现的几乎任何模块,都可以自己在 CCC 中实现
- 相比老用户,也许没有 cocos 游戏编程经验的朋友,要更能上手 CCC,因为老用户们,往往会被过去的观念,和做法约束,而这些做法在 CCC 中往往已经不在可行
- CCC 在编辑器这一方面还是有着非常强大的飞跃的,比起之前让人无力吐槽的 CocosStudio,用 javascript 编写,并且和游戏使用一致引擎 API 的编辑器,绝对是无比强大的,编辑器中的可视化功能,加上 Gizmo 的扩展,可以制作出很棒的编辑器
- 当然同上,编辑器扩展这一块的文档的缺失是更为严重的,加上编辑器并未开源,导致不少情况下,只能通过打印一些对象的属性和方法,加上猜测和实践,才能实现自己想要的效果
- CCC 编辑器的开发使用了现在很多流行的技术,如果深入学习必然是获益匪浅的,如 node.js,npm,gulp,ES6,Babel 等等
- 现在的 CCC 社区还是十分活跃的,大部分的问题,在这里基本上都能找到答案,毕竟有 cocos 引擎这么多年的用户积累
- 最后想说的,是 CCC 绝对是有着光明前途的,当然最大的一个前提,就是官方能够做到不离不弃,即使是一步一个脚印,慢慢的走,相信在官方和广大爱好者的帮助下,CCC 一定会成功的
注意
下面的是关于阅读文章的一些注意点 :
- 这篇文章是通过将 CCC 和过去的 cocos 引擎就行比较而写,因此阅读的朋友最好具有过去的 cocos 引擎游戏开发的知识
- 这篇文章的很多内容是本人根据 CCC 中自带的源码分析得出的,难免会有错误,请见谅
- 这篇文章的内容都是基于 Web 而写,换句话说,也许会有 Native 下不适用的情况出现
Part.1 CCC 是什么
CCC 作为一款新的游戏开发工具 ,我们第一要关注的就是它到底提供了什么东西。
通过官网我们可以下载到 Cocos Creator.app,通过浏览文件夹中的内容,可以看到,这款工具主要可以分为三个部分 :
- Editor
- cocos2d-x-lite
- cocos2d-js-lite
不同于 cocos2d-x , cocos2d-js 的一个重要的点,就是它是一款编辑器,类似于之前的 CocoStudio或是早期的 CocosBuilder ,SpriteBuilder 等。
CCC 主要由三个部分组成 :
Editor
首先,毫无疑问的它是又一款编辑器,你可以把它理解为 CocoStudio,CocosBuilder 或是 SpriteBuilder。它也包含了 UI 编辑,动画编辑等功能。但是同时,与之前其他的编辑器相比,它又是有着翻天覆地的革新的一款编辑器,这在以后会慢慢讲到,现在,我们只需要知道它是一款类似于 Unity3D 的游戏编辑器。
Cocos2d-x-lite
CCC 制作的游戏支持平台 :
iOS / Android / OS X / Windows
因此理所当然的,它内置了 cocos2d-x 引擎,但是这里的引擎和我们平时所用的是有所区别的。
官方对于内置的 cocos2d-x 的定义是,这是一个为 CCC 所专门定制过的 lite 版引擎,简称为 cocos2d-x-lite。
其实简单的看一下源码以后会发现, 它是在 cocos2d-x 引擎的基础上,去掉了 3D 模块,物理引擎模块,对 CocoStudio 及 CocosBuilder 的支持等等。
具体的定制内容,可以查看附带在产品中的说明 :
CocosCreator.app/Contents/Resources/cocos2d-x/README.md
此外,我们也可以在 github 上下载到 lite 版的源码 :
https://github.com/cocos-creator/cocos2d-x-lite
cocos2d-js-lite
C++ 版本有了 lite 版,对应的 javascript 分支必然也有相对应的 lite 版本。
由于这次的 CCC 编辑器是完全用 javascript 编写的,因此这里的 cocos2d-js-lite 版将会同时为编辑器以及最后打包生成的游戏所服务。
引擎的具体变化,后面会详细的涉及到,一些相关的说明,同样可以查看附带在产品中的说明 :
CocosCreator.app/Contents/Resources/engine/README.md
同样,lite 版的下载地址 :
https://github.com/cocos-creator/engine
Part.2 设计理念
官方在文档中提出 :
过去使用 cocos 引擎开发游戏,属于代码驱动的模式,而现在的 ccc 则属于数据驱动的开发模式。
一些详细的描述 : http://www.cocoscreator.com/docs/creator/getting-started/cocos2d-x-guide.html
其实 CCC 的游戏开发设计理念,可以说是 Component-Based Entity System,也就是基于组件的实体系统开发模式,以下简称 ECS。
ECS 设计模式
实体组件系统的设计模式,简单来说,就是把各种各样的功能点,设计封装成组件(Component)的形式,然后将这些组件,按需挂载到一个个类似于容器的节点上,从而形成一个个功能各异的实体(Entity) ,最后再通过系统(System),来组织管理这些实体,从而形成一个完整复杂的游戏。
假设我们有一个空白的逻辑节点类:
xx.Node = cc.class.extend({});
到这里为止,它什么功能都不具备,但是接下来我们就可以通过为它增加组件,来为这个节点增加各种功能,比如说 :
xx.TransformComponent = cc.Class.extend({ position: ..., anchor: ..., scale: ..., rotation: ..., skew: ..., });
这样一个节点提供了一系列与形变相关的属性,以及方法,通过添加这个组件,我们的节点,就具有了这些属性及方法。
var aNode = new xx.Node(); aNode.addComponent(xx.TransformComponent);
接着,假设我们想要在世界中渲染这个节点,让我们可以真切的看到这个对象,我们可以设计为其增肌一个渲染用的组件:
xx.RenderComponent = cc.Class.extend({ // ... }); aNode.addComponent(xx.RenderComponent);
这样一来,这个节点就可以被渲染在屏幕上了。
就像这样,我们可以为这个节点添加各种组件,如 :
aNode.addComponent(xx.KeyboardControllerComponent); aNode.addComponent(xx.SteeringComponent); aNode.addComponent(xx.ShootComponent); aNode.addComponent(xx.AIComponent);
这样,一个可以用键盘操纵,在屏幕上移动,射击,同时具有智能 AI 的节点就诞生了。这样的一个节点(附带了6个组件),我们可以把它再封装为一个实体,从而在游戏中直接使用,而不需要每次都对其进行组装。
就像这样,通过将这些可重用的组件,以不同的组合形式,挂载到不同的节点上,从而组成各种各样的实体。
关于 ECS 的用处,优点缺点等等,在网上有着大量的文章,大家可以自行搜索阅读,这里只是一个粗浅的解释。
当然,作为一个游戏引擎,基本上只需要提供实体和组件的支持,对于系统而言,基本上都是交给编程人员来完成的。
ECS 的理念十分的重要,因为在下面分析引擎作出修改的时候,都会紧紧的切合着这一理念的。
组合 vs 继承
ECS 的一个重要的理念就是,组合优于继承(Composition over inheritance),关于组合和继承的优缺点比较,在网上主要有下面的观点 :
继承
- 优点
- 容易进行新的实现,因为大多数可继承而来
- 易于修改或扩展那些被复用的实现
- 缺点
- 破坏封装性,父类的实现细节暴露给子类
- 父类实现更改时,子类必须修改
- 从父类继承的实现不将能在运行期间进行改变
组合
- 优点
- 逻辑节点通过内部的组件对象来访问
- 被包含组件的内部细节对外不可见
- 对装性好
- 相互依赖性较小,组件与逻辑节点之间的依赖关系较少
- 组件专注于一项任务
- 可以在运行期间动态地定义组件的组合方式
- 缺点
- 系统中对象过多
- 为了能将多个组件组合使用,必须仔细地对接口进行定义
可见,组合继承都有着各自的优缺点。
CCC 中,毫无疑问的是以组合的设计模式为主的,官方也提倡不用继承用组合,但是就我个人而言 ,不使用继承是不对的,如何平衡这两者在游戏系统中的关系,合理的使用,扬长避短,才是最重要的。
上面一节的例子中,如果完全抛开 ECS 的设计理念,而是使用继承来实现的话,可以想象到,这颗继承树会变得相当复杂,且难以扩展,而组件的理念则是能够将这颗很深的树分解,将原本复杂的,垂直的设计变为横向的设计。
Part.3 cocos2d-js-lite
为了给不具备组件机制(其实是有的,只是很简陋)的 cocos2d-js 引擎添加健全的组件系统,CCC 必然需要对原有的 cocos2d-js 引擎动一个手术,而这个手术的要求也是相当高的:
- 尽最大可能保留现有引擎的 API,减少学习成本
- 增加实体组件的机制
下面就来看一下,lite 中是怎么动这个手术的。
基础类的变化
首先我们关注的,是cocos2d-js 中,大部分基础类型的变化,可以看到,它们几乎在 lite 版中都被替换成了新的类 :
cocos2d-js | 继承关系 | cocos2d-js-lite | 继承关系 | 文件 |
---|---|---|---|---|
cc | 命名空间 | _ccsg | 命名空间 | |
cc.Class | 函数 | cc._Class | 函数 | _CCClass.js |
cc.Node | cc.Class.extend | _ccsg.Node | cc._Class.extend | CCSGNode.js |
cc.Scene | cc.Node.extend | _ccsg.Scene | _ccsg.Node.extend | CCSGScene.js |
cc.Layer | cc.Node.extend | cc.Layer | _ccsg.Node.extend | CCLayer.js |
cc.Sprite | cc.Node.extend | _ccsg.Sprite | _ccsg.Node.extend | CCSGSprite.js |
另外我们可以注意到,原有的大部分类 cc.Xxx 基本都被重命名为了 _ccsg.Xxx,这个原因在后面会给出。
这些被改名了的基础类,在 lite 版中其实仍然存在,只是它们有了新的名称和定义:
cocos2d-js | cocos2d-js-lite | 继承关系 |
---|---|---|
cc.Class | cc.Class, CCClass | function |
cc.Node | cc.Node, Node | extends cc._BaseNode |
cc.Scene | cc.Scene, Scene | extends cc._BaseNode |
cc.Layer | cc.Layer | N/A |
cc.Sprite | cc.Sprite, Sprite | extends: cc._RendererUnderSG |
N/A | cc.BaseNode, BaseNode | extends: cc.Object |
N/A | cc.Object, CCObject | function |
到这里,我们可以初步看到,这个手术的方案的核心 :
- 将原有类重命名成其它名字
- 将原有类本身的实现,改变掉
换句话说,在 CCC 中,即使我们创建一个 Sprite ,其实我们得到的,也不以前的那个 cc.Sprite 对象了。这个在后面也会有更详细的说明。
在开始说明前,我们可以先对于 lite 版中的继承树,有个大致的印象 :
- cc.Object - cc._BaseNode - cc.Node - cc.Scene - cc.Component - cc.SGComponent - cc.RendererInSG - cc.Mask - cc.TiledMap - cc.RendererUnderSG - cc.Sprite - cc.Label - CCParticleSystem
cc._BaseNode
让我们回想前面所提到的,实体组件的设计模式,其中有一点,就是将一些最基础的属性,抽取出,作为一个组件,而这里的,cc._BaseNode 其实就是这样的一个特殊的组件 — 它其实并不是以一个组件的形式存在 — 而是以一个基类的形式存在。
首先,我们应该都知道,过去的 cocos2d-js 中的 cc.Node,提供了很多的基础属性和方法 :
cc.Node = cc.Class.extend({ // ... _rotationX: 0, _rotationY: 0.0, _scaleX: 1.0, _scaleY: 1.0, _position: null, _skewX: 0.0, _skewY: 0.0, _visible: true, _anchorPoint: null, _contentSize: null, _realOpacity: 255, _realColor: null, // ... addChild: function() {}, removeChild: function() {}, convertToNodeSpace: function() {}, convertToWorldSpace: function() {}, // ... });
而这些基础的属性方法,在 lite 版中,都被转移到了 cc._BaseNode 中 :
var BaseNode = cc.Class({ extends: cc.Object, // ... properties: { _opacity: 255, _color: cc.Color.WHITE, _anchorPoint: cc.p(0.5, 0.5), _contentSize: cc.size(0, 0), _rotationX: 0, _rotationY: 0.0, _scaleX: 1.0, _scaleY: 1.0, _position: cc.p(0, 0), _skewX: 0, _skewY: 0, // ... } // ... addChild: function() {}, removeChild: function() {}, convertToNodeSpace: function() {},> convertToWorldSpace: function() {}, // ... });
除此以外,cc._BaseNode 还有一些额外的特点 (后面谈逻辑树,渲染树的时候会说到):
- 持有一个 _ccsg.Node 对象的引用
- 自身属性与 _ccsg.Node 对象的属性同步,如 position, color, rotation 等
- 属性发生变化时会发出事件通知 (为编辑器的集成服务)
setPosition: function (newPosOrxValue, yValue) { // ... if (CC_EDITOR) { this.emit(POSITION_CHANGED, oldPosition); } else { this.emit(POSITION_CHANGED); } }
- 提供事件的通知机制以及属性的序列化
- 不介入渲染工作,渲染工作由 _ccsg.Node 执行
cc.Object && cc.Class
在 cocos2d-js 中,cc.Class 是所有对象的一个基类 :
- cc.Class - cc.Node - cc.Action - cc.Event
cc.Class 是一切的基类,继承这个基类,生成新的子类的方法则是 cc.Class.extend :
cc.Node = cc.Class.extend({/*...*/}); cc.Action = cc.Class.extend({/*...*/}); cc.Event = cc.Class.extend({/*...*/});
现在,在 lite 版中,我们同样需要一个基类,那就是 cc.Object :
function CCObject () { this._name = ''; this._objFlags = 0; }
前面提到过 lite 引擎不仅服务于游戏,同样服务于编辑器,这一点在 cc.Object 的 _objFlags 的属性的可以看出 :
var Destroyed = 1 << 0; var RealDestroyed = 1 << 1; var ToDestroy = 1 << 2; var DontSave = 1 << 3; var EditorOnly = 1 << 4; var Dirty = 1 << 5; var DontDestroy = 1 << 6; var Destroying = 1 << 7; var Activating = 1 << 8; //var HideInGame = 1 << 9; //var HideInEditor = 1 << 10; var IsOnEnableCalled = 1 << 11; var IsEditorOnEnableCalled = 1 << 12; var IsPreloadCalled = 1 << 13; var IsOnLoadCalled = 1 << 14; var IsOnLoadStarted = 1 << 15; var IsStartCalled = 1 << 16; var IsRotationLocked = 1 << 17; var IsScaleLocked = 1 << 18; var IsAnchorLocked = 1 << 19; var IsSizeLocked = 1 << 20; var IsPositionLocked = 1 << 21;
这些属性,显然会被应用在 CCC 的编辑器中。
再回顾一下前面提到的继承树,cc._BaseNode 理所应当的继承于 cc.Object,这样它所有的基类也理所应当的具有了在编辑器中正常工作的能力。
有了基类,就差一个对应于原有的 cc.Class.extend 方法,来继承这个基类了,在 lite 版中,这个函数是是 cc.Class :
function CCClass (options) { } cc.Class = CCClass;
使用方法类似于之前的 cc.Class.extend,只是继承的对象,通过 extends 属性来指明 :
var Node = cc.Class({ extends: cc._BaseNode, // ... }); var Component = cc.Class({ extends: cc.Object, // ... });
继承的实现方式和之前并没有太大的区别,其实这里的 cc.Class 方法除了 extends 属性关键字,指明继承的父类之外,还有很多类似的有用的关键字,这在后面会陆续提到。
cc.Node
前面也已经提到,lite 版中的 cc.Node 已经完全不同于过往,那么它到底有什么不同呢。
首先,通过源码其实就能发现,现在的 cc.Node 已经没有了那些基础的属性,这些都已经被提取到了上面的 cc._BaseNode 中了。
现在的 cc.Node 中剩下的,只是下面的一些方法 :
getComponent: function (typeOrClassName) {}; getComponents: function (typeOrClassName) {}; addComponent: function (typeOrClassName) {}; removeComponent: function (component) {};
到这里,我们回想一下前面所谈到的 ECS 模式,应该就能察觉到,这里的 cc.Node 其实就是那个充当容器的 节点,因为我们可以通过上面的这几个方法,在它的身上挂载各种的组件,来让它变得无所不能。
再看一眼 cc.Node 的继承关系 :
cc.Node = cc.Class({ extends: cc._BaseNode // ... });
这里,从 ECS 的设计模式上来说,cc._BaseNode 也应该是以一个组件的形式被挂载到cc.Node 上的,但是这里使用的是一种继承的方式。
因为 cc.Node 作为在编辑器者游戏中所有节点的子类,它必然的会需要 cc._BaseNode 提供的这些属性和方法,这里不选择组合的方式,而是直接通过继承,来为 cc.Node 提供这些功能,是更合理的选择。
其实还有一个好处,就是前面提到过的保持 API 的不变,一旦通过组件的形式挂载,那么在调用这些方法的时候,必须要先获取到组件,才可以。比如说,对于一个 Node ,过去可以 :
aNode.setPosition(); aNode.getPosition();
如果通过组件的形式挂载之后,则会变成 :
aNode.getComponent("cc._BaseNode").setPosition(); aNode.setComponent("cc._BaseNode").getPosition();
不仅如此,由于这些属性方法都是非常基础的,在节点的其他组件上,也都会非常平凡的交互,如此一来,就会造成组件之间平凡的相互调用这种不好的设计模式 :
// 组合方式的 cc._BaseNode aNode.addComponent("cc._BaseNode"); aNode.addComponent("AComponent"); aNode.addComponent("BComponent"); // 在组件中,想要获取位置的话 // aComponent.js this.node.getComponent("cc._BaseNode").getPosition(); // bComponent.js this.node.getComponent("cc._BaseNode").getPosition();
比较之下,显然,通过继承的方式,来访问这些属性更为合理 :
// 继承方式的 cc._BaseNode aNode.addComponent("AComponent"); aNode.addComponent("BComponent"); // 在组件中,想要获取位置的话 // aComponent.js this.node.getPosition(); // bComponent.js this.node.getPosition();
最后总结一下,现在的 cc.Node 其实就是一个类似容器的节点,它的地位相当于 ECS 设计中,没有挂载任何组件的,一个节点的地位 (正真的实体,是挂在了组件的节点所形成的 Prefab,这在以后会提到),它提供了各种组件操作的方法。
cc.Component vs cc.SGComponent
既然有了 cc.Node 这个实体节点,那么接下来就可以往上面挂载组件了。
cc.Component 是所有组件的基类。它在继承树种的位置是 :
- cc.Object - cc._BaseNode - cc.Node - cc.Component
这里大家可以思考一下,为什么它不是继承自 cc._BaseNode 的?
cc.Component 的具体内容,我们这里暂时不关心。
直接继承于 cc.Compoennt 的组件,其实可以称之为逻辑组件,也就是说,它通常是不应该包含任何的渲染任务的。
比如说我们前面提到的 KeyboardControllerComponent ,就可以是一个单纯的脚本组件,它直接继承自 cc.Component :
var KeyboardControllerComponent = cc.Class({ // extends extends: cc.Component, });
其实在 CCC 中,80% 的情况下,我们缩写的组件,都只需要继承自 cc.Component,这就已经足够了。
其实 cc.Component 还有一个子类,也非常的重要 : cc.SGComponent。
cc.SGComponent 在继承数中的位置是 :
- cc.Object - cc._BaseNode - cc.Node - cc.Component - cc.SGComponent
看到名字,我们发现它和前面提到的 _ccsg.Xxx 很相似。看来前面留下的疑问,现在是解决的时候了 :
- 以前的那些渲染节点,cc.Node, cc.Sprite, cc.Label 之类的为什么没有删掉
- 为什么这些节点被重命名为了 _ccsg.Xxx
cc.Sprite
要解决上面的疑问,拿一个 cc.Sprite 作为剖析的对象是最好的解决方案。
首先,在 lite 版中的 cc.Sprite ,已经变成了一个 cc.Component 对象,它不再像是以前一样,也是一个独立的节点的概念。
过去我们可以:
var sprite = new cc.Sprite("..."); this.addChild(sprite);
而现在,遵守组件模式的使用原则,我们首先需要一个节点,才能再挂载这个组件 :
var node = new cc.Node(); node.addComponent(cc.Sprite); this.addChild(node);
显然,这是一个负责渲染的组件,那么它必然会继承自 cc.SGComponent(虽然不是直接的,后面会说到)。
官方文档中提到过,逻辑树会生成渲染树,开发者并不需要关心这些。
这句话的意义就是,你只需要向节点上挂载这些有渲染功能的组件就行了,至于它们怎么渲染的你无需关注。换句话说,也就是渲染的实际实现被隐藏了起来。
这里,是时候回想起,我们提到的那些被迫改名的渲染类们 : _ccsg.Sprite, _ccsg.Label, _ccsg.Xxxx,其实,很容易猜到,它们在 lite 版中的作用就是负责实际的幕后的渲染工作。
拿 cc.Sprite 来说,他继承自 cc.SGComponent ,在 cc.SGComponent 中有一个重要的属性 :
var SGComponent = cc.Class({ extends: cc.Component, properties: { _sgNode: { default: null, serializable: false } }, // ... });
这个属性保存的变量,其实就是实际负责渲染的对象。
此外,它还有一个类似于接口的方法,需要它的子类去实现 :
_createSgNode: null,
对于 cc.Sprite 组件而言,_sgNode 其实就是 _ccsg.Sprite (确切的说是 cc.Scale9Sprite)。注意这里的 cc.Scale9Sprite 其实就是以前的 cc.Scale9Sprite,它并不是组件,这里 lite 版中没有给它改名成 _ccsg.Scale9Sprite :
_createSgNode: function () { return new cc.Scale9Sprite(); },
到这里,我们就可以大概就可以想到,过去的渲染相关的类,被重命名为了 _ccsg.Xxxx的原因和意义了 :
- 下划线意味着,这些类都退到了后台,成为幕后工作者,官方不再希望用户直接去使用了
- sg 意味着 Scene Graph ,也就是说这些类都是负责着渲染树中的渲染工作的
- 官方说的 “不需要开发者关心渲染树”,就是因为这些类都已经隐藏,并一一对应的绑定在各个 cc.SGCompoennt 组件中了。
同时我们还可以总结出几点 :
- 继承一个 cc.SGComponent 的情况,适用于创建渲染相关的组件
- lite 版中,并没有将过去所有的 cc.Xxx 都改为 _ccsg.xxx,比如 cc.Scale9Sprite (可能是因为它没有对应的组件,所以不需要让位出来)
RendererInSG vs RendererUnderSG
上面提到 cc.Sprite 不是直接继承自 cc.SGComponent 的,而是 cc.RendererUnderSG。
在 cc.SGComponent 下,还有两个重要的子类 :
- cc.Object - cc._BaseNode - cc.Node - cc.Component - cc.SGComponent - cc.RendererInSG - cc.RendererUnderSG
官方在注释中的解释如下 :
cc.RendererInSG: Rendering component in scene graph.
cc.RendererUnderSG: Rendering component which will attach a leaf node to the cocos2d scene graph.
它们具体的区别,这里就不细谈了。
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); } });
换句话说,上面的逻辑树的示意图,其实,在它的幕后,一颗渲染树,已经自动的被生成了 :
逻辑树 渲染树 + Canvas + Canvas + cc.Scene + _ccsg.Scene + cc.Node (cocos) + _ccsg.Node - Sprite Component ==> - cc.Scale9Sprite + cc.Node (label) + _ccsg.Node - Label Component - _ccsg.Label
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继续完成这系列教程!)