Cocos Creator 之旅 by SuperSuRaccoon
2016.10.25 by cocos
教程

原文作者: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 能够做到什么程度,当然测试下来的效果还是很让人满意的 :

fog_of_war
grid3d
line_of_sight
shader

下面是本人这一个月不到的使用感受 :

  • 现在的 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.Nodecc.Class.extend_ccsg.Nodecc._Class.extendCCSGNode.js
cc.Scenecc.Node.extend_ccsg.Scene_ccsg.Node.extendCCSGScene.js
cc.Layercc.Node.extendcc.Layer_ccsg.Node.extendCCLayer.js
cc.Spritecc.Node.extend_ccsg.Sprite_ccsg.Node.extendCCSGSprite.js

另外我们可以注意到,原有的大部分类 cc.Xxx 基本都被重命名为了 _ccsg.Xxx,这个原因在后面会给出。

这些被改名了的基础类,在 lite 版中其实仍然存在,只是它们有了新的名称和定义:

cocos2d-jscocos2d-js-lite继承关系
cc.Classcc.Class, CCClassfunction
cc.Nodecc.Node, Nodeextends cc._BaseNode
cc.Scenecc.Scene, Sceneextends cc._BaseNode
cc.Layercc.LayerN/A
cc.Spritecc.Sprite, Spriteextends: cc._RendererUnderSG
N/Acc.BaseNode, BaseNodeextends: cc.Object
N/Acc.Object, CCObjectfunction

到这里,我们可以初步看到,这个手术的方案的核心 :

  • 将原有类重命名成其它名字
  • 将原有类本身的实现,改变掉

换句话说,在 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);
cocos2d-x DrawNode

这要一来,就可以正常的在游戏中使用 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
);

同样的结果,不一样的实现 :

d7605bc3-16e1-4c4d-bf41-8339c47093dd

当然,这里的实现才是正统的,符合 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 的工作方式。

supersu-creator-3

看到这里,其实我们就可以了解到,过去的 cocos 里的那些模块,并没有被废弃,它们还是在那里,只是官方并没有将其集成到 CCC 中,或是因为时间不够,或是因为认为没有必要。

但是不管怎样,个人的理解是,只要 CCC 提拱了一个完善的框架,那么即使部分模块的缺失,也是完全可以通过自己来解决弥补的,毕竟这就是开源的好处,放着好好的源码不看,这不是暴残天物吗...

(未完待续,作者还计划有生命周期、常见设计模式、最佳实践、编辑器扩展、Gizmo、Native等多个章节。期待SuperSuRaccoon继续完成这系列教程!)