精通Cocos2d-x游戏开发(基础卷)
上QQ阅读APP看书,第一时间看更新

6.3 节点和节点

在Cocos2d-x中,Node与Node之间,存在两种直接关系,第一种是父子关系,这是一种上下级的关系,游戏中存在很多的层,但每个Node只有一个父节点。第二种就是兄弟关系,它们有着同一个父节点,是一种平级的关系。

节点与节点的关系是可以不断变化的,可以通过Node提供的接口来组织节点的父子关系,以及节点的搜索查找。

6.3.1 添加子节点

Cocos2d-x可以通过以下方法来添加节点:

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);

默认情况下,localZOrder为0,tag为–1,name为"",当调用Node的addChild方法时,最终会调用addChildHelper来添加子节点(具体视版本而定),如图6-6所示为添加子节点的流程。

图6-6 添加子节点

❏添加子节点:传入Node、ZOrder、Tag和Name。

❏当第一次添加子节点的时候,默认为分配4个节点的空间到节点容器中,并对子节点执行retain操作。

❏将子节点插入到节点容器中,并设置子节点的_localZOrder(并没有对子节点进行排序)。

❏设置子节点的Tag或Name,设置子节点的Parent为this。

❏如果父节点是一个已经被添加到当前场景中的running节点,依次调用子节点的onEnter和onEnterTransitionDidFinish。

❏如果开启了_cascadeColorEnabled或_cascadeOpacityEnabled,对子节点的颜色或透明度进行更新。

❏当父节点已经被添加到场景中(父节点的onEnter已经执行完毕),此时父节点的addChild才会执行子节点的onEnter,否则需要将父节点被添加到场景中时才会执行onEnter和onEnterTransitionDidFinish。

setCascadeColorEnabled和setCascadeOpacityEnabled可以开启颜色和透明度的瀑布模式,早期的设置颜色和透明度并没有传递到子节点的功能,当希望一个对象慢慢变透明时,需要对其所有子节点都执行这个操作,瀑布模式下,对父节点设置颜色和透明度会自动递归执行到所有子节点身上。

6.3.2 删除节点

Cocos2d-x可以通过以下方法来删除节点。

//自己从父节点中移除,并执行cleanup
virtual void removeFromParent();
//自己从父节点中移除,并根据cleanup参数来判断执行cleanup
virtual void removeFromParentAndCleanup(bool cleanup);
//移除传入的子节点对象,并根据cleanup参数来判断执行子节点的cleanup
virtual void removeChild(Node* child, bool cleanup = true);
//移除传入指定Tag的子节点对象,并根据cleanup参数来判断执行子节点的cleanup
virtual void removeChildByTag(int tag, bool cleanup = true);
//移除传入指定名字的子节点对象,并根据cleanup参数来判断执行子节点的cleanup
virtual void removeChildByName(const std::string &name, bool cleanup = 
true);
//删除该节点的所有子节点,并执行子节点的cleanup
virtual void removeAllChildren();
//删除该节点的所有子节点,并根据cleanup参数来判断执行子节点的cleanup
virtual void removeAllChildrenWithCleanup(bool cleanup);

我们可以删除一个或多个子节点,也可以将节点从父节点中删除,具体的删除流程如图6-7所示。

图6-7 删除节点

❏删除子节点:传入Node / tag / name,以及是否执行清除的cleanup变量。

❏查找定位子节点,进行简单判断,判断通过后取出要删除的子节点。

❏如果m_bIsRunning为true,则依次调用子节点的onExitTransitionDidStart以及OnExit方法。

❏如果传入的cleanup变量为true,则会调用子节点的cleanup方法,cleanup方法将停止该节点以及所有子节点的所有Action和Schedule。

❏将子节点的父节点属性设置为NULL。

❏将子节点从子节点容器中移除,并调用子节点的Release方法。

6.3.3 节点查询

