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

6.1 节点的表现

节点包含了游戏对象最基础的属性,这些属性决定了游戏对象如何表现(位置、旋转和缩放),Node为它们提供了对应的get、set方法来操作。

6.1.1 位置

位置描述了一个节点在当前坐标系下的坐标,我们使用的是OpenGL坐标系,也就是右手坐标系。D3D使用的是左手坐标系,左右手坐标系的区别主要是Z轴的正负是相反的(坐标系中每个轴的朝向并不是绝对的而是相对的,当整个坐标系围绕Y轴旋转180°,左手坐标系的Z轴也可以朝向屏幕外,但它仍然是左手坐标系)。右手坐标系如图6-1所示。

图6-1 右手坐标系

Node主要提供了以下变量来存储节点的位置:

//节点的X、Y坐标
Vec2 _position;
//节点的Z坐标
float _positionZ;
//节点的标量化X、Y坐标
Vec2 _normalizedPosition;

_position表示相对于父节点的二维坐标X和Y,_positionZ用于描述节点在三维坐标系下的Z轴位置。一系列重载的getPosition和setPosition方法可以操作它们。

_normalizedPosition表示相对于其父节点ContentSize的百分比位置,X和Y为0时表示其父节点的左下角,为1表示右上角,Node会自动计算其相对坐标。调用setNormalizedPosition可以进行设置(先设置标量化坐标,再添加父节点,这样的执行顺序并不影响其效果,移除并添加到另外一个父节点下,也会显示正确的效果,这得益于其延迟计算的机制)。

在Cocos2d-x中,X和Y为0时的原点为屏幕的左下角。

6.1.2 旋转和倾斜

Node支持围绕X、Y、Z轴的旋转,Node的旋转需要划分为2D和3D两种情况,getRotation和setRotation方法用于2D的旋转。2D旋转围绕锚点的位置为圆心进行旋转,传入一个float类型的变量表示旋转的角度,90.0f即为90°。输入正数可以让节点顺时针旋转,输入负数可以让节点逆时针旋转,setRotation方法会将变量_rotationZ_X和_rotationZ_Y同时进行修改,赋值为传入的角度。如图6-2所示,演示了三个锚点不同的Sprite使用setRotation旋转90°的结果,图中的小圆点为锚点位置。

图6-2 旋转节点

//基于X轴的旋转角度
float _rotationX;
//基于Y轴的旋转角度
float _rotationY;
//基于Z轴的旋转角度,当用于描述旋转倾斜时,表示X轴上的倾斜分量
float _rotationZ_X;
//基于Z轴的旋转角度,当用于描述旋转倾斜时,表示Y轴上的倾斜分量
float _rotationZ_Y;
//基于X轴的倾斜角度
float _skewX;
//基于Y轴的倾斜角度
float _skewY;

然后来看一下旋转对子节点的影响,在这里按照A->B->C的顺序构造父子节点(A为根节点),并且每个节点都旋转90°,从图6-3中可以看到,父节点旋转,子节点也跟着父节点的锚点旋转,所以A旋转90°,到了B就是180°的旋转,而C就旋转了270°。

图6-3 旋转子节点

setRotation3D通过传入一个包含3个轴的旋转角度的Vec3参数,来实现3D的旋转,并且会同时修改_rotationX、_rotationY、_rotationZ_X和_rotationZ_Y属性。

setRotationX、setRotationY、setRotationSkewX、setRotationSkewY、setSkewX、setSkewY函数和SkewTo、SkewBy等动作,可以对Node进行扭曲,该操作将以锚点为原点,顺着X、Y轴倾斜一定的角度。如图6-4和图6-5所示,展示了setSkewX 45°和setSkewY 45°的效果。

图6-4 SetSkewX(45.0f)

图6-5 SetSkewY(45.0f)

通过OrbitCamera可以控制Node围绕X轴和Y轴进行旋转,可以形成叶子在空中翻转的效果。

6.1.3 缩放

Node支持基于X、Y、Z轴的缩放,缩放是基于锚点的缩放,通过一系列的getScale和setScale方法可以操作它们。

//对象在X轴上的缩放,默认值为1.0
float _scaleX;
//对象在Y轴上的缩放,默认值为1.0
float _scaleY;
//对象在Z轴上的缩放,默认值为1.0
float _scaleZ;

6.1.4 锚点Anchor

锚点是用于辅助位置摆放、旋转和缩放的一个点,其像一个钉子一样钉在节点上。锚点的取值是按照百分比来设定的,默认为(0,0)表示节点的左下角,(1,1)表示节点的右上角,但取值的范围并不限制(锚点可以在节点之外),默认值在正中间,取值(0.5, 0.5)。

//锚点的值,用于描述锚点的相对位置
Vec2 _anchorPoint;
//锚点的值乘以节点大小ContentSize计算出来在节点坐标系的实际位置
Vec2 _anchorPointInPoints;
//是否忽略锚点,如果是则默认锚点为(0,0)位置
bool _ignoreAnchorPointForPosition;

调用ignoreAnchorPointForPosition方法传入true可以忽略锚点,忽略锚点的情况下,会将锚点视为0来处理,不管锚点的值是多少。但这在某些版本的某种情况下可能会出现部分失效的BUG,导致anchorPointInPoints的值并没有被修改,并继续参与计算。所以在ignoreAnchorPointForPosition之后,可以手动将锚点设置为0来解决这样的问题。

