您的位置:首页 > 移动开发 > Cocos引擎

Cocos Creator 源码解读:siblingIndex 与 zIndex

2021-08-04 12:00 1251 查看

前言

本文基于 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
属性时也并非简单的赋值,又是进行了一顿位操作:

这里我们以二进制数的视角来分解该函数内的位操作。

  1. 通过
    & 0x0000ffff
    取出原
    _localZOrder
    的「低 16 位」;
  2. 将目标值
    value
    「左移 16 位」;
  3. 将左移后的
    value
    作为「高 16 位」与原
    _localZOrder
    的「低 16 位」合并;
  4. 最后得到一个「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
属性」。

🥱 该函数中同样也使用了位操作:

同上,以二进制数的视角来进行分解这里的位操作。

  1. 将父节点的
    _childArrivalOrder
    (前置)自增
    1
    ,并赋予
    arrivalOrder
    (如无父节点则为
    0
    );
  2. 通过
    & 0xffff0000
    取出当前节点的
    _localZOrder
    的「高 16 位」;
  3. arrivalOrder
    作为「低 16 位」与当前节点的
    _localZOrder
    的「高 16 位」合并;
  4. 最后得到一个新的「32 位的二进制数」并赋予当前节点的
    _localZOrder
    属性。

🤔 看到这里你是不是已经开始迷惑了?

别担心,答案即将揭晓!

下半部分 (Part 2)

sortAllChildren
函数的下半部分就比较好理解了。

基本就是通过「插入排序(Insertion Sort)」来「排序当前节点的

_children
属性(子节点数组)」。

其中主要根据子节点的

_localZOrder
属性的值来进行排序,
_localZOrder
属性值小的子节点排前面,反之排后面。

排序的关键 (Key of sorting)

🤔 分析完源码后发现,节点的排序并没有想象中那么简单。

我们可以先得出几个结论:

  1. siblingIndex 是节点在父节点的
    children
    属性中的下标;
  2. zIndex
    是一个独立的属性,和 siblingIndex 没有直接联系;
  3. siblingIndex 和
    zIndex
    的改变都会触发排序;
  4. siblingIndex 和
    zIndex
    共同组成了节点的
    _localZOrder
  5. zIndex
    的权重比 siblingIndex 大;
  6. 节点的
    _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)

分析完源码后,我们来总结一下。

在代码中修改节点顺序的方法主要有两种:

  1. 修改节点的
    zIndex
    属性
  2. 通过
    setSiblingIndex
    函数设置

无论使用以上哪种方法,最终都会「通过

zIndex
和 siblingIndex 的组合作为依据来进行排序」。

在多数情况下,「修改节点的

zIndex
属性会使其
setSiblingIndex
函数失效」。

这无形中增加了编码时的心智负担,也增加了问题排查的难度。

引擎内的用法 (Usage in engine)

出于好奇,我在引擎源码中搜了搜,想看看引擎内部有没有使用到

zIndex
属性。

结果是:只有几处与「调试」相关的地方使用到了节点的

zIndex
属性。

例如:预览模式下,左下角的 Profiler 节点。

以及碰撞组件的调试框等等,这里就不在赘述了。

建议 (Suggestion)

所以,为了避免一些不必要的 BUG 和逻辑冲突。

我的建议是:

「少用甚至不用 zIndex,而优先使用 siblingIndex 相关函数。」

🥴 听皮皮一句劝,zIndex 的水太深,你把握不住!

传送门

微信推文版本

个人博客:菜鸟小栈

开源主页:陈皮皮

Eazax Cocos 游戏开发工具包

更多分享

《Cocos Creator 性能优化:DrawCall》

《在 Cocos Creator 里画个炫酷的雷达图》

《用 Shader 写个完美的波浪》

《在 Cocos Creator 中优雅且高效地管理弹窗》

《JavaScript 内存详解 & 分析指南》

《Cocos Creator 编辑器扩展:Quick Finder》

《JavaScript 原始值与包装对象》

《Cocos Creator 源码解读:引擎启动与主循环》

公众号

菜鸟小栈

😺 我是陈皮皮,一个还在不断学习的游戏开发者,一个热爱分享的 Cocos Star Writer。

🎨 这是我的个人公众号,专注但不仅限于游戏开发和前端技术分享。

💖 每一篇原创都非常用心,你的关注就是我原创的动力!

Input and output.

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: