前言

本文基于 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 属性,只有 getSiblingIndexsetSiblingIndex 这两个相关函数。

注:本文统一使用 siblingIndex 来代指 getSiblingIndexsetSiblingIndex 函数。

另外,getSiblingIndexsetSiblingIndex 函数是由 cc._BaseNode 实现的。

???? cc._BaseNode

大家对这个类可能会比较陌生,简单来说 cc._BaseNodecc.Node 的基类。

此类「定义了节点的基础属性和函数」,包括但不仅限于 setParentaddChildgetComponent 等常用函数...

???? 源码节选:

函数: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_ZINDEXcc.macro.MAX_ZINDEX 之间。

另外,zIndex 属性是在 cc.Node 内使用 Cocos 定制版 gettersetter 实现的。

???? 源码节选:

属性: 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 1var _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 2if (_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)

我们前面有提到:

  • zIndexgetter「返回了 _localZOrder 的高 16 位」。

  • zIndexsetter「设置了 _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 个子节点。

???? 子节点的排序信息:

zIndex 0

一般来说,由于节点的 _childArrivalOrder 是从 1 开始的,并且在计算时会先自增 1

所以子节点的 _localZOrder 的「低 16 位」总会比其 siblingIndex 大 2 个数。

>场景二 (Scene 2)

在 1 个节点下放置了 1 个子节点,并将子节点的 zIndex 设为 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

???? 各个子节点的排序信息:

zIndex 0 & siblingIndex 0~5

>场景四 (Scene 4)

在同 1 个节点下放置了 6 个子节点,将这 6 个子节点的 zIndex 设为 05

???? 各个子节点的排序信息:

zIndex 0~5

可以看到,zIndex 的值会直接体现在 _localZOrder 的「高 16 位」;每当 zIndex 增加 1_localZOrder 就会增加 65537

所以说 siblingIndex 怎么可能打得过 zIndex

>场景五 (Scene 5)

在同 1 个节点下放置了 6 个子节点,将这 6 个子节点的 zIndex 设为 05

???? 修改第 6 个子节点的 siblingIndex04,其排序信息:

zIndex 5 & siblingIndex 0~4

可以看到,此时无论我们怎么修改第 6 个子节点的 siblingIndex,它都会自动变回 5(也就是同级节点中的最大值)。

因为这个子节点的 zIndex 在其同级节点之中有着绝对的优势。

~不太对劲 (Something wrong)

???? 这里有一个看起来不太对劲的现象!

比如,当我们把 siblingIndex5 修改为 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 属性。

Usage in engine

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

Profiler Node

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

建议 (Suggestion)

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

我的建议是:

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

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


公众号

菜鸟小栈

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

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

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

Input and output.

听皮皮一句劝zIndex 的水太深,你把握不住!相关推荐

  1. 《面试技巧》孩子,听叔一句劝,面试水太深,你把握不住。

    人世仙家本自殊,何须相见向中途.惊鸿瞥过游龙去,漫恼陈王一事无. 嗨,大家好,我是洛神,性别男.一个来自快乐星球的程序员. 欢迎大家专注我的公众号[程序员洛神],绝对让你有意外收获哟 前言 首先要先向 ...

  2. 【听哥一句劝,C++水很深,你把握不住啊!】C++提高班之 符与*符

    C++提高班之 &符与*符 像&和*这样的符号,既可以作为表达式中的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义: int i = 27;int &r = i ...

  3. 听我一句劝,单片机不要去学STM32真的

    听我一句劝,单片机不要去学STM32真的 ///插播一条:我自己在今年年初录制了一套还比较系统的入门单片机教程,想要的同学找我拿就行了免費的,私信我就可以哦~点我头像黑色字体加我地球呺也能领取哦.最近 ...

  4. 华为mate30epro支不支持鸿蒙,听我一句劝,华为手机可以支持,但这4款不要买

    原标题:听我一句劝,华为手机可以支持,但这4款不要买 如今的华为举步维艰,正在努力推进新机的发布,不断的打磨鸿蒙操作系统,华为的这份坚定意志可歌可泣,但有一说一,华为手机可以支持,只不过这四款希望大家 ...

  5. 听师兄一句劝,早点去追学姐学妹!

    听师兄一句劝,早点去追学姐学妹,为啥呢? 看完这篇文章你就知道了. 假设有三男(分别是 A ,B ,C )和三女(分别是 x,y ,z ),他(她)们对异性的心仪程度如对话框所示. 比如对于男 A 来 ...

  6. 听哥一句劝,按这套嵌入式的课程内容和课程体系去学习

    听哥一句劝,按这套嵌入式的课程内容和课程体系去学习 一.嵌入式的难点 嵌入式开发比互联网软开(比如Java后端)还是要难一些的,比如Java开发,基本也都是用户态的东西,但嵌入式很多东西都深入到了内核 ...

  7. 响铃:二手车水太深,汽车之家“诚信联盟”能成“抽水机”吗?

    文|曾响铃 来源|科技向令说(xiangling0815) 瓜子.优信.人人车等几个互联网二手车品牌铺天盖地的宣传让二手车这个行当一时间又成了舆论焦点. 艾瑞数据显示,预计到2020年,中国二手车交易 ...

  8. 域前置,水太深,偷学六娃来隐身

    前言 又是平静的一天,吉良吉影只想过平静的生活. 哦,对不起拿错剧本了. 重保期间,RT 使用了多种方法来攻击资产, 其中不乏低级的方法. 1. 给客服 MM 传恶意文件,威胁不运行就投诉的,伪造&l ...

  9. 听叔一句劝,消息队列的水太深,你把握不住!

    很多人在做架构设计时往往会"过度设计",简单问题复杂化,上来就引一堆中间件,我想大概原因主要有下面两点: 为了秀(学)技术而架构 我们常说技术是为业务服务的,不能为了技术而技术,为 ...

最新文章

  1. ARCore中根据屏幕坐标计算射线的算法
  2. C#的winform拼数字游戏
  3. C++的clone函数什么时候需要重载
  4. 【Linux安全】安全口令策略设置
  5. 你知道用git打补丁吗?
  6. linux下查看cmake的版本
  7. POJ 1094 拓扑排序
  8. 登峰连接程式改坐标软件_如何用SOLIDWORKS方程式驱动圆柱波浪线?
  9. HDU2075 A|B?【水题】
  10. java与python结合使用_Java与Python使用grpc跨平台调用
  11. kettle基础入门(一)kettle下载、安装
  12. 使用echarts-gl 绘制3D地球配置详解
  13. lwip 动态修改IP
  14. Mac最新版书籍分享
  15. 聊天室登录php,聊天室技术(二)-- 登录_PHP
  16. Java精品项目源码第53期流浪动物管理系统
  17. Python、C、Java 和 C++ 四足鼎立,其他已无胜算? | TIOBE 10 月编程语言排行榜
  18. matlab幂函数e,MATLAB e的幂函数拟合
  19. 企微社群引流方式大全
  20. 2019秋招阅文数据分析:sql查询连续天数

热门文章

  1. 15个Vue自定义指令,让你的项目开发爽到爆
  2. Jetpack Compose 架构比较:MVP MVVM MVI
  3. 游泳的鱼 AC于2018.7.21
  4. vue使用postcss-pxtorem px转rem
  5. bfgs算法 matlab,BFGS算法的最优化问题及在MATLAB中的实现
  6. 【博客话题】谈谈我工作的 入门恩师---“小武”
  7. Sharpen the Saw
  8. 布局(1) WP风格滑动布局模仿,类似360手机安全卫士
  9. Cadence16.6安装流程
  10. 关于文件预览的功能实现