算起来,正式投入cocos2d-x的学习已经有40多天了,在这期间,出了不少的问题,收获也算不少。但是,相对于40多天的时间来说,这个学习效率还是比较低。
首先,系统的了解了cocos2d-x引擎相关的知识点。从最基础的部分开始学习。在此之前,我所了解的游戏结构就是:场景->层->精灵。但是在看Sprite的时候,才发现不仅仅是这么简单。cocos2d-x 中所有的类都最终继承于Ref类,每个类都从父类继承了大量方法。
Sprite类,我通常用图片来创建一个精灵,然后对精灵进行操作。
但是Sprite的功能不仅于此。Sprite不仅可以直接使用图片来创建,还可以通过SpriteFrameCache来创建,而且使用纹理缓存来创建的方式可以大大提高效率。通常在使用的时候,需要给精灵添加一些方法或者属性,此时要用一个自定义的类来继承Sprite,重写create方法,实现父类的虚方法init。在写create的时候,可以要求传入一些参数来对数据进行初始化。在create中要调用或者间接调用父类的init方法,还要调用引擎提供的autorelease方法,引擎能在需要的时候释放对象所占用的内存。Sprite还继承了父类的runaction方法,使得Sprite能执行许多动作,例如缩放,移动,旋转,翻转等。让静态的图片也能产生动态的效果。在扩展包中,为了能更好的使用图片资源,还封装了Scale9Sprite类,它将图片资源分为9格,可以对不同的地方进行缩放,既能缩放图片大小,又能保留图片细节。Sprite其实不仅仅是一个精灵,它还可以添加子节点精灵,例如在许多图块需要同时进行操作时,可以将图块精灵添加到一个精灵中进行操作。精灵的坐标系原点在左下角,所以在添加子类精灵时需要注意。在执行触摸,或者需要取精灵中包含的精灵时,坐标需要转换为相对于父类精灵的坐标,此处坑死我了,可以直接使用node的函数,convertToNodeSpace()将传入的坐标转化为相对坐标。
这仅仅是一个精灵类,就从之前的认知上扩展了如此之多的分支,想想还有那么多类,然后世界观都要被颠覆了。
在Scene的使用上,自己用到的没有太多东西,但是其实它有很重要的作用。在写Scene时,最好是自己去重写create方法,和构造与析构函数。Scene还有一个重要功能,就是进行资源的加载和释放。我们可以在游戏的开始界面进行游戏场景的资源加载,这种异步加载模式,可以隐藏场景切换时加载资源造成的掉帧、卡顿等问题。而且在进入游戏场景之后,不需要再为加载资源来消耗cpu,使游戏更加流畅。虽然这个技术很牛x,但是我还木有用到。除了加载资源,Scene的作用就是显示游戏层。Layer和Scene不同,一般需要进行场景切换的情况不多,但是需要进行Layer的加载和切换的情况比比皆是。
在使用Layer进行遮罩的时候,例如在开始游戏界面选择设置、在游戏进行时暂停、在游戏结束时显示GameOver等,遮罩层的创建方式都有多种。Layer的遮罩背景可以使用一个全屏大小的按钮,可以对按钮写返回游戏界面的回调,当玩家点击了其他按钮以外的地方,可以直接返回游戏界面。当然也可以写空回调函数。这是使用按钮的方式,还有一种,就是给遮罩层添加空的触摸函数,这样玩家点击到按钮以外的地方,触摸会被遮罩层吃掉,不会影响到游戏层。
关于Label的使用倒是没有接触到太高端的东西。在使用Menu时,遇到过问题。我通常使用的菜单项是MenuItemImage,通过按钮图片名来进行菜单项创建非常方便。但是,将资源打包,在Scene加载后就不能使用这种方式。后来发现,使用MenuItemSprite可以解决这个问题,在创建菜单项时,直接使用两个精灵来做显示,创建精灵时可以使用SpriteFrameCache来创建,这就不需要直接使用图片文件名。
在了解了UI后,运行了测试项目的UI部分,看到各种列表,觉得以后列表分页选关之类的可以直接拿来用了。但是看过真实项目之后,发现项目中的效果比引擎提供的效果好得多,所以在实际使用的时候,还是需要自己手动来写UI。
用户交互方面,除了文本输入,在移动端上使用最多的就是触摸。所以触摸事件是很重要的。。。触摸事件是很重要的(重要的事说两遍)。首先你得有一个监听器,最常用的是单点触摸监听,就是使用EventListenerTouchOneByOne,监听事件包括onTouchBegan,onTouchMoved,onTouchEnded,onTouchCancled,就是开始触摸、触摸移动、触摸结束、触摸异常结束。通常触摸结束的回调和触摸异常结束的回调是一样的,如果在触摸时有电话接入,会导致异常退出,此时应该进行和正常退出同样的处理。触摸事件绑定函数的时候,通常会用到宏CC_CALLBACK_0等,这后面的数字,表示的是占位符的数量(详细讲解)。除了单点触摸事件,在触摸事件中还包括多点触摸,它的处理方式和单点触摸类似。在创建监听器时,还可以看到鼠标监听器,键盘监听器以及自定义监听器。重点:在创建监听器,绑定事件之后,需要将监听器添加到程序的事件分发器中,新版本的添加方式为Director::getInstance()->getEventDispatcher()。
在学习触摸事件的时候,发现一个非常好用的东西——lambda表达式。格式 [传值方式] (函数参数) {函数体}。这种表达式可以直接用来替换一个函数,在触摸事件绑定函数时。直接使用赋值符号,来对触摸事件进行绑定。lambda表达式适用的范围比较多,但也不是万能的,常用在菜单事件和触摸事件。说到函数,还有一种就是CallFunc类,一般会用在动作执行完后的回调。
Sprite的动作算一个比较难的点,为此浪费了很多时间。很多时候,我们需要等动作执行完后再继续执行活着转而执行某个函数。但是在程序中,调用runAction的时间是极短的,程序不会自己等待动作执行完。所以可以使用Sequence将动作打包,还可以使用Callfunc创建一个函数回调,添加到sequence中,这样函数体将在动作执行完之后被执行。如果需要几个动作同时执行,可以使用Spawn来打包几个action,然后在runAction时,动作将会同时进行,形成新的动作。还有的时候,因为需要在许多精灵的动作都结束后进行一次回调,所以精灵不能独自执行自己的操作,此时可以使用DelayTime来让回调函数延时执行,更好的方法是使用TargetedAction,来绑定一个精灵和一个动作,可以由其他的精灵来runAction,触发精灵来执行其绑定的动作。
想要在动作执行完之后调用回调函数来进行某些操作时,不仅是可以使用Callfunc连接在动作后面执行,还可以使用观察者模式(消息机制)。在动作结束后,使用NotificationCenter::getInstance()->postNotification()来发送消息,在需要获知这个消息的地方订阅消息,Notification::getInstance()->addObserver()添加观察者。在动作执行完,发送消息之后,如果观察者接受到指定名字的消息,就会调用绑定的一个函数。而且通过这种消息机制,不仅可以进行一种操作,所有订阅该消息的地方都能收到该条消息,并触发绑定事件。
在进行数据存储时,会经常用到map,vector。虽然在cocos2d-x引擎中有提供封装好的Map和Vector类,但是为了防止错误,通常还是使用c++标准库的map和vector。在map取对象时可以使用at(key)函数或者直接使用[key],使用vector取值时也可以使用[index]的方式。在对数据进行查找时,可以使用循环进行遍历,例如for(auto &it : map){}。因为迭代器it是引用,所以在这个循环中最好不要进行数据删除操作,否则map变化会导致it失效。
在进行管理类的编写时使用到了单例模式。在使用单例模式需要注意的点就是构造函数和析构函数私有化,私有化一个自己的类的指针用于持有对象,还必须要有一个静态的公有的。单例类在创建后会一直占用内存空间,所以要尽量少定义单例类。单例类一般用于进行其他类的管理,或者提供某些公用的功能。
这些是开发时比较常见的问题,或者说我踩的坑。还有一些是我之前了解过,但是没有碰到的,例如委托。其意义在于层之间的通信,或者子节点与父节点的通信。例如,一个层中包含了一个精灵,现在精灵想要调用层的函数,此时可以在精灵类外面写一个委托类,精灵持有一个委托类的对象。然后层去继承委托类,实现委托类的方法,然后再层中添加精灵的时候将层赋值给精灵的委托类指针(相当于父类指针),然后在精灵中就持有了层的对象,而且精灵只能调用委托类中的方法,既调用了父节点的方法,又保证了安全。
关于游戏的网络通信一块现在还没有用到,而且现在了解的很少。基本的流程就是:(首先,后台服务器需要搭建好)使用HttpRequest来创建一个用户请求对象,在对象中设置请求的URL、请求方式、请求头部、收到回复的回调函数。然后使用单例HttpClient来发送这个请求,并且设置请求超时等待时间。当服务器返回信息时,首先判断返回的是否是错误代码,然后使用char类型的容器来获取返回数据。这种http请求在小游戏中的使用主要是在分享、排行榜、购买等情况中。
还有我目前用的比较少的,就是骨骼动画,粒子系统,和声音引擎。
骨骼动画还分为两种Spine和Armature,用法都有总结过。但是目前还没有在demo中使用到。至于粒子系统,个人赶脚有一个比较好的在线粒子编辑器(传送门),在网站中能够直接看到粒子效果,而且能导出生成文件,直接在游戏中使用。但是据说在实际项目中,粒子系统很多参数都需要自己去动态的设置,所以这个还需要加深了解。还有一个用的少的,SimpleAudioEngine,就是在游戏中进行音乐和音效播发的类。之前有使用过这个类,用法也比较简单,首先对音乐资源进行预加载,播放的时候,背景音乐的播放方法和音效的不同,这一点要注意。在使用过程中也碰到了一些问题,例如,在背景音乐循环播放的时候,一次播放完后游戏会卡顿一下,不会是加载资源的问题,因为资源都已经预加载好了。这个问题等再学SimpleAudioEngine的时候去解决。
还有两个常用的,比较麻烦的东西。就是瓦片地图和物理引擎。
瓦片地图在关卡类游戏中用的比较多,或者说是必定会用的。在之前学习的视频教程中,首先拿原图在TiledMap中切分成图块,再使用图块拼接成设计好的地图,然后导出生成tmx文件,然后在层中创建TMXTiledMap类的对象,再添加到层中显示即可。使用这种方式,在触摸选中图库时,有可能会有不准的效果,而且在绘制地图的时候添加的对象层可能会有位置偏移(赶脚是我自己技术太low才遇到这种问题)。后来师父指导了一下,才明白一般使用tiled只是用来配置地图的属性,在程序中不是直接使用tiled绘制的那张地图,而是根据地中不同图块的坐标和属性来创建不同的精灵,最后组成真正的游戏地图。在使用的过程中,我还是遇到一个问题,虽然每种图块都有一个GID,在使用时根据GID和图块坐标来创建精灵,组合成地图。但是当需要有某个精灵和其他的精灵属性不同时,tiled貌似不是那么好用,因为不能对单独一个图块设置id或者属性,貌似可以用其他的层去实现,但是目前还没有使用过这种方法。
物理引擎也是比较头疼的一块,因为之前并没有了解和使用过物理引擎。最开始接触物理引擎的时候,是使用的cocos2d-x引擎自己封装好的物理引擎Physics,这个封装过的物理引擎使用起来比较方便,但是毕竟是为了使用方便而做的封装,所以效果并不是很好。在纹理设置和碰撞的时候,经常会出现一些意想不到的bug。后来了解到,使用的最多的应该还是Box2d,但是在cocos2d-x 3.4版本之后,测试项目中只能看到chipmunk的测试效果,所以Box2d的学习还是比较困难。
在网上找了一些视频和教程,总算对Box2d稍微了解了一点。使用起来感觉与Physics完全不同,在Physics中是刚体在跟随精灵,但是在使用Box2d的时候,是精灵在跟随刚体,而且精灵的位置需要在update中一帧一帧的刷新,对精灵的控制也受到了比较大的限制。在Box2d中创建物理刚体的时候也比较麻烦,给人感觉是很专业的物理引擎。首先在层中创建一个b2World对象,这就是物理世界。然后创建b2BodyDef对象,在其中设置好所需要创建的刚体的一堆属性,然后使用b2World对象,通过b2BodyDef对象中的设置来创建一个b2Body,这只是创建了一个刚体而已,在此之后需要对刚体设置形状、物理属性,这里要用到b2CircleShape、b2PolygonShape、b2EdgeShape的对象来设置形状,再使用b2FixtureDef来设置物理属性,Shape对象也是属性之一。然后使用之前创建的b2Body刚体来绑定这个b2FixtrueDef,就(才)完成了一个刚体的创建。相同的步骤,使用b2EdgeShape可以创建一条线,准确来说是边界。物理世界的边界或者场景的墙壁可以使用这种类型的形状来创建。
上面一堆也只总结了刚体如何创建,在物理引擎中很常用的关节还没有学习到,所以物理引擎这个坑有点大。
在做demo的过程中,还学习到一种方法,就是状态控制(并不知道是不是叫这个名字)。例如在游戏中需要进行很多种操作,在不同的情况下,所允许的操作也不一样。在项目产生这种需要时,可以为游戏层或者主角类创建一个枚举类型的对象来存储其状态。状态的控制有两种方式,比如在操作主角类时,可以使用事件回调,就是在每次完成某个动作、播放一段动画、触发某个事件之后调用状态改变的函数,将主角类的当前状态转换成其他状态,在执行操作时通过状态的判断就能很好的控制允许的操作。还有一种方式,动作计数。可以使用在场景中精灵比较多的情况下。为游戏层添加一个枚举类型的对象存储状态,还要创建两个计数器来进行动作的计数。在游戏层中的精灵执行动作前给动作总数计数器+1,动作执行完后给动作执行计数器+1,当精灵全部执行完动作,动作总数和执行数相等,此时游戏层可以进入下一个状态。这样在精灵很多,而且动作不同步的时候就能控制游戏层的操作不会发生冲突。但是这种计数的方式需要在update里面进行循环判断,相对之前一种,资源消耗比较高。
学习了解的cocos2d-x开发技术基本就是这么多。后面就总结一下代码规范。之前自己进行练习的时候,代码没有一个标准的规范,经常是现在写的东西是一个风格,明天写的又是另外一个风格。现在学习了师父整理的C++编程规范,变量名、函数名也都有了比较统一的风格。但是在写demo的时候还是发现了一些问题,例如:在游戏开发过程中,功能不能很好的划分清除,有很多函数会调用其他函数,最后导致在调用这些函数的时候,必须按照一定的先后顺序(因为有些指针要在别的函数中先赋值),否则程序就会崩溃。而且写类的时候会出现相互包含,相互持有指针的问题。自己对于一个完整的项目没有掌握,导致编写的过程中会出现结构不清晰,耦合度高的情况。这个还需要通过慢慢摸索来改进。
还有一些总结就是关于团队合作的东西。程序、策划、美术之间的沟通确实很重要,有时候策划并不知道程序中哪些效果可以实现,哪些效果不能实现或者很难实现,有时候策划在细节方面没有考虑好,容易导致做出来的功能并不符合游戏需要,程序容易有一种一番心血被浪费的感觉(这并不能成为程序打策划的原因)。而且在美术资源方面,程序和策划都要和美术沟通,因为策划需要去确定UI风格,程序要确定图片资源的格式,大小等。
总的来说,感觉cocos2d-x学习经历的坑确实不少,而且还有许多工作是要慢慢学习,自己摸索的。