Cocos2d-x 3.x基础学习:深入理解Cocos2d-x 3.x UI树

2015年03月25日 11:57 0 点赞 0 评论 更新于 2017-05-09 20:34

本文基于Cocos2d-x 3.2版本,其他版本的API可能会有所变化,主要探讨对UI树的理解。

Cocos2d-x 3.x 引擎的UI树系统

基础概念普及

Cocos2d-x的游戏世界通常由一个个场景(Scene)构成,例如登录场景、战斗场景等。每个场景又可细分为多个层(Layer),像界面层、地图层等。而层之下还包含了精灵、UI控件以及各类界面元素。这些元素都基于一个名为Node的基类。

Node类的功能分析

下面是Node类的部分声明(忽略了部分无关函数):

class CC_DLL Node : public Ref
{
public:
////// ADD //////
virtual void addChild(Node * child);
virtual void addChild(Node * child, int localZOrder);
virtual void addChild(Node* child, int localZOrder, int tag);
virtual void addChild(Node* child, int localZOrder, const std::string &name);

////// GET //////
virtual Node * getChildByTag(int tag) const;
virtual Node* getChildByName(const std::string& name) const;
template <typename T>
inline T getChildByName(const std::string& name) const { return static_cast<T>(getChildByName(name)); }
virtual void enumerateChildren(const std::string &name, std::function<bool(Node* node)> callback) const;
virtual Vector<Node*>& getChildren() { return _children; }
virtual const Vector<Node*>& getChildren() const { return _children; }
virtual ssize_t getChildrenCount() const;
virtual Node* getParent() { return _parent; }
virtual const Node* getParent() const { return _parent; }

////// REMOVES //////
virtual void removeFromParent();
virtual void removeFromParentAndCleanup(bool cleanup);
virtual void removeChild(Node* child, bool cleanup = true);
virtual void removeChildByTag(int tag, bool cleanup = true);
virtual void removeChildByName(const std::string &name, bool cleanup = true);
virtual void removeAllChildren();
virtual void removeAllChildrenWithCleanup(bool cleanup);
};

从上述代码可以看出,Node类的方法大致可分为三类:

1. addChild方法

该方法用于为当前节点添加子节点。例如,将一个精灵添加到层中可以这样实现:

Layer* l = Layer::create();
Sprite* s = Sprite::create();
l->addChild(s);
this->addChild(l);

addChild还有其他几个变种,这里不再详细解释。它是UI树的重要组成部分,会将节点添加到UI树中,并对该节点保持强引用(关于内存部分,后续文章会详细说明)。

2. getXXX方法

这类方法使用较为频繁,这里着重介绍两个新出现的方法:

template <typename T>
inline T getChildByName(const std::string& name) const { return static_cast<T>(getChildByName(name)); }
virtual void enumerateChildren(const std::string &name, std::function<bool(Node* node)> callback) const;
  • getChildByName<T>:与普通的getChildByName功能类似,只是增加了模板,无需手动进行静态转换。例如,要获取层下名为“exit”的精灵节点,可以这样使用:
    l->getChildByName<Sprite>("exit");
    

    这种方式使代码更加简洁。

  • enumerateChildren:该函数非常强大,它会搜索当前节点下的所有子节点,只要子节点的名字与参数name一致,就会执行第二个参数的回调函数callback(后续会单独出一篇文章对该函数进行详细分析)。

3. removeXXXX方法

这些方法用于从UI树中移除某个节点。例如,removeFromParent用于将自身从父节点中移除,removeChild用于将参数中的子节点从UI树中移除。

UI树的组成

一般来说,Scene是UI树的根节点,Layer节点通常作为Scene的子节点,并且Layer可以嵌套。Layer有许多变种,如LayerColorLayer上可以添加各种图片和控件,这些图片和控件也可以添加子节点。由于所有元素的基类都是Node,所以添加操作非常灵活。不过需要注意的是,虽然可以将Layer添加到Sprite上,但在正常需求下,这种做法不太符合逻辑。

合理使用Cocos2d-x 3.x引擎的UI树系统,能够加快游戏开发进度,提高游戏的稳定性。

UI树的内存管理机制及UI内存的合理管理

Cocos2d-x的内存管理机制概述

Cocos2d-x采用引用计数法进行内存管理。其核心思想是,当某个类需要引用变量x时,增加x的引用计数;当不再需要x时,减少其引用计数。这样,引用和释放操作成对出现,可避免内存泄露问题。

