cocos中的UI节点Node成树形结构。以Scene对象为根节点,树中每个节点都是Node对象。每个对象有一个children集合和parent节点,Scene的parent为空。

一、UI树结构

  1. class Node {
  2. void addChild(Node * child);
  3. void addChild(Node * child, int localZOrder);
  4. void addChild(Node* child, int localZOrder, int tag);
  5. void addChild(Node* child, int localZOrder, const std::string &name);
  6. void setParent(Node* parent);
  7. Node* getParent() { return _parent; } // 1个父节点
  8. Node* getChildByTag(int tag) const;
  9. Node* getChildByName(const std::string& name) const;
  10. ssize_t getChildrenCount() const;
  11. Vector<Node*>& getChildren() { return _children; } // 多个子节点
  12. }

二、UI树遍历

渲染系统最重要的职责就是遍历UI树,然后将元素绘制到屏幕中。
UI树的遍历有两个重要目的:

  • 决定绘制树元素绘制顺序
  • 元素的模型视图变换矩阵计算,结果供OpenGL ES渲染管线计算顶点位置。

3D游戏是根据模型顶点的Z坐标值经过投影变换之后得到深度值,然后通过深度测试(Depth Test)来先后进行绘制。2D游戏顶点Z坐标都0,也就是说没有深度(前后)的概念,后调用的绘制会覆盖前调用的绘制内容。
因此我们需要自己设计一个深度值(我们叫它“逻辑深度”)来决定这些节点的绘制顺序,Node类中的localZOrder就是元素的“逻辑深度”。UI树采用中序遍历的深度优先算法进行遍历,具体如下:
点击查看【processon】
遍历源码:

  1. Node::visit(......){
  2. ......
  3. if(!_children.empty())
  4. {
  5. // *******************************************
  6. // ***** 第一步,排序子节点,按localZOrder升序
  7. // *******************************************
  8. sortAllChildren();
  9. // *******************************************
  10. // ***** 第二步,绘制localZOrder < 0的子节点,即上图的“左子节点”
  11. // *******************************************
  12. for(auto size = _children.size(); i < size; ++i)
  13. {
  14. auto node = _children.at(i);
  15. if (node && node->_localZOrder < 0)
  16. node->visit(renderer, _modelViewTransform, flags);
  17. else
  18. break;
  19. }
  20. // *******************************************
  21. // ***** 第三步,绘制自己
  22. // *******************************************
  23. if (visibleByCamera)
  24. // 见下面解释,我们以Sprite的draw为例
  25. this->draw(renderer, _modelViewTransform, flags);
  26. // *******************************************
  27. // ***** 第四步,绘制localZOrder >=0的子节点,即上图的“右子节点”
  28. // *******************************************
  29. for(auto it=_children.cbegin()+i, itCend = _children.cend(); it != itCend; ++it)
  30. (*it)->visit(renderer, _modelViewTransform, flags);
  31. }
  32. ......
  33. }
  34. void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
  35. {
  36. if (_texture == nullptr)
  37. {
  38. return;
  39. }
  40. #if CC_USE_CULLING
  41. ...... // 如果出现在出现在屏幕外面,就不会生成绘制指令,叫auto culling
  42. if(_insideBounds)
  43. #endif
  44. {
  45. // 绘制精灵用的是TrianglesCommand。
  46. _trianglesCommand.init(_globalZOrder,
  47. _texture,
  48. getGLProgramState(),
  49. _blendFunc,
  50. _polyInfo.triangles,
  51. transform,
  52. flags);
  53. // 并没有执行绘制,而是加入了绘制栈中,后续统一执行
  54. renderer->addCommand(&_trianglesCommand);
  55. }

通过“逻辑深度”,我们可以指定同级元素的绘制顺序,即node节点与其直接子节点的绘制顺序,但是无法指定不同级元素的绘制顺序,比如让一个低层级的元素始终可见,当然我们可以将元素添加到满足需求的节点上,显然这就失去了灵活性。
cocos通过globalZOrder解决这个问题,新的绘制顺序逻辑如下:

  • 1、globalZOrder默认0。
  • 2、globalZOrder!=0,按globalZOrder进行排序。
  • 3、globalZOrder==0,按localZOrder进行排序。

但不能对SpriteBatchNode的子元素分别设置globalZOrder,因为这些元素会组织成一个BatchCommand,已经无需用到ZOrder了。

三、模型视图变换矩阵

每个节点维护了一个模型视图变换矩阵_modelViewTransform,它通过传入的父级变换矩阵parentTransform右乘节点在本地坐标系中的变换矩阵得到。
节点的变换矩阵和节点的相对坐标传入OpenGL ES渲染管线中,然后在管线中对相对位置执行该坐标变换。
相关源码:

  1. void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags){
  2. // UI树遍历时,传入了父节点的变换矩阵parentTransform
  3. ......
  4. uint32_t flags = processParentFlags(parentTransform, parentFlags);
  5. ......
  6. }
  7. uint32_t Node::processParentFlags(const Mat4& parentTransform, uint32_t parentFlags){
  8. ......
  9. if(flags & FLAGS_DIRTY_MASK) // 某些元素的位置发生变化,如自身位置
  10. // 或者父链上的某个元素位置发生变化。
  11. // 总之是会影响到当前节点位置的节点的位置发生变动时,
  12. // 才进行计算,可以提升遍历性能。
  13. _modelViewTransform = this->transform(parentTransform);
  14. return flags;
  15. }
  16. Mat4 Node::transform(const Mat4& parentTransform)
  17. {
  18. // 父节点模型视图变换矩阵 右乘 节点在本地坐标系中的变换矩阵 = 节点的模型视图变换矩阵。
  19. return parentTransform * this->getNodeToParentTransform();
  20. }