cocos中的UI节点Node成树形结构。以Scene对象为根节点,树中每个节点都是Node对象。每个对象有一个children集合和parent节点,Scene的parent为空。
一、UI树结构
class Node {void addChild(Node * child);void addChild(Node * child, int localZOrder);void addChild(Node* child, int localZOrder, int tag);void addChild(Node* child, int localZOrder, const std::string &name);void setParent(Node* parent);Node* getParent() { return _parent; } // 1个父节点Node* getChildByTag(int tag) const;Node* getChildByName(const std::string& name) const;ssize_t getChildrenCount() const;Vector<Node*>& getChildren() { return _children; } // 多个子节点}
二、UI树遍历
渲染系统最重要的职责就是遍历UI树,然后将元素绘制到屏幕中。
UI树的遍历有两个重要目的:
- 决定绘制树元素绘制顺序
- 元素的模型视图变换矩阵计算,结果供OpenGL ES渲染管线计算顶点位置。
3D游戏是根据模型顶点的Z坐标值经过投影变换之后得到深度值,然后通过深度测试(Depth Test)来先后进行绘制。2D游戏顶点Z坐标都0,也就是说没有深度(前后)的概念,后调用的绘制会覆盖前调用的绘制内容。
因此我们需要自己设计一个深度值(我们叫它“逻辑深度”)来决定这些节点的绘制顺序,Node类中的localZOrder就是元素的“逻辑深度”。UI树采用中序遍历的深度优先算法进行遍历,具体如下:
点击查看【processon】
遍历源码:
Node::visit(......){......if(!_children.empty()){// *******************************************// ***** 第一步,排序子节点,按localZOrder升序// *******************************************sortAllChildren();// *******************************************// ***** 第二步,绘制localZOrder < 0的子节点,即上图的“左子节点”// *******************************************for(auto size = _children.size(); i < size; ++i){auto node = _children.at(i);if (node && node->_localZOrder < 0)node->visit(renderer, _modelViewTransform, flags);elsebreak;}// *******************************************// ***** 第三步,绘制自己// *******************************************if (visibleByCamera)// 见下面解释,我们以Sprite的draw为例this->draw(renderer, _modelViewTransform, flags);// *******************************************// ***** 第四步,绘制localZOrder >=0的子节点,即上图的“右子节点”// *******************************************for(auto it=_children.cbegin()+i, itCend = _children.cend(); it != itCend; ++it)(*it)->visit(renderer, _modelViewTransform, flags);}......}void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags){if (_texture == nullptr){return;}#if CC_USE_CULLING...... // 如果出现在出现在屏幕外面,就不会生成绘制指令,叫auto cullingif(_insideBounds)#endif{// 绘制精灵用的是TrianglesCommand。_trianglesCommand.init(_globalZOrder,_texture,getGLProgramState(),_blendFunc,_polyInfo.triangles,transform,flags);// 并没有执行绘制,而是加入了绘制栈中,后续统一执行renderer->addCommand(&_trianglesCommand);}
通过“逻辑深度”,我们可以指定同级元素的绘制顺序,即node节点与其直接子节点的绘制顺序,但是无法指定不同级元素的绘制顺序,比如让一个低层级的元素始终可见,当然我们可以将元素添加到满足需求的节点上,显然这就失去了灵活性。
cocos通过globalZOrder解决这个问题,新的绘制顺序逻辑如下:
- 1、globalZOrder默认0。
- 2、globalZOrder!=0,按globalZOrder进行排序。
- 3、globalZOrder==0,按localZOrder进行排序。
但不能对SpriteBatchNode的子元素分别设置globalZOrder,因为这些元素会组织成一个BatchCommand,已经无需用到ZOrder了。
三、模型视图变换矩阵
每个节点维护了一个模型视图变换矩阵_modelViewTransform,它通过传入的父级变换矩阵parentTransform右乘节点在本地坐标系中的变换矩阵得到。
节点的变换矩阵和节点的相对坐标传入OpenGL ES渲染管线中,然后在管线中对相对位置执行该坐标变换。
相关源码:
void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags){// UI树遍历时,传入了父节点的变换矩阵parentTransform......uint32_t flags = processParentFlags(parentTransform, parentFlags);......}uint32_t Node::processParentFlags(const Mat4& parentTransform, uint32_t parentFlags){......if(flags & FLAGS_DIRTY_MASK) // 某些元素的位置发生变化,如自身位置// 或者父链上的某个元素位置发生变化。// 总之是会影响到当前节点位置的节点的位置发生变动时,// 才进行计算,可以提升遍历性能。_modelViewTransform = this->transform(parentTransform);return flags;}Mat4 Node::transform(const Mat4& parentTransform){// 父节点模型视图变换矩阵 右乘 节点在本地坐标系中的变换矩阵 = 节点的模型视图变换矩阵。return parentTransform * this->getNodeToParentTransform();}
