上一章 我们分析了Cocos2d-x的内存管理,主要解剖了 Ref、PoolManager、AutoreleasePool这三个类,了解了对象是如何自动释放的机制。之前有一个类 Node经常出现在各种场合,不是做为参数就是做为返回值,那么这一章节我们就去看看这个Node类到底在Cocos2d-x里处于一个什么样的地位。
直接进入主题,我们打开CCNode.h文件。我去,这个文件有1500行,这么长怎么看啊,放松一下整体看过一遍,松了一口气,还好,还没那么糟,文件虽然大,注释占了有90%的篇幅,代码不多,注释多正好 方便我们阅读,信心来了。
CCNode.h文件里面一共定义了两个类 Node与__NodeRGBA类,从命名上可以看得出 __NodeRGBA类肯定是依靠Node类,或者说是Node类的一个扩展,那么好我们开始看一下今天的主角 Node。
老规矩,在看Node类之前我们先看一下这个文件里面的头文件和引用的类都有哪些,通过这些信息能让我们更好的了解Node类和其它类的关系。
#include "ccMacros.h" #include "CCAffineTransform.h" #include "CCGL.h" #include "ccGLStateCache.h" #include "CCGLProgram.h" #include "CCScriptSupport.h" #include "CCProtocols.h" #include "CCEventDispatcher.h" #include "CCVector.h" #include "kazmath/kazmath.h" NS_CC_BEGIN class GridBase; class Point; class Touch; class Action; class LabelProtocol; class Scheduler; class ActionManager; class Component; class ComponentContainer; class EventDispatcher; class Scene; class Renderer; #if CC_USE_PHYSICS class PhysicsBody; #endif /** * @addtogroup base_nodes * @{ */ enum { kNodeOnEnter, kNodeOnExit, kNodeOnEnterTransitionDidFinish, kNodeOnExitTransitionDidStart, kNodeOnCleanup }; bool nodeComparisonLess(Node* n1, Node* n2); class EventListener;
果然这个Node类是个重要的角色啊 下面简要分析一下引入的类
GridBase、Point 自己定义 的数据类型,格子与点
Touch 应该和手机触摸事件相关的
Action 动作相关
LabelProtocol 标签UI接口
Scheduler 调度控制
ActionManager 动作管理器 (动作这部分后面我们专门的章节来讨论)
Component 组件(是什么组件呢?不看代码不知道啊,之后碰到了再分析源码)
ComponentContainer (组件容器,貌似和UI有关系)
EventDispatcher、EventListener 这是事件相关的(事件我们也分章节讨论)
Scene 场景
Renderer 渲染相关
还定义了几个事件,用了一个枚举 kNodeOnEnter kNodeOnExit kNodeOnEnterTransitionDidFinish kNodeOnExitTransitionDidStart kNodeOnCleanup
bool nodeComparisonLess(Node* n1, Node* n2); 这个函数看起来是一个谓词函数用来比较两个Node类的大小
到目前为止,Node类涉及到的元素我们也有了初步了解,接下来我们看一下Node类的定义
class CC_DLL Node : public RefNode类继承了Ref类,采用了Cocos2d-x的内部内存管理机制,这个上一章节我们讨论过不再详述。
再看一下Node类所有成员变量,看看究竟Node都封装了哪些东东。
类定义最先定义了一个类静态变量值得我们注意一下。
static const int INVALID_TAG = -1;这是所有Node类对象共用的一个属性,从命令上可以知晓这个参数标记了 Node类是否有效,具体是干什么有效,后面 我们碰到使用这个变量的方法时就会知晓了,小鱼看到这里也不是很清楚。碰到再说。(预测是标记着,渲染,事件,触摸,等是否有效)。
再看其它属性,咱们一个一个来看。
float _rotationX; ///延X轴旋转角度 float _rotationY; ///延Y轴旋转角度最先看到这两个属性,是延X,Y旋转的角度顺便翻看了一下这两个属性的get / set 方法
/** * Sets the rotation (X,Y,Z) in degrees. * Useful for 3d rotations */ virtual void setRotation3D(const Vertex3F& rotation); /** * returns the rotation (X,Y,Z) in degrees. */ virtual Vertex3F getRotation3D() const;
看到这两个函数名,这真是个坑啊,原以为能命名成setRotationX或者setRotationY之类的,结果是两个变量都在一个方法里进行get / set
这里出现了一个新碰到的结构Vertex3F这是一个以OpenGl命名习惯的定义 Vertex (顶点) 3F 需要3个float参数 ,这么一翻译那么这是一个顶点的结构定义,随便看下Vertex3F的结构体定义体会一下。
struct
Vertex3F { Vertex3F(float _x, float _y, float _z) : x(_x) , y(_y) , z(_z) {} Vertex3F(): x(0.f), y(0.f), z(0
.f) {}
GLfloat x;
GLfloat y;
GLfloat z;
};
Vertex3F就是一个三维数据结构体,没什么深奥的。
为了理解好RotationX,RotationY 是什么样的属性,小鱼做了一个小小的Demo帮助一起理解,这里放出Gif图。
_rotationX 每100毫秒增加10度 呈现效果
_rotationY 每100毫秒增加10度 呈现效果
处于好奇,如果rotationX Y一起变化是个什么情况呢?
_rotationX _rotationY 每100毫秒各增加10度 呈现效果
哈哈,通过这几个效果一目了然这两个变量所起的作用了。
下面继续看,第二组Node的成员变量
// rotation Z is decomposed in 2 to simulate Skew for Flash animations float _rotationZ_X;///< rotation angle on Z-axis, component X float _rotationZ_Y; ///< rotation angle on Z-axis, component Y
又出来两个旋转属性,有点蒙啊,仔细看下注释,这两个属性是用来描述延Z转旋转时相对X,Y转的分量……这是熊么玩意?
为了破解这个属性的含义,我再从cpp文件里找一下关于这个属性的 get / set方法。
找到如下方法
/** * Sets the rotation (angle) of the node in degrees. * * 0 is the default rotation angle. * Positive values rotate node clockwise, and negative values for anti-clockwise. * * @param rotation The rotation of the node in degrees. */ virtual void setRotation(float rotation); /** * Returns the rotation of the node in degrees. * * @see `setRotation(float)` * * @return The rotation of the node in degrees. */ virtual float getRotation() const;
这两个方法是同时设置/获得 _rotationZ_X和_rotationZ_Y 的值,并且使 _rotationZ_X = _rotationZ_Y
还有四个是单独设置 _rotationZ_X与_rotationZ_Y的值的方法。
/** * Sets the X rotation (angle) of the node in degrees which performs a horizontal rotational skew. * * The difference between `setRotationalSkew()` and `setSkew()` is that the first one simulate Flash's skew functionality * while the second one uses the real skew function. * * 0 is the default rotation angle. * Positive values rotate node clockwise, and negative values for anti-clockwise. * * @param rotationX The X rotation in degrees which performs a horizontal rotational skew. */ virtual void setRotationSkewX(float rotationX); CC_DEPRECATED_ATTRIBUTE virtual void setRotationX(float rotationX) { return setRotationSkewX(rotationX); } /** * Gets the X rotation (angle) of the node in degrees which performs a horizontal rotation skew. * * @see `setRotationSkewX(float)` * * @return The X rotation in degrees. */ virtual float getRotationSkewX() const; CC_DEPRECATED_ATTRIBUTE virtual float getRotationX() const { return getRotationSkewX(); } /** * Sets the Y rotation (angle) of the node in degrees which performs a vertical rotational skew. * * The difference between `setRotationalSkew()` and `setSkew()` is that the first one simulate Flash's skew functionality * while the second one uses the real skew function. * * 0 is the default rotation angle. * Positive values rotate node clockwise, and negative values for anti-clockwise. * * @param rotationY The Y rotation in degrees. */ virtual void setRotationSkewY(float rotationY); CC_DEPRECATED_ATTRIBUTE virtual void setRotationY(float rotationY) { return setRotationSkewY(rotationY); } /** * Gets the Y rotation (angle) of the node in degrees which performs a vertical rotational skew. * * @see `setRotationSkewY(float)` * * @return The Y rotation in degrees. */ virtual float getRotationSkewY() const; CC_DEPRECATED_ATTRIBUTE virtual float getRotationY() const { return getRotationSkewY(); }
英文水平好的同学可以看看注释加以理解
像小鱼这样英语四级考了八次都没过的,可以参考下面的gif图来理解这两个变量究竟是来做什么的。
_rotationZ_X 每100毫秒增加10度 呈现效果
_rotationZ_Y 每100毫秒增加10度 呈现效果
_rotationZ_X _rotationZ_Y一起改变 呈现效果
怎么样,当_rotationZ_X与 _rotationZ_Y同时变化时就是在平面内旋转 setRotation 方法就是设置旋转的。
看了上面这些方法定义的时候我注意到了这样一段代码
以setRotationSkewX为例
void Node::setRotationSkewX(float rotationX) { if (_rotationZ_X == rotationX) return; _rotationZ_X = rotationX; _transformUpdated = _transformDirty = _inverseDirty = true; }
大家看一下标红的这行代码 设置了三个成员变量 _transformUpdated 、 _transformDirty、_inverseDirty 三个值为true;
再找一下这三个变量的声明代码
// "cache" variables are allowed to be mutable mutable kmMat4 _transform; ///< 变换矩阵 mutable bool _transformDirty; ///< 是否应该进行变换矩阵计算 mutable kmMat4 _inverse; ///< 倒转变换矩阵 mutable bool _inverseDirty; ///< 标记是否应该进行倒转计算 mutable kmMat4 _additionalTransform; ///附加的变换矩阵 bool _useAdditionalTransform; ///< 使用附加变换 bool _transformUpdated; ///< 标记是否需要在场景更新的最后进行矩阵变换计算。
可以猜到,在设置了Node的一些旋转属性后同时会设置了一些矩阵计算的标识,用来标记是否进行旋转后的矩阵计算。
继续看Node类的属性,我们看到了这面一组设置Node的缩放的变量
float _scaleX; ///< scaling factor on x-axis float _scaleY; ///< scaling factor on y-axis float _scaleZ; ///< scaling factor on z-axis
这个不用解释太多,就是延 x, y, z轴的缩放比例
看一下对应这三个值的get/set方法
/** * _scaleX的 get set 方法 */ virtual void setScaleX(float scaleX); virtual float getScaleX() const; /** * _scaleY的 get set 方法 */ virtual void setScaleY(float scaleY); virtual float getScaleY() const; /** * _scaleZ的get set 方法 */ virtual void setScaleZ(float scaleZ); virtual float getScaleZ() const; /** * 将_scaleX _scaleY _scaleZ设置成相同值的 get set 方法 */ virtual void setScale(float scale); virtual float getScale() const;
下面我用一个图来说明这几个方法的作用
缩放比例 当 0~1时是缩小 等于1时是原大小 大于1时是放大。
通过上在的图可以看到实际上在z方向的缩放对Node实际上不起作用,很正常因为这是2D引擎。
继续看Node类的属性
Point _position; ///< node的位置 float _positionZ; ///< 在OpenGl里的 z位置
node的这两个属性一看就知道是什么意思了,就是标记这个node的位置的变量, 这两个变量的 get set 方法很简单就不跟进了。
这里出现了一个新的类型 Point 从命名上就可以知道这是记录一个点的数据结构
我们看一下这个Point类是怎么定义的
class
CC_DLL Point { public: float x; float y;
这个类后面有一大堆的方法和运算符重载操作,有兴趣的同学可以跟进看一下,大概就是操作这 x, y两个值 的,有比较两个point的方法求平方和,点乘,叉乘,等向量运算的方法,可以把它理解成一个点,也可以理解成一个向量,具体看实际应用。
再继续向下看Node的成员变量
float _skewX; ///< skew angle on x-axis float _skewY; ///< skew angle on y-axis又出来了两个形变的变量,从字面上理解是与x ,y 轴的角度
再跟进这个个变量的 get set 方法很简单,就是普通的赋值与返回值。
我再用图例来学习这个变量会影响到Node的什么属性。
_skewX 每100毫秒增加10度 呈现效果
_skewY 每100毫秒增加10度 呈现效果
再看一下 skewX与 skewY一同变化的效果
不用多说,仔细观察这些变化 就可以知道这两个参数的作用了。
接下来是两个有关锚点的成员变量
Point _anchorPointInPoints; ///< anchor point in points Point _anchorPoint; ///< anchor point normalized (NOT in points) Size _contentSize; ///< untransformed size of the node什么是锚点呢?有过动画制作经验的同学可能了解,在这里小鱼简单提一下,锚点通俗一点理解就是图形在做变形,旋转时候的一个基准点,比如上面的一些gif图形进行x,y轴方向变换时的原点就是它的锚点,上面所有图的锚点都是左下角,这也是cocos2d-x默认的锚点位置。
接下来我们开始研究这三个变量的作用。为什么会有两个锚点的变量来描述这个属性呢?看_anchorPoint 的注释有一名 Not in points 说明这不是描述点的。不能理解,那么我们看一下锚点的get set方法找找线索。
const Point& Node::getAnchorPointInPoints() const { return _anchorPointInPoints; } /// anchorPoint getter const Point& Node::getAnchorPoint() const { return _anchorPoint; } void Node::setAnchorPoint(const Point& point) { #if CC_USE_PHYSICS if (_physicsBody != nullptr && !point.equals(Point::ANCHOR_MIDDLE)) { CCLOG("Node warning: This node has a physics body, the anchor must be in the middle, you cann't change this to other value."); return; } #endif if( ! point.equals(_anchorPoint)) { _anchorPoint = point; _anchorPointInPoints = Point(_contentSize.width * _anchorPoint.x, _contentSize.height * _anchorPoint.y ); _transformUpdated = _transformDirty = _inverseDirty = true; } }
这两个锚点的变量有两个get方法和一个set方法。两个get方法是分别得到这两个变量的,不用多说了,看set方法
set方法接收一个参数point 并且接收的这个参数后面直接赋值给了 _anchorPoint 这个变量,那么也就是说这个set方法可以直接设置 _anchorPoint的值。
在这个方法里面还出现了一个成员变量 _contentSize 顾名思义这个变量描述了两个 note对象的长宽大小
在设置完_anchorPoint的值后 还有一行代码来设置 _anchorPointInPoints变量的值,我们分析一下这行代码
_anchorPointInPoints = Point(_contentSize.width * _anchorPoint.x, _contentSize.height * _anchorPoint.y );
_anchorPointInPoints的值是整体Node的大小乘以_anchorPoint的 x ,y 分量得到的,这说明什么?
很明显,_anchorPointInPoints是具体点的坐标而 _anchorPoint是 锚点在node上x , y分量的比例,如果锚点在node上面那么 _anchorPoint的x,y分量取值范围肯定是[0,1]这间的数值。默认就为0。
小结一下,如果设置node的锚点那么只有一个函数来实现 setAnchorPoint 参数只有一个,含义就是锚点在node上x,y分量的比例。 举个例子,如果要将锚点设置到node的中心只要这一段函数 setAchorPoint( Point(0.5f,0.5f)); 就可以了。
_contentSize的get ,set方法我们就略过了, 值得注意一点 setContentSize 方法中,当改变了_contentSize之后还做了一个操作就是重新更新了锚点的位置也就是重新计算了_anchorPointInPoints这个变量。
这里面出现了一个新碰到的数据类型就是Size,我们简单看一下它的定义
class CC_DLL Size { public: float width; float height;
这个类只有两个成员函数 宽度和长度,来描述一个方形区域的大小尺寸。
接下来的node类成员变量
kmMat4 _modelViewTransform; ///< ModelView transform of the Node.
视图模型的转换矩阵,这个应该是与渲染相关的,等用到这个变量的时候再仔细研究。
下面的一组成员变量很重要,一看就重要。
int _localZOrder; ///< Local order (relative to its siblings) used to sort the node float _globalZOrder; ///< Global order used to sort the node Vector<Node*> _children; ///< array of children nodes Node *_parent; ///< weak reference to parent node int _tag; ///可以给当前的Node对象定义一个 int类型的标识 std::string _name; ///可以给当前Node对象起一个字符串类型的名称。
这几个变量说明Node是一个树的数据结构,除非是根否则每个node对象都有父结点, _parent,每个Node还有一系列子结点 _children 这些子结点用了一个 Vector来存放,并且每个Node对象都有同级子结点(同一个父亲的子结点)的一个Z轴方向的顺序_localZOrder,在在游戏开发中有一个名词叫做深度排序,类似这个意思。
还有一个变量_globalZOrder这是全局的一个深度排序。
通过这几个成员变量,我们可以了解到,node对象上还可以嵌套Node对象,并且这些嵌套的node对象都是当前node的了结点。
查了一下这几个变量相关的方法下面列一下咱们一个一个的分析。
先看向Node对象中增加子对象的方法,这里有三个重载版本的 addChild
首先看下面版本的addChild方法
void Node::addChild(Node *child, int zOrder, int tag) { CCASSERT( child != nullptr, "Argument must be non-nil"); CCASSERT( child->_parent == nullptr, "child already added. It can't be added again"); if (_children.empty()) { this->childrenAlloc(); } this->insertChild(child, zOrder); #if CC_USE_PHYSICS if (child->getPhysicsBody() != nullptr) { child->getPhysicsBody()->setPosition(this->convertToWorldSpace(child->getPosition())); } for (Node* node = this->getParent(); node != nullptr; node = node->getParent()) { if (dynamic_cast<Scene*>(node) != nullptr) { (dynamic_cast<Scene*>(node))->addChildToPhysicsWorld(child); break; } } #endif child->_tag = tag; child->setParent(this); child->setOrderOfArrival(s_globalOrderOfArrival++); if( _running ) { child->onEnter(); // prevent onEnterTransitionDidFinish to be called twice when a node is added in onEnter if (_isTransitionFinished) { child->onEnterTransitionDidFinish(); } } if (_cascadeColorEnabled) { updateCascadeColor(); } if (_cascadeOpacityEnabled) { updateCascadeOpacity(); } }
这里接收了三个参数,子child指针 ,zorder顺序,标识tagID;
先判断了当前结点是的子结点列表是否为空,如果 是空调用了Node类的成员函数 childrenAlloc(); 分配了内存。
然后调用了
this->insertChild(child, zOrder);
这个函数是根据zOrder的顺序将child加入到当前结点的子结点列表里面,我们跟进这个函数看看是怎么实现的。
void Node::insertChild(Node* child, int z) { _reorderChildDirty = true; _children.pushBack(child); child->_setLocalZOrder(z); }
这个insertChild并没有将 _children这个vecort的顺序重新排列,只是将child这个对象加到了_children 列表的最后面。还做了改child的zOrder的设置操作。
注意到这里使用了_reorderChildDirty 这个变量,命名上理解,这个变量是标记着,当前Node对象的子对象列表是否需要重新排列操作,这里的设计很巧妙,在insertChild的时候不必排列一遍结点顺序,小鱼猜,肯定在使用这个结点的时候会根据_reorderChildDirty 这个变量的值来决定这个结点是否需要排列一遍子结点的顺序。于是我在Node类定义中找到了这样的一个方法。
void Node::sortAllChildren() { if( _reorderChildDirty ) { std::sort( std::begin(_children), std::end(_children), nodeComparisonLess ); _reorderChildDirty = false; } }
这个方法是根据子结点的zorder顺序排列所有的子结点
接着看addchild后面的操作,涉及到 CC_USE_PHYSICS 部分的我们先略过,只要知道Node也有支持物理引擎就可以了。
child->_tag = tag; 设置tag值
child->setParent(this); 将新加入的child结点的父结点重新定向为当前结点。
child->setOrderOfArrival(s_globalOrderOfArrival++);这个函数要注意一下, 在这里出现了一个Node类的静态变量 s_globalOrderofArrival 这个变量是标记着系统创建Node对象的总数,在后面的方法中,只对这个变量做了自增的操作,删除Node结点对其值没有影响。
这个s_globalOrderOfArrival++除了记录创建Node对象的总数外还有什么作用呢?
跟进 setorderOfArrival方法
void Node::setOrderOfArrival(int orderOfArrival) { CCASSERT(orderOfArrival >=0, "Invalid orderOfArrival"); _orderOfArrival = orderOfArrival; }
我们可以看到,每个node是用 _orderofArrival来记录它的全局创建的globalId值(s_globalOrderOfArrival) 的。
我们再看 _orderOfArrival的声明。
int _orderOfArrival; ///< used to preserve sequence while sorting children with the same localZOrder
看到这个变量的注释,一切都明白了,这个s_globalOrderofArrival值其实就是给每个Node一个创建时候的全局ID,主要的用途是在做Node z轴深度排序的时候如果两个结点的 localZorder值一样,那么就以_orderofArrival值来做排序。
再看这里面有一个_running的成员变量,这个变量是标记这Node结点是否处于运行状态的,至于什么是运行状态,看到这里还不是很明确,只要知道就是一个状态就可以了。
在running状态下,新加入的子结点会调用一个 onEnter函数,前面提到过,Node类定义了一些事件,这个onEnter就是其中的一个事件,从代码上直接可以理解这个onEnter事件的含义是,当结点被加入到正在运行的结点后就会触发的一个回调函数。
下面还有一个回调事件
// prevent onEnterTransitionDidFinish to be called twice when a node is added in onEnter if (_isTransitionFinished) { child->onEnterTransitionDidFinish(); }
从命名上理解,_isTransitionfinished 是一个标记着Node对象是否做完了变换的一个状态,这里触发了事件的回调函数 onEnterTransitionDidFinish
在addChild声明上有一句注释,如果child加到一个running的node里面那么 onEnter和onEnterTransitionDidFinish会被立即调用。
在addChild的最后又出现两个状态变量和两个函数
if (_cascadeColorEnabled) { updateCascadeColor(); } if (_cascadeOpacityEnabled) { updateCascadeOpacity(); }
这两个变量是标记是否继承父结点的颜色与透明度如果父结点定义了要子结点继承它的颜色与透明设置那么会递归的更新每个子结点的颜色与透明度与父结点一样。
到此,这个addChild方法我们已经看明白了,关于物理方面的东西先放到一边,从后章节我们来分析。
再看看其它两个addChild的重载方法
void Node::addChild(Node *child, int zOrder) { CCASSERT( child != nullptr, "Argument must be non-nil"); this->addChild(child, zOrder, child->_tag); } void Node::addChild(Node *child) { CCASSERT( child != nullptr, "Argument must be non-nil"); this->addChild(child, child->_localZOrder, child->_tag); }
这两个方法很简单,里面主要都是调用了上面我们分析的三个参数的重载addchild方法。
参数中如果没有指定 zOrder则将结点加入到 0 这个位置。
再看几个关于child操作的get方法
/** * 通过tag得到child指针 * * @param tag An identifier to find the child node. * * @return a Node object whose tag equals to the input parameter */ virtual Node * getChildByTag(int tag); /** * 返回_children的引用,与const引用 * * @return the array the node's children */ virtual Vector<Node*>& getChildren() { return _children; } virtual const Vector<Node*>& getChildren() const { return _children; } /** * 返回子结点的数量 * * @return The amount of children. */ virtual ssize_t getChildrenCount() const;
还有一些关于parent的操作,这里代码很简单,看一眼就了解了。
还有一些删除结点的操作,这里我贴一下代码,把说明写到注释中,大家理解一下就可以了。难度不大。
/** * 将当前结点从它的父结点中清除,并且调用了clean方法做了清除,clean方法是什么后面我们有分析。. */ virtual void removeFromParent(); /** 同上面的removeFromParent方法,有一个参数可以指定是否调用 clean方法做清除 */ virtual void removeFromParentAndCleanup(bool cleanup); /** * 通过 child的指针来删除一个指定的结点 */ virtual void removeChild(Node* child, bool cleanup = true); /** * 通过tag值来删除一个子结点 */ virtual void removeChildByTag(int tag, bool cleanup = true); /** * 清除所有的子结点并且所有的子结点都会调用 cleanup 方法 */ virtual void removeAllChildren(); /** * 清除所有子结点,根据cleanup的参数设置来决定是否调用cleanup方法 (这里出现了多次cleanup这个函数后面我们重点分析一下源码) */ virtual void removeAllChildrenWithCleanup(bool cleanup); /**
* 这个函数干了几件事,
* 1. 设置了_reorderChildDirty = true 设置当前结点的子结点进行排序
* 2. 重新更新了child的 _orderOfArrival 值为最新的
* 3. 设置 child的 _localZOrder值为 localZOrder */ virtual void reorderChild(Node * child, int localZOrder); /** * 根据 子结点的的 _localZOrder排列当前结点所有的child ,有了这个方法不用每加一个结点或者改变某个子结点的zOrder就排一次,这个排列方法要在结点显示到屏幕之前调用。
*/ virtual void sortAllChildren(); /** * tag的get set 方法 */ virtual int getTag() const; virtual void setTag(int tag);
上面几个方法都很简单,大家可以结合代码还有我的解释来理解。
这里还有一个变量不得不提及一下
// XXX: Yes, nodes might have a sort problem once every 15 days if the game runs at 60 FPS and each frame sprites are reordered. int Node::s_globalOrderOfArrival = 1;
还是这个结点的全局计数,在 reorderChild方法中,会给子结点更新s_globalOrderOfArrival值,而globalOrderOfArrival值是一个只增不减的变量,还是个int类型的,所以globalOrderOfArrival值会有被用完的时候,这个变量在定义的时候有注释说明了这一点。
如果程序以每秒60帧,每帧都调用reordered的速率运行15天,Node结点排序就会出现问题。
这里确实有这样的问题,不过大家也不用担心,又不是服务器程序,连续15天不停的运行基本不会发生这样的情况的,程序重新启动这个变量又从1开始了。
上面几个child的操作中多次提及到了 cleanup(); 操作,下面我们来分析下cleanup的源码。
void Node::cleanup() { // actions this->stopAllActions(); this->unscheduleAllSelectors(); #if CC_ENABLE_SCRIPT_BINDING if ( _scriptType != kScriptTypeNone) { int action = kNodeOnCleanup; BasicScriptData data(this,(void*)&action); ScriptEvent scriptEvent(kNodeEvent,(void*)&data); ScriptEngineManager::getInstance()->getScriptEngine()->sendEvent(&scriptEvent); } #endif // #if CC_ENABLE_SCRIPT_BINDING // timers for( const auto &child: _children) child->cleanup(); }
这个cleanup其实就干了四件事
这就是清理工作。
现在回到我们分析Node类的主线主,继续看Node类的成员变量,在上面代码分析中部分成员变量已经做了分析下面就不再赘述。
void *_userData; ///< A user assingned void pointer, Can be point to any cpp object Ref *_userObject; ///< A user assigned Object
通过注释,和命名分析,这再从个函数是Node类给开发者预留的再从个数据接口,说白一点就是,如果你觉得Node类哪些功能还不够用,或者它与哪些对象要进行绑定,在不使用继承的方式下,可以将你自定义的数据挂靠在Node类的 _userData或 _userObject指针上面。
针对这再从个变量的get set方法很简单,大家自行看下代码。
GLProgram *_shaderProgram; ///< OpenGL shader Scheduler *_scheduler; ///< scheduler used to schedule timers and updates ActionManager *_actionManager; ///< a pointer to ActionManager singleton, which is used to handle all the actions EventDispatcher* _eventDispatcher; ///< event dispatcher used to dispatch all kinds of events
这几个成员变量,从命名上就可以了解它的作用,
OpenGl的shader控制对象
Scheduler 调度程序控制
ActionManager 动作管理器
EventDispatcher 事件分发器
这些对象的具体工作原理我们在这里不做深入讨论,只要知道Node类偶合了这些东西就可以了。
还有一个很重要的Node类的属性
bool _visible; ///< is this node visible
描述结点是否可见,如果隐藏一个结点的显示可以调用关于这个变量的get set方法,判断一个结点是否被显示 可以调用 isVisible方法来查询。
最后几个是来描述结点的透明属性的方法,这里不多说大家了解一下就可以了。
// opacity controls GLubyte _displayedOpacity; GLubyte _realOpacity; Color3B _displayedColor; Color3B _realColor;
至此,我们已经完整的了解 了Node类的所有属性,基本明白了Node是个什么玩意,它的地位肯定是Cocos2d-x里很重要的,因为它是就是用来操作显示对象的一个基类。
成员变量看完了,我们来看一下Node类提供的方法都有哪些,上面分析成员变量的时候已经分析了大部分的方法,下面我们看看剩下的方法都用来做什么的。
先从Node结点的创建方法函数开始。
Node::Node(void) : _rotationX(0.0f) , _rotationY(0.0f) , _rotationZ_X(0.0f) , _rotationZ_Y(0.0f) , _scaleX(1.0f) , _scaleY(1.0f) , _scaleZ(1.0f) , _positionZ(0.0f) , _position(Point::ZERO) , _skewX(0.0f) , _skewY(0.0f) , _anchorPointInPoints(Point::ZERO) , _anchorPoint(Point::ZERO) , _contentSize(Size::ZERO) , _useAdditionalTransform(false) , _transformDirty(true) , _inverseDirty(true) , _transformUpdated(true) // children (lazy allocs) // lazy alloc , _localZOrder(0) , _globalZOrder(0) , _parent(nullptr) // "whole screen" objects. like Scenes and Layers, should set _ignoreAnchorPointForPosition to true , _tag(Node::INVALID_TAG) // userData is always inited as nil , _userData(nullptr) , _userObject(nullptr) , _shaderProgram(nullptr) , _orderOfArrival(0) , _running(false) , _visible(true) , _ignoreAnchorPointForPosition(false) , _reorderChildDirty(false) , _isTransitionFinished(false) #if CC_ENABLE_SCRIPT_BINDING , _updateScriptHandler(0) #endif , _componentContainer(nullptr) #if CC_USE_PHYSICS , _physicsBody(nullptr) #endif , _displayedOpacity(255) , _realOpacity(255) , _displayedColor(Color3B::WHITE) , _realColor(Color3B::WHITE) , _cascadeColorEnabled(false) , _cascadeOpacityEnabled(false) { // set default scheduler and actionManager Director *director = Director::getInstance(); _actionManager = director->getActionManager(); _actionManager->retain(); _scheduler = director->getScheduler(); _scheduler->retain(); _eventDispatcher = director->getEventDispatcher(); _eventDispatcher->retain(); #if CC_ENABLE_SCRIPT_BINDING ScriptEngineProtocol* engine = ScriptEngineManager::getInstance()->getScriptEngine(); _scriptType = engine != nullptr ? engine->getScriptType() : kScriptTypeNone; #endif kmMat4Identity(&_transform); kmMat4Identity(&_inverse); kmMat4Identity(&_additionalTransform); }
1. 初始化列表里一些变量的默认值,简单过一眼,后面拿到了 Director实例
2 . 将Director的 动作管理器,定时器,事件分发器的指针都赋值给了这个Node结点并且增加了一次引用 计数。
3. 脚本相关变量的初始化
4. 初始化了一些矩阵(现在可以不了解这些矩阵的具体用处,只知道有这么一回事就行了)。
Node::~Node() { CCLOGINFO( "deallocing Node: %p - tag: %i", this, _tag ); #if CC_ENABLE_SCRIPT_BINDING if (_updateScriptHandler) { ScriptEngineManager::getInstance()->getScriptEngine()->removeScriptHandler(_updateScriptHandler); } #endif // User object has to be released before others, since userObject may have a weak reference of this node // It may invoke `node->stopAllAction();` while `_actionManager` is null if the next line is after `CC_SAFE_RELEASE_NULL(_actionManager)`. CC_SAFE_RELEASE_NULL(_userObject); // attributes CC_SAFE_RELEASE_NULL(_shaderProgram); for (auto& child : _children) { child->_parent = nullptr; } removeAllComponents(); CC_SAFE_DELETE(_componentContainer); #if CC_USE_PHYSICS setPhysicsBody(nullptr); #endif CC_SAFE_RELEASE_NULL(_actionManager); CC_SAFE_RELEASE_NULL(_scheduler); _eventDispatcher->removeEventListenersForTarget(this); #if CC_NODE_DEBUG_VERIFY_EVENT_LISTENERS && COCOS2D_DEBUG > 0 _eventDispatcher->debugCheckNodeHasNoEventListenersOnDestruction(this); #endif CCASSERT(!_running, "Node still marked as running on node destruction! Was base class onExit() called in derived class onExit() implementations?"); CC_SAFE_RELEASE(_eventDispatcher); }
大家可以仔细看一下,析构中,清理了一些指针如 _userObject、_shaderprogram、
还清理了components 这个components是什么呢,我们先只了解到命名程序,后续章节来单独分析它,这里只知道清理了Node的组件就可以了。
在做这些指针的清理工作时用到了一些宏定义 如 CC_SAFE_RELEASE_NULL , CC_SAFE_RELEASE 等,大家跟一下代码,其实没什么深奥的就是判断指针是不是空,不空就做 release或者 free 等操作。
值得提及的是在Node的类定义文件中有一行代码。
private: CC_DISALLOW_COPY_AND_ASSIGN(Node);
这个宏定义是将Node类的拷贝构造函数和赋值运算符重载定义成了私有方法。
这说明,在针对Node操作的时候不能进行赋值运算,Node做参数的时候要以指针的形式来传递参数。
下面我们看一下Node类最重要的一个创建对象方法create方法
Node * Node::create(void) { Node * ret = new Node(); if (ret && ret->init()) { ret->autorelease(); } else { CC_SAFE_DELETE(ret); } return ret; }
这个create方法很简单,
1. new一个Node对象
2. 调用Node的init方法进行初始化。
3. 设置Node 为自动内存释放。
Cocos2d-x推荐我们作用create方法来创建结点,而不要用new的方式创建。我们在应用的时候应该尊重这条建议。
create方法里面提供到了一个init方法,下面我们跟进源码,看一下init都干了些什么。
virtual bool init();
bool Node::init() { return true; }
看到了这么干净的init方法,里面就一行代码。怎么回事呢?
我们看一下init方法的声明为 virtual 所以这个方法是给它的子类使用的,不同的子类有不同的init过程 所以这里只是预留了一个初始化的接口,在你的类中如果有自己的特殊的初始化过程那么重载这个init就可以了。
Node的创建方法我们就分析到这里,下面我们继续看Node类有哪些方法还没有查看过。
/** * Override this method to draw your own node. * The following GL states will be enabled by default: * - `glEnableClientState(GL_VERTEX_ARRAY);` * - `glEnableClientState(GL_COLOR_ARRAY);` * - `glEnableClientState(GL_TEXTURE_COORD_ARRAY);` * - `glEnable(GL_TEXTURE_2D);` * AND YOU SHOULD NOT DISABLE THEM AFTER DRAWING YOUR NODE * But if you enable any other GL state, you should disable it after drawing your node. */ virtual void draw(Renderer *renderer, const kmMat4& transform, bool transformUpdated); virtual void draw() final;
我们看到了有再从个重载的draw方法, 大家 跟一下代码可以看到这两个方法基本是一个空的实现,注释上已经说的很清楚了,在你自己的node实现中重载第一个方法来用OpenGL绘制自己的node
第二个不带参数的方法是不能被重载的,大家注意一下这一点.
接下来是两个visit方法
/** * Visits this node's children and draw them recursively. */ virtual void visit(Renderer *renderer, const kmMat4& parentTransform, bool parentTransformUpdated); virtual void visit() final;
和上面draw的定义结构很像,我们看一个带参数的visit的定义 .
void Node::visit(Renderer* renderer, const kmMat4 &parentTransform, bool parentTransformUpdated) { // quick return if not visible. children won't be drawn. if (!_visible) { return; } bool dirty = _transformUpdated || parentTransformUpdated; if(dirty) _modelViewTransform = this->transform(parentTransform); _transformUpdated = false; // IMPORTANT: // To ease the migration to v3.0, we still support the kmGL stack, // but it is deprecated and your code should not rely on it kmGLPushMatrix(); kmGLLoadMatrix(&_modelViewTransform); int i = 0; if(!_children.empty()) { sortAllChildren(); // draw children zOrder < 0 for( ; i < _children.size(); i++ ) { auto node = _children.at(i); if ( node && node->_localZOrder < 0 ) node->visit(renderer, _modelViewTransform, dirty); else break; } // self draw this->draw(renderer, _modelViewTransform, dirty); for(auto it=_children.cbegin()+i; it != _children.cend(); ++it) (*it)->visit(renderer, _modelViewTransform, dirty); } else { this->draw(renderer, _modelViewTransform, dirty); } // reset for next frame _orderOfArrival = 0; kmGLPopMatrix(); }
这里面代码还挺多,我们简单分析一下其实就干了这么几件事
小结一下:
node结点具体怎么来渲染使用的是 draw方法,
如果要显示一个node结点及其子结点那么我们就要使用visit方法,这样可以递归的按照zOrder顺序地draw每一个子结点。
下来还有一些与定时器有关的方法,这些方法的具体使用及功能实现,我们在单独分析schedule类的时候再合并讨论。
还有一些小的功能函数,我贴一下源码,大家简单做了 解
void Node::resume() { _scheduler->resumeTarget(this); _actionManager->resumeTarget(this); _eventDispatcher->resumeEventListenersForTarget(this); } void Node::pause() { _scheduler->pauseTarget(this); _actionManager->pauseTarget(this); _eventDispatcher->pauseEventListenersForTarget(this); }
暂停与恢复暂停
从函数过程看,暂停主要是暂时暂停了 计时器,动作,事件,在手机游戏 进入到后台运行的时候,我们会调用 这些方法。
好,到这里,我们把Node类的源码基本分析完了,在这里小鱼做一个小小的总结,帮大家回顾一下上面都啰嗦了些什么。
好啦,node类我们分析到这,不过在本章节开始,在我们看ccnode.h文件时,说过这个文件有两个类,现在我们看一下另一个类是个什么东西。
class CC_DLL __NodeRGBA : public Node, public __RGBAProtocol { public: // overrides virtual GLubyte getOpacity() const override { return Node::getOpacity(); } virtual GLubyte getDisplayedOpacity() const override { return Node::getDisplayedOpacity(); } virtual void setOpacity(GLubyte opacity) override { return Node::setOpacity(opacity); } virtual void updateDisplayedOpacity(GLubyte parentOpacity) override { return Node::updateDisplayedOpacity(parentOpacity); } virtual bool isCascadeOpacityEnabled() const override { return Node::isCascadeOpacityEnabled(); } virtual void setCascadeOpacityEnabled(bool cascadeOpacityEnabled) override { return Node::setCascadeOpacityEnabled(cascadeOpacityEnabled); } virtual const Color3B& getColor(void) const override { return Node::getColor(); } virtual const Color3B& getDisplayedColor() const override { return Node::getDisplayedColor(); } virtual void setColor(const Color3B& color) override { return Node::setColor(color); } virtual void updateDisplayedColor(const Color3B& parentColor) override { return Node::updateDisplayedColor(parentColor); } virtual bool isCascadeColorEnabled() const override { return Node::isCascadeColorEnabled(); } virtual void setCascadeColorEnabled(bool cascadeColorEnabled) override { return Node::setCascadeColorEnabled(cascadeColorEnabled); } virtual void setOpacityModifyRGB(bool bValue) override { return Node::setOpacityModifyRGB(bValue); } virtual bool isOpacityModifyRGB() const override { return Node::isOpacityModifyRGB(); } protected: __NodeRGBA(); virtual ~__NodeRGBA() {} private: CC_DISALLOW_COPY_AND_ASSIGN(__NodeRGBA); };
这个类是一个__NodeRGBA类,从命名上看是带RGB颜色的Node类,里面还继承了一个抽象类,__RGBAProtocol
大家可以看一下__RGBAProtocol这个类,里面全是纯虚函数,也就是一个接口类,而__NodeRGBA类实现的这些接口其实都是调用 了Node基类的方法。
我们又看到__NodeRGBA的构造函数是 protected保护模式的,又没有提供create方法来创建这个类的对象,通过这些我们可以分析到,现在这个版本的node类已经集成了__NodeRGBA了。
我在整个工程里面搜索了一下 __NodeRGBA 基本没有一 个地方用到过这个类,这样我们可以放心 的不去理会__NodeRGBA这个类了,它已经没有用了。
今天这章节有点长啊!!因为Node类确实是一个重量级的人物,我们分析了它的源码,涉及到 物理,实时器,动作,矩阵方面的内容小鱼暂时一笔带过,因为在看源码的时候 切忌一根筋的往里看,要使用广度优先的方式,从全局的角度来分析。这些内容我们暂时只要知道是干什么的,具体怎么干,等有了大局观再逐个击破。
我们不着急看Node类的一些重要的子类如 Scene Layer等,下一节,我们来分析 Scheduler 类,了解一下Cocos2d-x的定时器调度是怎么实现的,它怎么使用。