前言
本文基於 Cocos Creator 2.4.5 撰寫。
🎉 普天同慶
來了來了,《源碼解讀》系列文章終於又來了!
👾 溫馨提醒
本文包含大段引擎源碼,使用大屏設備閱讀體驗更佳!
Hi There!
節點(cc.Node)作爲 Cocos Creator 引擎中最基本的單位,所有組件都需要依附在節點上。
同時節點也是我們日常開發中接觸最頻繁的東西。
我們經常會需要「改變節點的排序」來完成一些效果(如圖像的遮擋)。
A Question?
😕 你有沒有想過:
節點的排序是如何實現的?
Oops!
🤯 我在分析了源碼後發現:
節點的排序並沒有想象中那麼簡單!
😹 渣皮語錄
聽皮皮一句勸,zIndex 的水太深,你把握不住!
正文
節點順序 (Node Order)
🤔 如何修改節點的順序?
首先,在 Cocos Creator 編輯器中的「層級管理器」中,我們可以隨意拖動節點來改變節點的順序。
🤨 但是,在代碼中我們要怎麼做呢?
我最先想到的是節點的 setSiblingIndex
函數,然後是節點的 zIndex
屬性。
我猜大多數人都不清楚這兩個方案有什麼區別。
那麼接下來就讓我們深入源碼,一探究竟!
siblingIndex
「siblingIndex」即「同級索引」,意爲「同一父節點下的兄弟節點間的位置」。
siblingIndex 越小的節點排越前,索引最小值爲 0
,也就是第一個節點的索引值。
需要注意的是,實際上節點並沒有 siblingIndex 屬性,只有 getSiblingIndex
和 setSiblingIndex
這兩個相關函數。
注:本文統一使用 siblingIndex 來代指 getSiblingIndex
和 setSiblingIndex
函數。
另外,getSiblingIndex
和 setSiblingIndex
函數是由 cc._BaseNode
實現的。
💡 cc._BaseNode
大家對這個類可能會比較陌生,簡單來說
cc._BaseNode
是cc.Node
的基類。此類「定義了節點的基礎屬性和函數」,包括但不僅限於
setParent
、addChild
和getComponent
等常用函數...
📝 源碼節選:
函數:cc._BaseNode.prototype.getSiblingIndex
getSiblingIndex() {
if (this._parent) {
return this._parent._children.indexOf(this);
} else {
return 0;
}
},
函數:cc._BaseNode.prototype.setSiblingIndex
setSiblingIndex(index) {
if (!this._parent) {
return;
}
if (this._parent._objFlags & Deactivating) {
return;
}
var siblings = this._parent._children;
index = index !== -1 ? index : siblings.length - 1;
var oldIndex = siblings.indexOf(this);
if (index !== oldIndex) {
siblings.splice(oldIndex, 1);
if (index < siblings.length) {
siblings.splice(index, 0, this);
} else {
siblings.push(this);
}
this._onSiblingIndexChanged && this._onSiblingIndexChanged(index);
}
},
[源碼] base-node.js#L514: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/utils/base-node.js#L514
🕵️ 做了什麼?
扒拉源碼後發現,siblingIndex 的本質其實很簡單。
那就是「當前節點在父節點的 _children
屬性中的下標(位置)」。
getSiblingIndex
函數返回的是「當前節點在父節點的 _children
屬性中的下標(位置)」。
setSiblingIndex
函數則是設置「當前節點在父節點的 _children
屬性中的下標(位置)」。
💡
cc._BaseNode.prototype._children
節點的
_children
屬性其實就是節點的children
屬性。而
children
屬性是一個getter
,返回的是自身的_children
屬性。另外
children
屬性沒有實現setter
,所以你直接給children
屬性賦值是無效的。
zIndex
「zIndex」是「用來對節點進行排序的關鍵屬性」,它決定了一個節點在兄弟節點之間的位置。
zIndex
的值介於 cc.macro.MIN_ZINDEX
和 cc.macro.MAX_ZINDEX
之間。
另外,zIndex
屬性是在 cc.Node
內使用 Cocos 定製版 getter
和 setter
實現的。
📝 源碼節選:
屬性: cc.Node.prototype.zIndex
// 爲了減少篇幅,已省略部分不相關代碼
zIndex: {
get() {
return this._localZOrder >> 16;
},
set(value) {
if (value > macro.MAX_ZINDEX) {
value = macro.MAX_ZINDEX;
} else if (value < macro.MIN_ZINDEX) {
value = macro.MIN_ZINDEX;
}
if (this.zIndex !== value) {
this._localZOrder = (this._localZOrder & 0x0000ffff) | (value << 16);
this.emit(EventType.SIBLING_ORDER_CHANGED);
this._onSiblingIndexChanged();
}
}
},
[源碼] CCNode.js#L1549: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/CCNode.js#L1549
🕵️ 做了什麼?
扒拉源碼後發現,zIndex
的本質其實也很簡單。
那就是「返回或設置節點的 _localZOrder
屬性」。
🧐 沒那麼簡單!
有趣的是,在 getter
中並沒有直接返回 _localZOrder
屬性,而是返回了 _localZOrder
屬性右移(>>
)16 位後的數值。
在 setter
中設置 _localZOrder
屬性時也並非簡單的賦值,又是進行了一頓位操作:
這裏我們以二進制數的視角來分解該函數內的位操作。
-
通過 & 0x0000ffff
取出原_localZOrder
的「低 16 位」; -
將目標值 value
「左移 16 位」; -
將左移後的 value
作爲「高 16 位」與原_localZOrder
的「低 16 位」合併; -
最後得到一個「32 位的二進制數」並賦予 _localZOrder
。
😲 嗯?
慢着!
_localZOrder
又是幹啥用的?咋這麼繞!別急,答案在後面~
排序 (Sorting)
細心的朋友應該發現了,siblingIndex 和 zIndex
的源碼中都沒有包含實際的排序邏輯。
但是它們都有一個共同點:「最後都調用了自身的 _onSiblingIndexChanged
函數」。
_onSiblingIndexChanged
📝 源碼節選:
函數:cc.Node.prototype._onSiblingIndexChanged
_onSiblingIndexChanged() {
if (this._parent) {
this._parent._delaySort();
}
},
🕵️ 做了什麼?
而 _onSiblingIndexChanged
函數內則是調用了「父節點」的 _delaySort
函數。
_delaySort
📝 源碼節選:
函數:cc.Node.prototype._delaySort
_delaySort() {
if (!this._reorderChildDirty) {
this._reorderChildDirty = true;
cc.director.__fastOn(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
}
},
🕵️ 做了什麼?
一頓操作順藤摸瓜後發現,真正進行排序的地方是「父節點」的 sortAllChildren
函數。
💡 盲生,你發現了華點!
值得注意的是,
_delaySort
函數中的sortAllChildren
函數調用不是立即觸發的,而是會在下一次update
(生命週期)後觸發。延遲觸發的目的應該是爲了避免在同一幀內的重複調用,從而減少不必要的性能損耗。
sortAllChildren
📝 源碼節選:
函數:cc.Node.prototype.sortAllChildren
// 爲了減少篇幅,已省略部分不相關代碼
sortAllChildren() {
if (this._reorderChildDirty) {
this._reorderChildDirty = false;
// Part 1
var _children = this._children, child;
this._childArrivalOrder = 1;
for (let i = 0, len = _children.length; i < len; i++) {
child = _children[i];
child._updateOrderOfArrival();
}
eventManager._setDirtyForNode(this);
// Part 2
if (_children.length > 1) {
let child, child2;
for (let i = 1, count = _children.length; i < count; i++) {
child = _children[i];
let j = i;
for (;
j > 0 && (child2 = _children[j - 1])._localZOrder > child._localZOrder;
j--
) {
_children[j] = child2;
}
_children[j] = child;
}
this.emit(EventType.CHILD_REORDER, this);
}
cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
}
},
[源碼] CCNode.js#L3680: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/CCNode.js#L3680
>上半部分 (Part 1)
隨着一步步深入,我們終於來到了關鍵部分。
現在讓我們琢磨琢磨這個 sortAllChildren
函數。
進入該函數的前半段,映入眼簾的是一行賦值語句,將 _childArrivalOrder
屬性設(重置)爲 1
;
緊跟其後的是一個 for 循環,遍歷了當前節點的所有「子節點」,並一一執行「子節點」的 _updateOrderOfArrival
函數。
🤨 嗯?這個 _updateOrderOfArrival
函數又是何方神聖?
~_updateOrderOfArrival
📝 源碼節選:
函數:cc.Node.prototype._updateOrderOfArrival
_updateOrderOfArrival() {
var arrivalOrder = this._parent ? ++this._parent._childArrivalOrder : 0;
this._localZOrder = (this._localZOrder & 0xffff0000) | arrivalOrder;
this.emit(EventType.SIBLING_ORDER_CHANGED);
},
🕵️ 做了什麼?
顯而易見的是,_updateOrderOfArrival
函數的作用就是「更新節點的 _localZOrder
屬性」。
🥱 該函數中同樣也使用了位操作:
同上,以二進制數的視角來進行分解這裏的位操作。
-
將父節點的 _childArrivalOrder
(前置)自增1
,並賦予arrivalOrder
(如無父節點則爲0
); -
通過 & 0xffff0000
取出當前節點的_localZOrder
的「高 16 位」; -
將 arrivalOrder
作爲「低 16 位」與當前節點的_localZOrder
的「高 16 位」合併; -
最後得到一個新的「32 位的二進制數」並賦予當前節點的 _localZOrder
屬性。
🤔 看到這裏你是不是已經開始迷惑了?
別擔心,答案即將揭曉!
>下半部分 (Part 2)
而 sortAllChildren
函數的下半部分就比較好理解了。
基本就是通過「插入排序(Insertion Sort)」來「排序當前節點的 _children
屬性(子節點數組)」。
其中主要根據子節點的 _localZOrder
屬性的值來進行排序,_localZOrder
屬性值小的子節點排前面,反之排後面。
排序的關鍵 (Key of sorting)
🤔 分析完源碼後發現,節點的排序並沒有想象中那麼簡單。
我們可以先得出幾個結論:
-
siblingIndex 是節點在父節點的 children
屬性中的下標; -
zIndex
是一個獨立的屬性,和 siblingIndex 沒有直接聯繫; -
siblingIndex 和 zIndex
的改變都會觸發排序; -
siblingIndex 和 zIndex
共同組成了節點的_localZOrder
; -
zIndex
的權重比 siblingIndex 大; -
節點的 _localZOrder
直接決定了節點的最終順序。
siblingIndex 如何影響排序 (How siblingIndex affects sorting)
我們前面有提到:
-
getSiblingIndex
函數「返回了當前節點在父節點的_children
屬性中的下標(位置)」。 -
setSiblingIndex
函數「設置了當前節點在父節點的_children
屬性中的下標(位置),並通知父節點進行排序」。
隨後在父節點的 sortAllChildren
函數中的上半部分,會以這個下標作爲節點 _localZOrder
的低 16 位。
🧐 所以我們可以這樣理解:
siblingIndex 是元素下標,在排序過程中,其決定了 _localZOrder
的「低 16 位」。
zIndex 如何影響排序 (How zIndex affects sorting)
我們前面有提到:
-
zIndex
的getter
「返回了_localZOrder
的高 16 位」。 -
zIndex
的setter
「設置了_localZOrder
的高 16 位,並通知父節點進行排序」。
🧐 所以我們可以這樣理解:
zIndex
實際上只是一個軀殼,其本質是 _localZOrder
的「高 16 位」。
_localZOrder 如何決定順序 (How _localZOrder works)
父節點的 sortAllChildren
函數中根據子節點的 _localZOrder
大小來進行最終排序。
我們可以將 _localZOrder
看做一個「32 位二進制數」,其由 siblingIndex 和 zIndex
共同組成。
但是,爲什麼說「zIndex
的權重比 siblingIndex 大」呢?
因爲 zIndex
決定了 _localZOrder
的「高 16 位」,而 siblingIndex 決定了 _localZOrder
的「低 16 位」。
所以,只有在 zIndex
相等的情況下,siblingIndex 的大小纔有決定性意義。
而在 zIndex
不相等的情況下,siblingIndex 的大小就無所謂了。
🌰 舉個栗子
這裏有兩個 32 位二進制數(僞代碼):
A: 0000 0000 0000 0001 xxxx xxxx xxxx xxxx
B: 0000 0000 0000 0010 xxxx xxxx xxxx xxxx
由於 B 的「高 16 位」(
0000 0000 0000 0010
)比 A 的「高 16 位」(0000 0000 0000 0001
)大,所以無論他們的「低 16 位」中的x
是什麼,B 都會永遠大於 A。
實驗一下 (Experiment)
我們可以寫個小組件來測試下 siblingIndex 和 zIndex
對於 _localZOrder
的影響。
📝 一頓打碼:
const { ccclass, property, executeInEditMode } = cc._decorator;
@ccclass
@executeInEditMode
export default class Test_NodeOrder extends cc.Component {
@property({ displayName: 'siblingIndex' })
get siblingIndex() {
return this.node.getSiblingIndex();
}
set siblingIndex(value) {
this.node.setSiblingIndex(value);
}
@property({ displayName: 'zIndex' })
get zIndex() {
return this.node.zIndex;
}
set zIndex(value) {
this.node.zIndex = value;
}
@property({ displayName: '_localZOrder' })
get localZOrder() {
return this.node._localZOrder;
}
@property({ displayName: '_localZOrder (二進制)' })
get localZOrderBinary() {
return this.node._localZOrder.toString(2).padStart(32, 0);
}
}
>場景一 (Scene 1)
在 1 個節點下放置了 1 個子節點。
🖼 子節點的排序信息:
一般來說,由於節點的 _childArrivalOrder
是從 1
開始的,並且在計算時會先自增 1
。
所以子節點的 _localZOrder
的「低 16 位」總會比其 siblingIndex 大 2 個數。
>場景二 (Scene 2)
在 1 個節點下放置了 1 個子節點,並將子節點的 zIndex
設爲 1
。
🖼 子節點的排序信息:
可以看到,僅僅將節點的 zIndex
屬性設爲 1
,其 _localZOrder
就高達 65538
。
🔠 大概的計算過程如下(極爲抽象的僞代碼):
1. zIndex = 1 = 0b0000000000000001
2. siblingIndex = 0
3. arrivalOrder = 1 + (siblingIndex + 1)
4. arrivalOrder = 0b0000000000000010
5. _localZOrder = (zIndex << 16) | arrivalOrder
6. _localZOrder = 0b00000000000000010000000000000000 | 0b0000000000000010
7. _localZOrder = 0b00000000000000010000000000000010 = 65538
📝 繼續簡化後的僞代碼:
_localZOrder = (zIndex << 16) | (siblingIndex + 2)
💡 By the way
當一個節點沒有父節點時,它的
arrivalOrder
永遠是0
。其實此時它是啥已經不重要了,畢竟沒有父節點的節點本來就不可能會被排序。
>場景三 (Scene 3)
在同 1 個節點下放置了 6 個子節點,將所有子節點的 zIndex
都設爲 0
。
🎥 各個子節點的排序信息:
>場景四 (Scene 4)
在同 1 個節點下放置了 6 個子節點,將這 6 個子節點的 zIndex
設爲 0
到 5
。
🎥 各個子節點的排序信息:
可以看到,zIndex
的值會直接體現在 _localZOrder
的「高 16 位」;每當 zIndex
增加 1
,_localZOrder
就會增加 65537
。
所以說 siblingIndex 怎麼可能打得過 zIndex
!
>場景五 (Scene 5)
在同 1 個節點下放置了 6 個子節點,將這 6 個子節點的 zIndex
設爲 0
到 5
。
🎥 修改第 6 個子節點的 siblingIndex
從 0
到 4
,其排序信息:
可以看到,此時無論我們怎麼修改第 6 個子節點的 siblingIndex
,它都會自動變回 5
(也就是同級節點中的最大值)。
因爲這個子節點的 zIndex
在其同級節點之中有着絕對的優勢。
~不太對勁 (Something wrong)
😲 這裏有一個看起來不太對勁的現象!
比如,當我們把 siblingIndex
從 5
修改爲 0
時,_localZOrder
也相應從 327687
變成 327682
;但是當 siblingIndex
自動變回 5
時,_localZOrder
也還是 327682
,並沒有變回 327687
。
🤔 爲什麼會這樣?
原因其實很簡單:
當我們修改節點的 siblingIndex 時會觸發排序,排序過程中會「根據節點當前時刻的 siblingIndex 和 zIndex
生成新的 _localZOrder
」;
最後在父節點的 sortAllChildren
函數中會根據子節點的 _localZOrder
來對 _children
數組進行排序,此時「子節點的 siblingIndex 也會被動更新」,「但是 _localZOrder
卻沒有重新生成」。
但是,由於 zIndex
存在「絕對優勢」,這種“奇怪的現象”其實並不會影響到節點的正常排序~
總結 (Summary)
分析完源碼後,我們來總結一下。
在代碼中修改節點順序的方法主要有兩種:
-
修改節點的 zIndex
屬性 -
通過 setSiblingIndex
函數設置
無論使用以上哪種方法,最終都會「通過 zIndex
和 siblingIndex 的組合作爲依據來進行排序」。
在多數情況下,「修改節點的 zIndex
屬性會使其 setSiblingIndex
函數失效」。
這無形中增加了編碼時的心智負擔,也增加了問題排查的難度。
引擎內的用法 (Usage in engine)
出於好奇,我在引擎源碼中搜了搜,想看看引擎內部有沒有使用到 zIndex
屬性。
結果是:只有幾處與「調試」相關的地方使用到了節點的 zIndex
屬性。
例如:預覽模式下,左下角的 Profiler 節點。
以及碰撞組件的調試框等等,這裏就不在贅述了。
建議 (Suggestion)
所以,爲了避免一些不必要的 BUG 和邏輯衝突。
我的建議是:
「少用甚至不用 zIndex,而優先使用 siblingIndex 相關函數。」
🥴 聽皮皮一句勸,zIndex 的水太深,你把握不住!
公衆號
菜鳥小棧
😺 我是陳皮皮,一個還在不斷學習的遊戲開發者,一個熱愛分享的 Cocos Star Writer。
🎨 這是我的個人公衆號,專注但不僅限於遊戲開發和前端技術分享。
💖 每一篇原創都非常用心,你的關注就是我原創的動力!
Input and output.
本文分享自微信公衆號 - Creator星球遊戲開發社區(creator-star)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。