本问是阅读《我所理解的cocos2d-x》第二章——Cocos2dx架构一瞥的2.3小章节后的总结。
(参考书籍《我所理解的cocos2d-x》,秦春林著)
(文中cocos2d-x版本为3.17)
一、Node类
cocos2dx支持在屏幕上绘制精灵、文本、形状、粒子、地图等,所有这些对象都继承自Node类,Node类定义了元素的布局、变换、坐标等系统
二、Position和anchorPoint
1、Node中的position属性表示一个元素在坐标系中的位置。
2、Node类中的anchorPoint属性表示锚点,即用元素的哪个位置来表示对应位置(如position);锚点的位置取决于元素的本地坐标系,范围时(0,0)至(1,1)之间,比如(0.5,0.5)就表示anchorPoint在这个元素的正中间。
三、坐标系
1、cocos2dx中的坐标系
cocos2dx的坐标系与OpenGL坐标系保持一致,都是“右手坐标系”,即横x,纵y,z朝外。
2、本地坐标系和世界坐标系
cocos2dx中坐标系分为本地坐标系和世界坐标系。
世界坐标系是指整个屏幕的坐标系,屏幕左下角为原点,自左向右是x轴,自下而上是y轴。
本地坐标系也叫相对坐标系,是指相对于父节点的坐标。以父元素的左下角为远点,向右为x,向上为y。
3、相对坐标的目的和优点
相对坐标系简化了UI元素布局,它使我们只需要关心局部,UI树则会帮助我们实现坐标系的转换。
4、本地坐标世界坐标转换方法
如果我们需要用到世界坐标系(不同层级的元素碰撞检测),可以使用以下方法进行转换。
class CC_DLL Node:public Ref
{
public:
Vec2 convertToNodeSpace(const Vec2& worldPoint) const;
Vec2 convertToWorldSpace(const Vec2& nodePoint) const;
Vec2 convertToNodeSpaceAR(const Vec2& worldPoint) const;
Vec2 convertToWorldSpaceAR(const Vec2& nodePoint) const;
Vec2 convertTouchToNodeSpace(Touch * touch) const;
Vec2 convertTouchToNodeSpaceAR(Touch * touch) const;
}
convertToNodeSpace方法是将一个世界坐标转换到该元素的本地坐标系中。
使用方法如下
//计算出node2在node1的本地坐标系中的相对坐标。
auto point = node1->convertToNodeSpace(node2->getPosition());
/*
*使用此方法可以判断两个物体是否相交
*通过计算node2在node1坐标系中的坐标
*node2锚点为(1,1)相对坐标的xy都小于0,那么肯定不相交
*node2锚点为(0,0)相对坐标的xy都大于node1元素的宽高,不相交
*/
convertToWorldSpace方法是将一个本地坐标转换到世界坐标中。
使用方法如下
auto point = node1->convertToWorldSpace(node2->getPosition());
//node1一定要是node2的父节点
//convertToWorldSpace方法是通过父节点,将子节点坐标转换为世界坐标
//注意!!!
//返回值是node2锚点的世界坐标!!!
不依赖父节点直接计算出世界坐标可以实用下面这个方法
Vec2 convertToNodeSpaceAR(const Vec2& worldPoint) const;
四、UI树
1、Node类中的UI树
cocos2dx中的UI树的根节点是Scene类,UI树的每一个节点都是Node实例对象。每一个Node对象都有一个Node*的Vector对象—— _children,以及一个Node指针—— _parent。其中Scene的parent属性为空。
class CC_DLL Node:public Ref
{
pubcli:
virtual void addChild(Node * child);
virtual void addChild(Node * child, int localZOrder);
virtual void addChild(Node* child, int localZOrder, int tag);
virtual Node * getChildByTag(int tag) const;
virtual Vector<Node*>& getChildren() { return _children; }
virtual const Vector<Node*>& getChildren() const { return _children; }
virtual ssize_t getChildrenCount() const;
virtual void setParent(Node* parent);
virtual Node* getParent() { return _parent; }
virtual const Node* getParent() const { return _parent; }
public:
Vector<Node*> _children;
Node* _parent;
}
2、ui树的遍历
(1)cocos2dx的渲染系统的职责就是遍历UI树中的每一个元素,然后将每个元素绘制到屏幕。
(2)UI树遍历两个最重要的目的(1)决定元素的绘制顺序(2)遍历过程中实现元素的模型视图变换矩阵计算,最后将这些矩阵提供给OpenGLES
(3)3D图形中,我们根据Z轴坐标值,使用深度测试进行正确绘制。但是在2D图形绘制中,我们的Z轴坐标都默认为0。
cocos2dx使用localZOrder来表示元素的逻辑深度**(本地坐标系中)**,
(4)UI树的遍历是中序遍历,(1)先遍历localZOrder小于0的“左节点”(2)遍历根节点(父节点)(3)遍历localZOrder大于0的“右节点”。
void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
// quick return if not visible. children won't be drawn.
if (!_visible)
{
return;
}
int i = 0;
if(!_children.empty())
{
//首先将所有的子节点根据localZOrder进行排序,从小到大
sortAllChildren();
// draw children zOrder < 0
for(auto size = _children.size(); i < size; ++i)
{
auto node = _children.at(i);
//此处绘制localZOrder小于父节点的
if (node && node->_localZOrder < 0)
node->visit(renderer, _modelViewTransform, flags);
else
break;
}
// self draw
//绘制父节点
if (visibleByCamera)
this->draw(renderer, _modelViewTransform, flags);
//此处的+i是将下标跳到了localZOrder大于等于父节点的对象开始
for(auto it=_children.cbegin()+i, itCend = _children.cend(); it != itCend; ++it)
(*it)->visit(renderer, _modelViewTransform, flags);
}
else if (visibleByCamera)
{
this->draw(renderer, _modelViewTransform, flags);
}
_director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
// FIX ME: Why need to set _orderOfArrival to 0??
// Please refer to https://github.com/cocos2d/cocos2d-x/pull/6920
// reset for next frame
// _orderOfArrival = 0;
}
3、localZOrder逻辑深度
在刚才代码中有提到sortAllChildren()方法;
void Node::sortAllChildren()
{
if (_reorderChildDirty)
{
sortNodes(_children);
_reorderChildDirty = false;
_eventDispatcher->setDirtyForNode(this);
}
}
//------------------------------------------
template<typename _T> inline
static void sortNodes(cocos2d::Vector<_T*>& nodes)
{
static_assert(std::is_base_of<Node, _T>::value, "Node::sortNodes: Only accept derived of Node!");
#if CC_64BITS
std::sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
return (n1->_localZOrderAndArrival < n2->_localZOrderAndArrival);
});
#else
std::stable_sort(std::begin(nodes), std::end(nodes), [](_T* n1, _T* n2) {
return n1->_localZOrder < n2->_localZOrder;
});
#endif
}
源代码中可以看出,所有子元素根据localZOrder由小到大排序(return n1->_localZOrder < n2->_localZOrder;)如果localZOrder相同,则保持添加进Vector的顺序
4、localZOrder逻辑深度的限制
cocos2dx中的localZOrder仅能控制元素在父节点的所有子节点中的绘制顺序,并不能指定元素的实际深度,也无法参与到其他不同层级元素之间的排序
5、globalZOrder
为了解决localZOrder不能解决的问题(不同层级之间的深度排序),cocos2dx新增了globalZOrder
(1)每个元素的默认globalZOrder都是0;
(2)如果globalZOrder不为0,则按照升序排序
(3)否则,按照localZOrder排序
6、globalZOrder的限制
不能对SpriteBatchNode的元素单独设置globalZOrder。因为SpriteBatchNode将所有的子元素组成一个BatchCommand进行批绘制,所以SpriteBatchNode中的子元素根本没有机会应用globalZOrder。
7、事件分发的顺序
cocos2dx—3.x版本中,元素的绘制顺序也影响着事件分发的顺序。
绘制顺序从底向顶,事件分发的顺序则是从顶向底,二者刚好相反。(最后绘制的顶层Node,最先接受事件消息)
8、模型视图变换矩阵
相对坐标对我们是友好的,但是对OpenGL不友好,因为OpenGL里面没有相对坐标这个概念,也没有UI树这个概念…
但是我们绘制图元的时候依旧需要世界坐标以及模型矩阵。
怎么办呢???没有相对坐标,那我们可以用“相对矩阵”!在cocos2dx中就是如此,为了方便我们,程序依旧是将元素的相对坐标系传递给OpenGL,然后将元素相对于世界坐标系的相对模型视图变换矩阵传输至渲染管线,然后在渲染管线中,将相对坐标再变回世界坐标!!!
每一个Node对象都维护了一个模型视图变换矩阵,这个矩阵由父节点的模型变换矩阵右乘当前节点在本地坐标系中的变换矩阵得到(父节点矩阵右乘本地矩阵是因为Node对象先进行本地变换再将坐标回到世界坐标系!顺序不能错)
每个元素的模型视图矩阵的计算只有在场景中某些相关元素位置发生变更时才会重新进行。(这里主要是指自身的位置发生了变化或者父级链上的某个节点发生了变化)
9、运行时游戏对象
Node类种设置了一个tag变量,用于查找UI对象,
Node
{
public:
int _tag; ///< a tag. Can be any number you assigned just to identify this node
std::string _name; ///<a string label, an user defined string to identify this node
virtual int getTag() const;
virtual void setTag(int tag);
virtual const std::string& getName() const;
virtual void setName(const std::string& name);
virtual Node * getChildByTag(int tag) const;
/**
* Gets a child from the container with its name.
*
* @param name An identifier to find the child node.
*
* @return a Node object whose name equals to the input parameter.
*
* @since v3.2
*/
virtual Node* getChildByName(const std::string& name) const;
}
Node* Node::getChildByName(const std::string& name) const
{
CCASSERT(!name.empty(), "Invalid name");
std::hash<std::string> h;
size_t hash = h(name);
for (const auto& child : _children)
{
// Different strings may have the same hash code, but can use it to compare first for speed
if(child->_hashOfName == hash && child->_name.compare(name) == 0)
return child;
}
return nullptr;
}
从源码中可以看出,Node类对象可以给对象设置一个tag或者一个name,父节点可以通过tag或者name查找对应的子节点。
tag是整数,对比操作肯定会比name快一点,但是设置string的话更容易理解。(毕竟是字符串)
五、UI元素与内存管理
1、_referenceCount引用计数管理内存
前面提到,为了便于内存管理,cocos2dx通过引用计数_reference对所有的Node元素进行内存管理。
真正对引用计数进行管理的是Ref类,而Node类继承了Ref类。
Ref::Ref()
: _referenceCount(1) // when the Ref is created, the reference count of it is 1
#if CC_ENABLE_SCRIPT_BINDING
, _luaID (0)
, _scriptObject(nullptr)
, _rooted(false)
#endif
{
#if CC_ENABLE_SCRIPT_BINDING
static unsigned int uObjectCount = 0;
_ID = ++uObjectCount;
#endif
#if CC_REF_LEAK_DETECTION
trackRef(this);
#endif
}
我们调用UI对象的create方法创建,引用计数默认为1,然后我们将它放进autoreleasePool。
并且autoreleasePool里面的元素一帧后会执行一次 _reference - 1的操作。
Node * Node::create()
{
Node * ret = new (std::nothrow) Node();
if (ret && ret->init())
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}
如果我们将对象添加进任意父节点,它的引用计数_referenceCount都会 +1。(代码如下)
所以ret->autorelease();这个操作其实是将我们创建但是没有加进UI树的对象在一帧后释放。
void Node::addChild(Node* child, int localZOrder, const std::string &name)
{
CCASSERT(child != nullptr, "Argument must be non-nil");
CCASSERT(child->_parent == nullptr, "child already added. It can't be added again");
addChildHelper(child, localZOrder, INVALID_TAG, name, false);
}
void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
if (_children.empty())
{
this->childrenAlloc();
}
this->insertChild(child, localZOrder);
if (setTag)
child->setTag(tag);
else
child->setName(name);
child->setParent(this);
}
// helper used by reorderChild & add
void Node::insertChild(Node* child, int z)
{
#if CC_ENABLE_GC_FOR_NATIVE_OBJECTS
auto sEngine = ScriptEngineManager::getInstance()->getScriptEngine();
if (sEngine)
{
sEngine->retainScriptObject(this, child);
}
#endif // CC_ENABLE_GC_FOR_NATIVE_OBJECTS
_transformUpdated = true;
_reorderChildDirty = true;
_children.pushBack(child);
child->_setLocalZOrder(z);
}
2、重写vector
cocos2dx中重写了vector,所以我们的_referenceCount的加减操作都在里面。
在我们将对象添加进父节点(加进vector< Node* >_children)或者从父结点删除的时候,会进行对应的引用计数+1或-1。
/** Adds a new element at the end of the Vector. */
void pushBack(T object)
{
CCASSERT(object != nullptr, "The object should not be nullptr");
_data.push_back( object );
object->retain();
}
/** Removes the last element in the Vector. */
void popBack()
{
CCASSERT(!_data.empty(), "no objects added");
auto last = _data.back();
_data.pop_back();
last->release();
}
3、_reference改变时检查是否释放
调用release函数的时候,判断_referenceCount是否为0,为0就释放。
void Ref::release()
{
CCASSERT(_referenceCount > 0, "reference count should be greater than 0");
--_referenceCount;
if (_referenceCount == 0)
{
delete this;
}
}
所以在操作对象的过程中,绝对不能让它的引用计数变为0,一旦为0就会被释放掉。
比如说我们想要重用一个元素,将一个元素从一个父节点转移到另一个父节点,为了防止子节点被释放我们应该进行如下操作。
auto node = parent->getChildByTag(10);//首先我们获得子节点,此时引用计数为1
node->retain();//子节点进行retain,引用计数+1,变为2;
node->removeFromParent();//子节点从当前父节点脱离,引用计数变为1;
node->autorelease();//加入autoReleasePool,在一帧结束后进行引用计数减1操作
parent2->addchild(node);//将子节点加入新的父节点,此时引用计数为2。但是因为加入了autoReleasePool,所以一帧后引用计数依旧会变为1。
上面的操作中最重要的就是retain()操作,一定要保证子节点的引用计数大于等于1,不然就会被释放!!!