Cocos Creator 源码解读:siblingIndex 与 zIndex
前言
本文基于 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 Creator 编辑器扩展:Quick Finder》
公众号
菜鸟小栈
😺 我是陈皮皮,一个还在不断学习的游戏开发者,一个热爱分享的 Cocos Star Writer。
🎨 这是我的个人公众号,专注但不仅限于游戏开发和前端技术分享。
💖 每一篇原创都非常用心,你的关注就是我原创的动力!
Input and output.
- cocos Creator js 房卡麻将/血战/H5四川麻将/源码架设/源码下载支持iOS/Android/H5..
- Yii2底层源码解读系列之一 入口文件index.php
- 微信小游戏:CocosCreator 《果蔬去哪了》 线上游戏源码资源分享
- DEDE源码分析与学习--index.php文件解读
- cocos Creator js 房卡麻将/血战/H5四川麻将源码下载搭建
- 菜鸟解读qt源码----qsqlindex.h
- CI框架源码解读(1)-入口文件index.php
- IndexedRDD 源码解读一
- DEDE源码分析与学习---index.php文件解读。
- jdk6标准类库源码解读 之 java.lang(一) StringBuffer/StringBuilder
- faster rcnn 源码解读
- spring cloud源码解读(2)-eureka server
- 第15课:Spark Streaming源码解读之No Receivers彻底思考
- GBDT源码解读及实现(一)
- [转载] Docker网络详解及pipework源码解读与实践
- PHP网页游戏学习之Xnova(ogame)源码解读(二)
- PHP网页游戏学习之Xnova(ogame)源码解读(五)
- PHP网页游戏学习之Xnova(ogame)源码解读(十二)
- cocos creator 学习(九)讲web项目部署到服务器
- HttpClient 4.3连接池参数配置及源码解读