Cocos2d-x还扩展了自动延迟释放机制。例如,变量x在某一帧需要使用,但下一帧可以释放。手动在下一帧释放x操作繁琐且不直观,Cocos2d-x提供了autorelease函数,将对象加入默认的自动释放池,在一帧结束时,引擎会自动清理自动释放池中的变量内存。

Cocos2d-x通常使用create方法创建指针,该方法被封装成宏定义CREATE_FUNC,其实现如下:

#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
__TYPE__ *pRet = new __TYPE__(); \
if (pRet && pRet->init()) \
{ \
pRet->autorelease(); \
return pRet; \
} \
else \
{ \
delete pRet; \
pRet = NULL; \
return NULL; \
} \
}

在这个宏定义中,首先使用new创建对象,此时对象的引用计数为1。初始化完成后,调用autorelease,引用计数仍为1,但对象被加入自动释放池,在这一帧结束时,对象的引用计数将变为0,对象会被释放。

节点加入UI树时引用计数的变化

下面是NodeaddChild方法的源码分析(真正的实现在addChildHelper中,忽略了不相关的代码):

void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
this->insertChild(child, localZOrder);

if (setTag)
child->setTag(tag);
else
child->setName(name);

child->setParent(this);
child->setOrderOfArrival(s_globalOrderOfArrival++);
}

真正的实现位于insertChild函数中,继续跟进:

void Node::insertChild(Node* child, int z)
{
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);
child->_setLocalZOrder(z);
}

这里将child加入到了_children中,_children的声明如下:

Vector<Node*> _children;

注意,这是Cocos2d-x自己实现的可变数组,与std::vector的最大区别是引入了引用计数机制。下面是pushBack方法的实现:

void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain();
}

重点在于object->retain(),它会对添加进来的对象增加引用计数,这表明所有加入UI树的节点都会被UI树保持强引用。

节点从UI树中移除时引用计数的变化

removeChild函数为例进行分析:

void Node::removeChild(Node* child, bool cleanup /* = true */)
{
// explicit nil handling
if (_children.empty())
{
return;
}

ssize_t index = _children.getIndex(child);
if( index != CC_INVALID_INDEX )
this->detachChild( child, index, cleanup );
}

该函数最终调用detachChild函数,继续跟进(忽略无关代码):

void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
// set parent nil at the end
child->setParent(nullptr);

_children.erase(childIndex);
}

重点代码是_children.erase(childIndex),继续查看其实现:

iterator erase(ssize_t index)
{
CCASSERT(!_data.empty() && index >=0 && index < size(), "Invalid index!");
auto it = std::next( begin(), index );
(*it)->release();
return _data.erase(it);
}

这里执行了(*it)->release(),减少了对象的引用计数,从而将节点从UI树中分离且不会造成内存泄露。

内存管理的实际应用及问题解决

考虑以下代码:

Scene* s = Scene::create();
Director::getInstance()->runWithScene(s);
Layer* l = Layer::create();
s->addChild(l);

// 若干帧后
s->removeChild(l);

这段代码不会造成内存泄露,开发者无需关心内存分配问题,引擎会自动处理。但这种方式也存在不足。

例如,需要在sl之间插入一个新层m,由于removeChildl的内存已被释放,直接操作会出现问题。可以采用以下方法解决:

Scene* s = Scene::create();
Director::getInstance()->runWithScene(s);
Layer* l = Layer::create();
l->setTag(1);
s->addChild(l);

// 若干帧后
auto l = s->getChildByTag(1);
l->retain();
s->removeChild(l);
Layer* m = Layer::create();
s->addChild(m);
m->addChild(l);
l->release();

这种方法通过提前增加引用计数避免了l的内存被释放,但需要注意retainrelease方法必须成对出现,否则会造成内存泄露。为避免开发者忘记调用release,可以使用智能指针,示例代码如下:

Scene* s = Scene::create();
Director::getInstance()->runWithScene(s);
Layer* l = Layer::create();
l->setTag(1);
s->addChild(l);

// 若干帧后
RefPtr<Node*> l = s->getChildByTag(1);
s->removeChild(l);
Layer* m = Layer::create();
s->addChild(m);
m->addChild(l);

使用智能指针可以完全无需关心内存的申请和释放,后续会对智能指针进行详细分析。

作者信息

boke

boke

共发布了 1025 篇文章