从此之后,Creator 再无秘密

一位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);
    }
});

换句话说,上面的逻辑树的示意图,其实,在它的幕后,一颗渲染树,已经自动的被生成了 :

7d42858809b23153a18e3de74c415c94.jpeg

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 了 。 

da18434834b6ed836a8e262572259386.jpeg

 但是对于这种方式,官方的说明是,这是一种 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
);

同样的结果,不一样的实现 :8498269c43e2adadde2af35f996c9dfa.jpeg 

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

4b2196ea39cfc004c40242381d6f9e23.jpeg

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

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

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

7acc1074d3bcae92f6a63e8cc74bb08c.jpeg

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值