虽然场景中的元素都继承自Node类,但是在这里,我们把场景类Scene、层类Layer和精灵类Sprite说为高级节点类,把它们抽象出单独一节来讲解。
在讲解这 3 个类之前,先介绍一下它们共同的基类 Node。虽然在程序中很少直接使用该类,但是它封装了一个节点所需的大部分功能,包括对节点的空间、颜色、动画、执行过程等的控制。
Node 类的功能主要包括设置和获得图形属性函数、父子关系函数、tag 和 user data 函数、GLProgram 相关函数、事件回调函数、动画函数、定时器函数、空间变换函数、坐标转换和一些其他功能函数等。
1.图形控制函数
图形控制函数用来设置节点的 Z-order、位置、旋转和缩放等变换,部分函数如表 2-5所示。
表2-5 图形控制函数表
这些函数的使用在讲解Sprite类时结合精灵类讲解。
2.父子关系函数
父子关系函数用来向父节点中加入子节点、获得父节点等功能,部分函数如表 2-6所示。
表2-6 父子关系函数表
程序片段2-6阐释了这些函数的使用。
程序片段2-6 父子关系函数使用
3.生命周期函数
一个节点只要生成了,那么它就会执行如表2-7所示的函数。
表2-7 生命周期函数表
对于表 2-7 中的这 4 个函数,它们的执行顺序是,当节点进入场景时,先执行 onEnter函数,然后执行onEnterTransitionDidFinish函数,在Node类的addChildHelper函数中可以看到如程序片段2-7所示的代码。
程序片段2-7 addChildHelper函数部分代码
当一个节点被删除时,先执行onExitTransitionDidStart 函数,然后执行onExit 函数,在删除节点的执行函数detachChild中可以看到如程序片段2-8所示的代码。
程序片段2-8 detachChild函数部分代码
如果在子节点中重载了这些函数,则在子节点中必须调用父类函数,如Node::onEnter()等。
4.动画函数
动画函数可以定义一个动画,然后把该动画作用于节点上,当该节点不需要该动画时,可以通过相关功能函数删除该动画,这些函数如表2-8所示。
表2-8 动画相关函数表
有了这些函数,要使一个节点执行一个动画就比较简单了。例如,让一个节点从(100,100)的位置匀速移动到(400,200)的位置,实现代码如程序片段2-9所示。
程序片段2-9 动画函数使用
5.定时器函数
一个节点可以设置 3 种类型的定时器函数,具体细节将在第 4 章讲解。Node 类提供了生成定时器相关的功能函数,如表2-9所示。
表2-9 定时器函数表
例如,要定义一个每帧都会执行的定时器,实现代码如程序片段2-10所示。
程序片段2-10 定时器函数使用
在节点中调用了该函数后,然后重载Update函数,则Update 函数就会每帧都会执行,直到在程序中调用了unscheduleUpdate函数后,该节点的生命周期结束。
6.坐标转换
坐标转换涉及的知识有点多,将分为3个点来将,分别是坐标系、屏幕与OpenGL坐标转换和世界与本地坐标转换。
1)坐标系
Cocos2d-x 引擎中涉及 4 种坐标系:屏幕坐标系、OpenGL 坐标系、世界坐标系和本地坐标系。其中,屏幕坐标系和OpenGL坐标系如图2-4和图2-5所示。
图2-4 屏幕坐标系
图2-5 OpenGL坐标系
从图2-4和图2-5中可以看出:
· 屏幕坐标系,原点在左上角, X 轴向右增长, Y 轴向下增长。
· OpenGL坐标系,原点在左下角, X 轴向右增长, Y 轴向上增长。
对于世界坐标系和本地坐标系,它们都同OpenGL坐标系,原点在左下角。其中,世界坐标系是指绝对坐标,而本地坐标系是局部坐标。
2)屏幕坐标系与OpenGL坐标系转换
在程序中,我们使用的都是OpenGL坐标系,但在Director类中提供了实现屏幕坐标系和OpenGL坐标系之间转换的方法,实现代码如程序片段2-11所示。
程序片段2-11 屏幕坐标系与OpenGL坐标系转换
3)世界坐标系与本地坐标系转换
Node节点中提供了世界坐标系和本地坐标系进行坐标转换的函数,如表2-10所示。
表2-10 世界坐标系与本地坐标系转换函数表
对于这两者之间的转换,可以从图形学的知识中来理解。每个节点都有一个变量,存储着它到世界坐标系下的变换。在图形学中,使用矩阵的方式来存储,这个矩阵包括的信息有节点的位置、旋转、缩放等。当一个位置要变换到另一个节点的坐标系下,等于该位置与矩阵变量中相应信息计算的结果。这种理解方式可以从convertToNodeSpace的实现代码中得到验证,如程序片段2-12所示。
程序片段2-12 convertToNodeSpace函数
本地坐标系的坐标原点在左下角,所以表中的前两个函数是相对于左下角来计算的。而后两个函数是相对于节点的锚点来计算的。下面通过一个例子来讲解,执行代码如程序片段2-13所示。
程序片段2-13 坐标系转换
执行这段代码后,输出结果如图2-6所示。
这两张图片在屏幕中的显示位置如图2-7所示。
· pos1:把A节点的位置转换到B节点的本地坐标系下,由于B节点的本地坐标系在左下角,即坐标(160,200)的位置,以这个位置为原点,则计算(400-160,300-200)可得该结果。
图2-6 坐标输出
图2-7 两个节点在坐标系中的位置
· pos3:同 pos1,只是以 B 节点的锚点为坐标系的原点,则计算(400-200,300-240)可得该结果。
· pos2:注意,前面有讲到矩阵与向量之间的计算,从数学上讲就是 A 节点的位置向量与 B 节点的世界矩阵计算的结果。而从几何的方式来说,则就是把 B 节点的左下角当成本地坐标系的原点,A相对于B节点偏移了(400,300),由于A节点在世界坐标系下左下角的位置为(160,200),那么B 节点转换到世界坐标系下的位置就为(160+400,200+300)。
· pos4:同pos2,只是A节点本地坐标系的原点移到了锚点所在的位置(200,240),通过(200+400,240+300)计算可得。
需要注意的是,不要认为 convertToWorldSpace 就一定能得到位置向量在世界坐标系下的点,从数学的角度来看,它只是矩阵与向量之间的一个计算,由于转换节点中有一个矩阵变量是在世界坐标系下的,所以要获得被转换节点在世界坐标系下的位置信息有两种方法,如程序片段2-14所示。
程序片段2-14 获得节点世界坐标
不管是本地坐标系与本地坐标系之间的转换,还是本地坐标系与世界坐标系的转换,都是一个向量转换到另一个坐标系下,从数学的角度来看,都是矩阵与向量之间的一个计算,问题是首先分清楚目标矩阵是什么,然后再来理解它们之间的变换。
7.其他
节点类的 tag 函数和 user data 函数,用来设置节点的 tag、name 等信息,有了这些信息,在程序中就可以通过它们获取相应的节点;GLProgram 相关函数,当用 Shader 指定节点的着色时,可以调用这些函数指定节点要使用的着色程序;空间变换函数,可以获得和设置父子节点,或者子节点到世界空间的变换。除了这些函数外,还有一些常用的函数,如表2-11所示。
表2-11 Node类其他的常用函数
关于这些函数的使用,如程序片段2-15所示。
程序片段2-15 Node类一些其他常用函数的使用
游戏中可能有多个场景,但在同一时刻只能存在一个运行中的场景,由于在 3.x 版本中加入了 3D 编程,涉及 3D 的一些知识也加入到场景中,如相机、灯光等。其中,相机用于拍摄场景中的对象,在相机范围之内的对象显示到屏幕上,而在相机范围之外的对象则剔除掉。灯光用于模拟现实中明暗变化、聚光灯和高光等效果。引擎以场景为运行单元,场景的相关操作都封装到Scene类中,部分函数如表2-12所示。
表2-12 Scene类部分函数
如果要创建一个场景,然后让该场景立即运行,实现代码如程序片段2-16所示。
程序片段2-16 Scene使用
Render 是渲染场景的入口,在 Director 类的 drawScene 函数中有它的调用,如程序片段2-17所示。
程序片段2-17 render函数入口
程序片段2-16中,使用Director类的replaceScene方法切换场景时,两个场景之间会立即转换。引擎提供了一些类,可以让两个场景在切换时有一个变化,如淡入淡出、翻页和交叉进入等。场景类 Scene 也是一个节点,所以同样可以运行动画。那么,对于当前场景,可以让它运行一个动画,以某种方式消失,而下一个场景,同样运行一个动画,以某种方式进入。这样,在场景切换的这段时间里,内存中就会有两个场景的数据。只有当场景切换完成后,上一个场景的数据才会删除。所以,读者在使用这种场景切换方式时,要考虑这个问题,即内存是否能够同时满足两个场景数据的要求。
场景转换的基类为 TransitionScene,有多个子类,分别实现不同的场景切换方式,具体如表2-13所示。
表2-13 TransitionScene子类
表 2-13 中这些类的实现方式,实际上是让两个场景运行不同的动画效果,只是引擎帮助开发者封装了一些,如果觉得这些都不能满足项目的需求,则可以继承 TransitionScene类,使用其他动画实现自己需要的效果。关于动画,将会在第 3 章讲解。有了这些类,对于它们的使用就比较简单了,具体如程序片段2-18所示。
程序片段2-18 场景切换实例程序
程序片段 2-18 中,Chapter2TransTwo 是目标 Layer,然后把它加入到场景中,使用TransitionSlideInL 在两个场景之间进行切换。create 函数中的第一个参数为时间,第二个参数是目标场景。最后,使用replaceScene开始两个场景之间的转换。实现效果如图2-8所示。
图2-8 场景转换效果图
图 2-8(a)是 TransitionSlideInL 切换方式,下一个场景把上一个场景向右推开了;图2-8(b)是 TransitionProgressInOut 切换方式,下一个场景从屏幕中心由小变大逐渐覆盖上一个场景。
Layer 类除了有一个静态的 create 函数外,还封装了触摸、键盘和加速器的响应事件,它们对应的函数都是以on开头的,如触摸事件onTouchBegan、键盘事件onKeyPressed和加速器事件onAcceleration,其他的函数在3.5版本中都已经被废弃,建议不要再使用。
除了Layer类外,还有3个与层相关的类,它们分别是纯色类LayerColor、颜色变化类LayerGradient 和多层控制类 LayerMultiplex。LayerColor 类创建的层只有一个颜色,所以可以当作背景或其他的用途。例如,使用如程序片段2-19所示的代码来创建。
程序片段2-19 LayerColor类的使用
LayerGradient 类用于创建一个颜色梯度变化的层,通过指定开始颜色和结束颜色后,该层会在一个指定的方向颜色渐变,如程序片段2-20所示。
程序片段2-20 LayerGradient类的使用
当游戏中有一个页面要显示排行榜,里面有积分和战斗力排行时,把它们做成两个Layer,然后使用LayerMultiplex控制,通过类似Tab按钮,在这两个Layer中来回切换,实现代码如程序片段2-21所示。
程序片段2-21 LayerMultiplex类的使用
在游戏中,如果有需要,可以把场景分成几个层,如背景层、人物层、特效层和 UI 层等,通过层可以控制物体渲染的先后顺序。通过一个或多个层,组成游戏中的一个场景。
如果要在屏幕上显示一张图片,可以使用Sprite类来创建。
1.Sprite创建
创建 Sprite 对象有多种方式,既可以指定图片所在的位置,又可以使用 Texture2D 对象;既可以使用图片的全部,又可以使用其中的某个部分。程序片段 2-22 阐述了 Sprite 对象的几种创建方式。
程序片段2-22 Sprite创建方式
在使用图片的部分来创建Sprite时,Rect中的前两个值为0是指图片的左上角,这点要注意。Test.plist是一个文本文件,它有一个对应的png文件,这个png文件是由多个小图拼在一起的一张图,plist文件中存储各个小图在png 图片中的位置和大小等信息。这张图可以使用Texture Packer等软件来生成,如图2-9所示,左边的3张图通过该软件合并后变成右边一张图,并且生成一个plist文件。
有了拼接后的plist和png文件,就可以使用SpriteFrameCache类的相应函数把各个小图加入到帧缓存中,以后创建 Sprite 时可以直接到帧缓存那里取。使用这种方式,不但减少了文件的大小和内存的使用,还减少了文件加载的次数,尤其在进行批次渲染时提高了游戏的性能。
图2-9 小图合并
2.位置和锚点
设置一个节点的位置比较简单,直接调用 setPosition 函数即可,如在一个大小为(960,640)的窗口中可以通过如程序片段2-23所示的方式来设置位置。
程序片段2-23 setPosition函数的使用
锚点是节点本地坐标系中的一个点,其值的范围在0~1之间,左下角为(0,0),右上角为(1,1)。锚点只对节点的位置、旋转、缩放和扭曲起作用,对颜色和透明度没有效果。对于锚点的解释,就好像你手上有一张扑克牌和一枚图钉,扑克牌竖放,且方向不变,当我把图钉插在扑克牌的中间时,那么它的锚点坐标为(0.5,0.5),左下角为(0,0),右上角为(1,1)等,如果把这张扑克牌设置在(480,320)的位置,那么就把这枚图钉钉在这个位置上,扑克牌换成上面的笑脸图片,具体实现如程序片段2-24所示。
程序片段2-24 锚点使用
通过设置不同的锚点,其效果如图2-10所示。
图 2-10 是节点 Sprite 通过 setPosition 函数设置在同一个位置,只是节点的锚点不同而已。图 2-10(a)的锚点为(0,0),图 2-10(b)的锚点为(0.5,0.5),图 2-10(c)的锚点为(1,1)。从图 2-10 中可以得知,即使节点的位置一样,但如果通过 setAnchorPoint 函数设置不同的锚点,显示效果则会不一样。
3.旋转
旋转操作也是相对于锚点的,在 2D 的世界里,旋转一般是在一个平面内顺时针或逆时针旋转,调用函数setRotation 来实现。当角度为正时,顺时针旋转,否则逆时针旋转。当然也可以调用函数setRotationX设置节点绕 X 轴旋转、调用函数setRotationY绕 Y 轴旋转,从而达到不一样的效果,程序片段2-25演示了旋转函数的使用。
图2-10 锚点不同时Sprite节点的显示效果
程序片段2-25 setRotation函数的使用
通过设置不同的旋转角度,执行后,其效果如图2-11所示。
图2-11 旋转效果图
4.缩放
缩放可以在两个方向上进行, X 轴或 Y 轴,通过函数setScale、setScaleX和setScaleY实现,执行如程序片段2-26所示。
程序片段2-26 setScale函数的使用
执行这段代码后,对应的效果如图2-12所示。
图2-12 缩放效果图
5.扭曲
扭曲可从 X 或 Y 轴两个方向进行,使用函数setSkewX 和setSkewY 来实现,执行程序片段2-27所示。
程序片段2-27 扭曲使用
执行后,效果如图 2-13 所示,左边第一个是 X 轴方向的扭曲,中间是正常的,右边是 Y 轴方向的扭曲。
6.颜色和透明度
节点类提供了 setColor 函数来设置它的颜色,其颜色值封装在结构 Color3B 中,它有 3个成员变量,分别表示红、绿、蓝的成分值,取值范围为0~255,该结构也预设了一些颜色值,可以通过它们直接设置颜色值,程序片段2-28演示了setColor函数的使用。
程序片段2-28 setColor函数的使用
效果如图2-14所示,左边设置为橙色,中间是正常色,右边为绿色。
图2-13 扭曲效果图
图2-14 设置不同颜色效果图
可以通过程序片段2-29来设置节点的透明度,取值范围为0~255。
程序片段2-29 setOpacity函数的使用
执行程序片段 2-28 后,效果如图 2-15 所示,左边的透明度为 100,中间的透明度为150,右边的透明度是正常的,即为255。
图2-15 不同透明度效果图
7.平铺
有些游戏背景中的图片是有规律的,它由多张同样大小的图片组成,其效果类似家里的地板。所以,只要一块地板的图片,就可以铺满整个屏幕,这样大大地节省了图片资源。但是,对于这样的图片,尺寸为2的 n 次方,长宽相等,并且图片的4边是能够对接上的,通过 Texture2D 类的 setTexParameters 函数来实现,关于这种 Sprite 对象的创建,如程序片段2-30所示。
程序片段2-30 平铺图片实现
Sprite 对象的创建和前面介绍的没什么差别,主要是设置对象的纹理属性和调用setTextureRect函数设置纹理的矩形区域。