Node提供了一些简单的方法可以在节点树中快速查找节点:

//获取指定tag的子节点
virtual Node * getChildByTag(int tag) const;
//获取指定名字的子节点
virtual Node* getChildByName(const std::string& name) const;
//按照正则表达式进行查找并调用回调
virtual void enumerateChildren(const std::string &name, std::function<bool 
(Node* node)> callback) const;
//获取所有的子节点
virtual Vector<Node*>& getChildren();
//获取当前节点的父节点
virtual Node* getParent();

当有多个相同Tag的节点或多个名字相同的节点时,返回找到的第一个子节点,子节点数组默认是按照插入顺序进行排序的。

Tag和Name在addChild方法中不能同时设置,但在addChild方法之后可以手动设置。

enumerateChildren是一个通过查询节点名字,对节点树进行批处理的函数,通过传入需要匹配的节点表达式,自动遍历并回调callback,将匹配到的节点传入callback,在callback中对符合条件过滤出来的节点进行处理。return true则表示完成批量处理,结束遍历,return false则会让enumerateChildren继续遍历。enumerateChildren使用C++11的正则表达式来匹配子节点,表达式并不支持unicode。

enumerateChildren的搜索字符串由搜索通配符和正则表达式组成,通配符描述了搜索的方向,正则表达式描述了节点名字的匹配规则,关于正则表达式的规则,在第5章中有详细介绍。enumerateChildren包含3个搜索通配符。

❏//:递归搜索所有的子节点,该通配符只能放在字符串的最前面。

❏..:搜索当前节点的父节点,只能放在字符串的最后,并且前接/。/..会被替换为"[[:alnum:]]+/"插入到搜索字符串最前面,且不能重复拼接,如/../../..。

❏/:搜索当前节点的子节点,可以放在除字符串开头之外的任何位置。

下面是一些搜索字符串的拼写规则。

❏enumerateChildren("//MyName",...):使用了递归搜索通配符//,递归搜索当前节点的所有子节点,找出所有名字为MyName的节点。

❏enumerateChildren("[[:alnum:]]+",...):没有使用搜索通配符,遍历搜索当前节点下的所有子节点,包括名字为""的节点。

❏enumerateChildren("A[[:digit:]]",...):没有使用搜索通配符,搜索名字为A后跟一个数字的子节点,如A0~A9。

❏enumerateChildren("Abby/Normal",...):使用了子节点搜索通配符/,搜索所有名为Abby的子节点,并在该子节点下搜索所有名为Normal的子节点。

❏enumerateChildren("//Abby/Normal",...):使用了递归搜索//和子节点搜索通配符/,递归搜索当前节点下所有名为Normal,并且父亲为Abby的节点。

❏enumerateChildren("node[[:digit:]]+",...):没有使用搜索通配符,搜索名字为node后跟若干数字的子节点,如node100,node99。

❏enumerateChildren("node/..",...):使用了子节点搜索/和父节点搜索通配符..,搜索节点树下名为node的孙子节点,传入孙子节点到回调中。

注意:大量的正则匹配执行起来,效率并不高。

6.3.4 节点之间的空间变换

Cocos2d-x使用OpenGL的右手坐标系,在屏幕中x向右、y向上、z向外,而在游戏中,存在两种类型的空间,即世界空间和节点空间。世界空间是OpenGL的一个全局、绝对坐标系,以屏幕的左下角为原点,可以用于描述触屏点击的位置以及辅助空间坐标转换。所有的节点空间都可以被转换为世界空间。

节点空间是节点本地的坐标系,是一个相对坐标系,每个节点的位置都是相对于父节点坐标系的,随着父节点的位置变化而变化,保持相对关系。在节点空间中,影响节点位置的,有3个外部因素;分别是父节点的位置、父节点的锚点和父节点的ContentSize

