从此之后,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);} });換句話說,上面的邏輯樹的示意圖,其實,在它的幕后,一顆渲染樹,已經自動的被生成了 :
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繼續完成這系列教程!)
總結
以上是生活随笔為你收集整理的从此之后,Creator 再无秘密的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 今天太阳真好
- 下一篇: 从人工智障到人工智能:人工智能在数字化转