preface
This article is based on Cocos Creator 2.4.5.
๐ Celebrate
Come to come, “source code interpretation” series of articles finally come again!
๐พ Warm reminder
This article contains a large section of engine source code, the use of large screen device reading experience is better!
Hi There!
As the basic unit of the Cocos Creator engine, all components need to be attached to the Node (cc.node).
Nodes are also the things we touch most frequently in daily development.
We often need to “change the order of nodes” to achieve some effect (such as image occlusion).
A Question?
๐ Have you ever thought about:
How is node ordering implemented?
Oops!
๐คฏ I found after analyzing the source code:
Sorting nodes is not as easy as you might think!
๐น slag skin quotations
Listen to pipi advice, zIndex water is too deep, you can not hold!
The body of the
Node Order
๐ค How do I change the sequence of nodes?
First, in the “Hierarchy Manager” in the Cocos Creator editor, you can change the order of the nodes by dragging them around.
๐คจ But what do we do in code?
The first thing that comes to mind is the setSiblingIndex function of the node, followed by the zIndex property of the node.
My guess is that most people don’t know the difference between the two plans.
So let’s go deep into the source code to find out!
siblingIndex
“SiblingIndex” is “siblingIndex”, meaning “the position between siblings under the same parent node”.
The smaller the siblingIndex is, the minimum index value is 0, that is, the index value of the first node.
Note that the node does not actually have siblingIndex, only getSiblingIndex and setSiblingIndex.
Note: siblingIndex is used in this papergetSiblingIndex
ๅ setSiblingIndex
Function.
In addition, the getSiblingIndex and setSiblingIndex functions are implemented by cc._basenode.
๐ก cc. _BaseNode
Cc._BaseNode is the base class of cc.Node.
This class “defines the basic properties and functions of a node”, including but not limited to common functions such as setParent, addChild, and getComponent…
๐ source code:
Function:cc._BaseNode.prototype.getSiblingIndex
getSiblingIndex() {
if (this._parent) {
return this._parent._children.indexOf(this);
} else {
return 0; }},Copy the code
Function: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); }},Copy the code
[source] base-node. Js# L514:Github.com/cocos-creat…
What did ๐ต๏ธ do?
Sifting through the source code found that the essence of siblingIndex is very simple.
That is the subscript of the current node in the _children property of the parent node.
getSiblingIndex
The function returns “the current node is on the parent node_children
Subscript (position) in property “.
setSiblingIndex
The function sets the current node to the parent node_children
Subscript (position) in property “.
๐ก cc. _BaseNode. Prototype. _children
The _children property of a node is the children property of the node.
And the children property is a getter that returns its own _children property.
Also, the children property doesn’t implement the setter, so it doesn’t work if you assign to the children property directly.
zIndex
“ZIndex” is “the key attribute used to sort nodes” and determines the position of a node between its siblings.
The value of zIndex is between cc.macro.min_zindex and cc.macro.max_zindex.
In addition, the zIndex property is implemented in cc.node using the Cocos custom getters and setters.
๐ source code:
Properties:cc.Node.prototype.zIndex
// To reduce space, some irrelevant code has been omitted
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(); }}},Copy the code
[source] ccNode.js #L1549:Github.com/cocos-creat…
What did ๐ต๏ธ do?
After pulling the source code found that the essence of zIndex is actually very simple.
That is, “Return or set the _localZOrder attribute of the node.”
๐ง is not that simple!
Interestingly, instead of returning the _localZOrder property directly in the getter, the value of the _localZOrder property is returned 16 bits to the right (>>).
Setting the _localZOrder property in the setter is also not a simple assignment, but a bit operation:
Here we break down the bitwise operations within the function from the perspective of binary numbers.
- through
& 0x0000ffff
Take out the original_localZOrder
The “lower 16 bits” of; - The target value
value
“Move 16 bits to the left”; - I’m going to move it to the left
value
As “high 16 bit” with original_localZOrder
The “lower 16 bit” merge of; - Finally, a “32-bit binary number” is obtained and given
_localZOrder
.
๐ฒ huh?
Slow! What does _localZOrder do? What a detour!
Don’t worry, the answer is in the back
Sorting (Sorting)
Careful friends should be found, siblingIndex and zIndex source code does not contain the actual sorting logic.
But they all have one thing in common: “They all end up calling their own _onSiblingIndexChanged function.”
_onSiblingIndexChanged
๐ source code:
Function:cc.Node.prototype._onSiblingIndexChanged
_onSiblingIndexChanged() {
if (this._parent) {
this._parent._delaySort(); }},Copy the code
What did ๐ต๏ธ do?
The _onSiblingIndexChanged function calls the _delaySort function of the parent node.
_delaySort
๐ source code:
Function:cc.Node.prototype._delaySort
_delaySort() {
if (!this._reorderChildDirty) {
this._reorderChildDirty = true;
cc.director.__fastOn(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this); }},Copy the code
What did ๐ต๏ธ do?
After a bit of digging around, it turns out that the real place to sort is the sortAllChildren function of the parent node.
๐ก blind, you found hua point!
It is worth noting that the sortAllChildren function call in _delaySort is not triggered immediately, but after the next update (life cycle).
The purpose of delayed firing should be to avoid repeated calls within the same frame, thereby reducing unnecessary performance costs.
sortAllChildren
๐ source code:
Function:cc.Node.prototype.sortAllChildren
// To reduce space, some irrelevant code has been omitted
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); }},Copy the code
[source] ccNode.js #L3680:Github.com/cocos-creat…
First Part (Part 1)
As we go along, we finally get to the key part.
Now let’s think about the sortAllChildren function.
By the first half of the function, I would have set (reset) the _childarbottom ORDER attribute to 1.
This is followed by a for loop that iterates through all the “children” of the current node and executes the _updateOrderOfArrival function of the “children” one by one.
๐คจ huh? What is the _updateOrderOfArrival function?
_updateOrderOfArrival
๐ source code:
Function: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);
},
Copy the code
What did ๐ต๏ธ do?
Obviously, the purpose of the _updateOrderOfArrival function is to “update the _localZOrder property of the node.”
๐ฅฑ this function also uses bit operations:
So again, let’s break it down in terms of binary numbers and the bit operations here.
- The parent node
_childArrivalOrder
(ๅ็ฝฎ) increment1
And givearrivalOrder
(If there is no parent node0
); - through
& 0xffff0000
Of the current node_localZOrder
The “high 16 bits” of; - will
arrivalOrder
As the “lower 16 bits” with the current node_localZOrder
The “high 16 bit” merge of; - Finally a new “32-bit binary number” is obtained and assigned to the current node
_localZOrder
Properties.
๐ค see here are you already confused?
Don’t worry, we’ll find out soon!
Second Half (Part 2)
The lower part of the sortAllChildren function is easier to understand.
It basically sorts the _children property of the current node through Insertion Sort.
The order is mainly based on the value of the _localZOrder attribute of the child nodes. The child nodes with a small value of _localZOrder attribute are ranked first and vice versa.
The Key to sorting
๐ค after analyzing the source code, it is found that the sorting of nodes is not as simple as imagined.
We can draw a few conclusions:
- SiblingIndex is the parent node
children
Subscripts in attributes; zIndex
Is a separate property that is not directly related to the siblingIndex;- SiblingIndex and
zIndex
Changes to trigger sorting; - SiblingIndex and
zIndex
Which together form the node_localZOrder
; zIndex
Is heavier than siblingIndex;- The node’s
_localZOrder
It directly determines the final order of nodes.
How siblingIndex affects sorting
As we mentioned earlier:
getSiblingIndex
The function “returns the value of the current node on the parent node_children
Subscript (position) in property “.setSiblingIndex
The function “sets the value of the current node on the parent node_children
Property, and tell the parent node to sort “.
This subscript is then used as the lower 16 bits of node _localZOrder in the upper part of the sortAllChildren function of the parent node.
๐ง so we can understand as follows:
SiblingIndex is the index of the element, which determines during sorting_localZOrder
Is “16 bits lower”.
How zIndex affects sorting
As we mentioned earlier:
zIndex
็getter
“Returned to the_localZOrder
The highest 16 bits “.zIndex
็setter
“Set up_localZOrder
16 bits high, and notify the parent node to sort “.
๐ง so we can understand as follows:
zIndex
It’s actually just a body, and it’s essentially_localZOrder
The “high 16 bits” of.
_localZOrder How to determine order (How _localZOrder works)
The sortAllChildren function of the parent node performs the final sort according to the _localZOrder size of the child node.
We can think of _localZOrder as a “32-bit binary number” consisting of both siblingIndex and zIndex.
But why say”zIndex
Is weighted more than siblingIndex?
Because zIndex determines the “high 16 bits” of _localZOrder, and siblingIndex determines the “low 16 bits” of _localZOrder.
So, only inzIndex
In the case of equality, the size of the siblingIndex is decisive.
And in thezIndex
If it’s not equal, the size of the siblingIndex doesn’t matter.
๐ฐ for example
There are two 32-bit binary numbers (pseudocode) :
- A:
0000 0000 0000 0001 xxxx xxxx xxxx xxxx
- B:
0000 0000 0000 0010 xxxx xxxx xxxx xxxx
Since B’s “higher 16 bits” (0000 0000 0000 0010) is larger than A’s “higher 16 bits” (0000 0000 0000 0001), B will always be greater than A, no matter what x is in their “lower 16 bits”.
Experiment
We can write a widget to test the effect of siblingIndex and zIndex on _localZOrder.
๐ a coding:
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 (binary)' })
get localZOrderBinary() {
return this.node._localZOrder.toString(2).padStart(32.0); }}Copy the code
Scene 1
A child node is placed under a node.
๐ผ Sort information about child nodes:
Generally speaking, the _childarbottom order of a node starts from 1, and will increase by 1 in calculation.
So the child’s _localZOrder is always 16 bits lower than its siblingIndex by 2 numbers.
Scene 2
A child node is placed under a node, and the child node’szIndex
Set to1
.
๐ผ Sort information about child nodes:
As can be seen, just setting the zIndex attribute of the node to 1, its _localZOrder is up to 65538.
๐ The approximate calculation process is as follows (extremely abstract pseudocode) :
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
Copy the code
๐ continues with simplified pseudocode:
_localZOrder = (zIndex << 16) | (siblingIndex + 2)
Copy the code
๐ก By the way
When a node has no parent, its arrivalOrder is always 0.
It doesn’t really matter what it is, because a node without a parent can’t be sorted anyway.
Scene 3
Six child nodes are placed under the same nodezIndex
All set to0
.
๐ฅ Sort information of each child node:
Scene 4
Six child nodes are placed under the same nodezIndex
Set to0
ๅฐ 5
.
๐ฅ Sort information of each child node:
As you can see, the value of zIndex is directly represented in the “high 16 bits” of _localZOrder; Every time zIndex increases by 1, _localZOrder increases by 65537.
So how can siblingIndex be beatenzIndex
!
Scene 5
Six child nodes are placed under the same nodezIndex
Set to0
ๅฐ 5
.
๐ฅ change the siblingIndex of the sixth child node from 0 to 4, its sorting information:
As you can see, no matter how much we change the siblingIndex of the sixth child node, it automatically changes back to 5 (the maximum value in the sibling node).
Because the zIndex of this child has an absolute advantage over its peers.
Something is wrong.
๐ฒ Here’s a phenomenon that doesn’t look right!
For example, when we change siblingIndex from 5 to 0, _localZOrder changes from 327687 to 327682; But when siblingIndex is automatically changed back to 5, _localZOrder is still 327682, not 327687.
๐ค Why is this?
The reason is simple:
When we modify the siblingIndex of the node, the sorting will be triggered. During the sorting process, “a new _localZOrder will be generated according to the siblingIndex and zIndex of the node at the current time”.
Finally, in the sortAllChildren function of the parent node, the _children array will be sorted according to the _localZOrder of the child node. At this time, “the siblingIndex of the child node will also be updated passively”. But _localZOrder is not regenerated.
However, due to the “absolute dominance” of zIndex, this “strange phenomenon” will not affect the normal ordering of nodes ~
Summary (Summary)
After analyzing the source code, let’s summarize.
There are two main ways to change the order of nodes in code:
- Modify node
zIndex
attribute - through
setSiblingIndex
The function setting
Either of these methods will eventually “sort by a combination of zIndex and siblingIndex”.
In most cases, “modifying the zIndex property of a node invalidates its setSiblingIndex function.”
This virtually increases the mental burden of coding, but also increases the difficulty of troubleshooting problems.
Usage in Engine
Out of curiosity, I searched the engine source code to see if the zIndex attribute was used internally.
The result: the zIndex property of a node is used only in a few “debug” related places.
For example, in preview mode, the Profiler node in the lower left corner.
And debug boxes for collision components, etc., which I won’t go into here.
(the Suggestion)
So, to avoid some unnecessary bugs and logical conflicts.
My advice:
“Use less or no zIndex in favor of siblingIndex related functions.”
๐ฅด listen to pipi advice, zIndex water is too deep, you can not hold!
portal
Wechat twitter version
Personal blog: Rookie small stack
Open source home page: Chen PI PI
More share
Cocos Creator Performance Optimization: DrawCall
“Draw a cool radar in Cocos Creator.”
Write a Perfect Wave with Shader
Gracefully and efficiently managing popovers in Cocos Creator
Detailed JavaScript Memory Analysis Guide
Cocos Creator Editor Extension: Quick Finder
JavaScript Raw Values and Wrapped Objects
Cocos Creator source Code interpretation: Engine startup and main Loop