关于cocos:Cocos-Creator-源码解读siblingIndex-与-zIndex

92次阅读

共计 9455 个字符,预计需要花费 24 分钟才能阅读完成。

前言

本文基于 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 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)

咱们后面有提到:

  • 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 个子节点。

🖼 子节点的排序信息:

一般来说,因为节点的 _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 设为 05

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

能够看到,zIndex 的值会间接体现在 _localZOrder 的「高 16 位」;每当 zIndex 减少 1_localZOrder 就会减少 65537

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

场景五 (Scene 5)

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

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

能够看到,此时无论咱们怎么批改第 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 属性。

例如:预览模式下,左下角的 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.

正文完
 0