6.1.5 渲染顺序ZOrder

渲染顺序决定了对象渲染的先后顺序,通过渲染顺序可以控制显示对象的遮挡关系,除了可以控制显示对象的遮挡关系之外,渲染顺序还可以有效优化游戏渲染的效率,这将在第15章中介绍到。渲染顺序由全局ZOrder和局部ZOrder决定,它们之间有什么关联呢?

ZOrder值越大,渲染出来就越靠前,局部ZOrder决定了在同一个父节点下的子节点之间的渲染顺序。而全局ZOrder可以决定在整个场景中的渲染顺序。

与ZOrder相关的成员变量如下:

//用于在节点空间内对子节点进行排序的局部ZOrder
int _localZOrder;
//用于对所有节点进行排序的全局ZOrder
float _globalZOrder;
//这是一个无限递增的变量,用于帮助在节点下_localZOrder相等的子节点进行排序
int _orderOfArrival;
//当子节点的_localZOrder发生了变化时,会变为true,以方便判断是否需要重新对子节点进行排序
bool _reorderChildDirty;

Node根据ZOrder值按照从小到大的顺序进行渲染,ZOrder值小的节点会被ZOrder值大的节点遮挡,谁先谁后,谁遮挡谁,是ZOrder要解决的核心问题。

在大量对象进行穿插的时候(如一群人在走动,可能出现某人的脚遮挡住某人的头部),就需要靠ZOrder来正确地显示遮挡关系。可以根据运动对象的Y轴坐标来设置ZOrder值,常用的做法是为这些对象设置ZOrder,添加到同一个节点下进行管理,ZOrder设置为MaxY - Node::PositionY,在对象的Y进行更新时,同时更新其ZOrder。

在实际开发中,需要根据游戏本身的特点来设计适合自己游戏的ZOrder相关的规则(如果担心对ZOrder的更新太频繁,可以考虑设置最小移动间隔,当Y轴变化到一定的程度才进行更新)。

全局ZOrder的应用非常灵活,可以以节点为单位自由地左右渲染顺序,解决了本地ZOrder难以解决的跨节点渲染顺序调整。因为局部ZOrder只关心在该节点下,子节点的渲染顺序,而全局ZOrder允许控制嵌套关系很复杂的节点之间的渲染顺序。所有节点默认的_globalZOrder都是0,这时完全按照节点树自身的_localZOrder进行渲染,设置了_globalZOrder的节点及其所有子节点都会被调整到新的渲染顺序。

通过设置全局ZOrder,可以实现将某个节点设置到最顶层或者最底层显示,或者将另外一个节点插入到一个节点的两个子节点中间进行显示(将任意一个节点A插入到兄弟节点B1和B2之间)。

注意:在Cocos2d-x 3.0~3.3的版本中,全局ZOrder的管理和排序是在EventDispatcher中实现的,其实不是很合理,EventDispatcher的职责应该是事件分发,而全局ZOrder是渲染相关的逻辑,可以交由一个专门处理渲染相关逻辑的类来做,而EventDispatcher实现事件分发功能和事件优先级,在Node和EventDispatcher中间再封装一层进行解耦。

6.1.6 尺寸

Node默认是没有尺寸的,默认大小为0,但Node封装了尺寸的概念,因为其也是一个非常基础、公共的特性,而调用setContentSize为Node设置尺寸,也可以将Node作为一个无形的区域来辅助碰撞检测。而ContentSize也影响着锚点的具体位置。

//节点的大小,没有经过缩放的原始尺寸
Size _contentSize;
//节点的大小是否改变
bool _contentSizeDirty;

getBoundingBox 可以计算出节点的实际大小,并用于碰撞检测(在同一帧中,一个节点getBoundingBox如果需要被多次调用,最好将其结果缓存起来多次使用,而不是多次调用),也可以直接用下面的代码手动计算矩形。

Rect( _position.x - _contentSize.width * _anchorPoint.x,
                    _position.y - _contentSize.height * _anchorPoint.y,
                    _contentSize.width, _contentSize.height);

6.1.7 懒惰计算

懒惰计算是修改Node属性时,Node采用的一种优化机制,当设置了位置、旋转和尺寸等时,矩阵需要对应地更新,懒惰计算机制并不直接更新其影响的内容,而是放在visit中进行处理。我们把Node中的属性划分为数值和显示两类,在这里对数值的修改是立即生效的,但是数值的修改可能需要等到下一帧才会作用到显示上。

在数值发生改变时,通过设置一个XXXDirty的布尔变量,来记录这一帧数值发生了改变,在下一次visit调用中,检查Dirty变量,如果是则进行计算。显示属性改变时会设置_transformDirty,子节点的ZOrder发生变化时会设置_reorderChildDirty。

延迟计算机制的好处在于,在这一帧中,如发生了大量的属性修改,则只计算一次;如不发生变化就不计算。通过延迟计算可节省计算开销。

6.1.8 其他属性介绍

//节点是否正在运行中
bool _running;
//节点是否可显示
bool _visible;
//用于识别节点的标记
    int _tag;
//节点的名字
std::string _name;
//节点附带的void*指针,用于在节点中传递数据
void *_userData;
//类似_userData,用于在节点中传递对象
Ref *_userObject;