当节点位置为(0,0)时,是处于父节点坐标系的原点,父节点坐标系的原点位于父节点Content的左下角。当父节点是一张图片时,原点是图片的左下角,当父节点是一个空的节点时,原点等于父节点所处的位置。父节点的大小是基于ContentSize进行计算的,通过setContentSize也可以随意设置节点的大小。

父节点的位置可以直接影响到子节点的位置,而在ContentSize不为0时,锚点的位置也可以通过影响父节点自身的Content位置来影响子节点的位置。

使用下面几个函数可以在节点空间和世界空间中进行转换(在Cocos2d-x 3.0之后CCPoint换成了Vec2,但本质是一样的)。

//将世界空间坐标系转换到节点坐标系
CCPoint convertToNodeSpace(const CCPoint& worldPoint);
//将节点空间坐标系转换到世界坐标系
CCPoint convertToWorldSpace(const CCPoint& nodePoint);
//将世界空间坐标系转换到节点空间坐标系与锚点相对的点
CCPoint convertToNodeSpaceAR(const CCPoint& worldPoint);
//将节点空间坐标系转换到世界空间坐标系与锚点相对的点
CCPoint convertToWorldSpaceAR(const CCPoint& nodePoint);

上面的4个函数用于在不同的坐标系进行点的转换,后面带AR的函数在转换的时候,是考虑了锚点的位置的。在两个坐标系进行转换时,是经过一次遍历,将节点的所有父节点都考虑在内,将每一层的空间矩阵进行计算,最后得出结果。

空间转换函数中,世界坐标的原点为屏幕的左下角,而节点坐标系的原点是节点内容的左下角,当调用的函数后面带AR的时候,节点坐标系的原点相当于锚点所在的位置。不带AR和带AR的区别是,一个以节点内容左下角为原点,不考虑锚点;而另一个以锚点所在位置为原点。具体的坐标转换可以参考图6-8所示。

图6-8 坐标转换

当调用convertToWorldSpace从节点空间坐标系转换到世界空间坐标系时,调用getNodeToWorldTransform一层层往上计算矩阵,然后依次相乘矩阵,最终得到节点空间坐标系转换到世界空间坐标系的矩阵,然后将传入的坐标点使用该矩阵计算后返回。

当调用convertToWorldSpaceAR将节点空间坐标系的坐标转换到世界空间坐标系时,先将坐标加上锚点在节点中的位置_anchorPointInPoints,再执行convertToWorldSpace。

当调用convertToNodeSpace将世界空间坐标系的坐标转换到节点空间坐标系时,先调用getNodeToWorldTransform获得节点到世界坐标系的矩阵,然后调用矩阵的getInversed方法求出其逆矩阵,也就是世界空间坐标系到节点空间坐标系的矩阵,然后将传入的坐标点使用该矩阵计算后返回。

当调用convertToNodeSpaceAR将世界空间坐标系的坐标转换到节点空间坐标系时,先将输入点减去锚点在节点中的位置_anchorPointInPoints,再执行convertToNodeSpace。

//假设sprite的纹理是50*50像素的图片,锚点默认为(0.5 0.5),直接添加在场景下,场景
位置为(0,0)
sprite->setPosition(ccp(75, 75));
//将世界空间坐标系 (50, 50) 的位置转换到默认的节点空间坐标系,结果为 (0, 0)
cout<<sprite->convertToNodeSpace(ccp(50,50));
//将世界空间坐标系 (75, 75) 的位置转换到以锚点为原点的节点坐标系,结果为 (0, 0)
cout<<sprite->convertToNodeSpaceAR(ccp(75,75));
//将默认的节点空间坐标系坐标的 (0, 0) 转换到世界空间坐标系中,结果为 (50, 50)
cout<<sprite->convertToWorldSpace(ccp(0,0));
//将以锚点为原点的节点空间坐标系的 (0, 0) 转换到世界空间坐标系中,结果为 (75, 75)
cout<<sprite->convertToWorldSpaceAR(ccp(0,0));