Cocos2d-x 3.x基础学习:深入理解Cocos2d-x 3.x UI树
本文基于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
有许多变种,如LayerColor
。Layer
上可以添加各种图片和控件,这些图片和控件也可以添加子节点。由于所有元素的基类都是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树时引用计数的变化
下面是Node
的addChild
方法的源码分析(真正的实现在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);
这段代码不会造成内存泄露,开发者无需关心内存分配问题,引擎会自动处理。但这种方式也存在不足。
例如,需要在s
和l
之间插入一个新层m
,由于removeChild
后l
的内存已被释放,直接操作会出现问题。可以采用以下方法解决:
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
的内存被释放,但需要注意retain
和release
方法必须成对出现,否则会造成内存泄露。为避免开发者忘记调用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);
使用智能指针可以完全无需关心内存的申请和释放,后续会对智能指针进行详细分析。