如何制作一个横版格斗过关游戏 cocos2d 2.0.4 下载-x 2.0.4

cocos2d-x 3.0 打造横版格斗游戏
&来源:读书人网&【读书人网():综合教育门户网站】
cocos2d-x 3.0 制作横版格斗游戏闲聊一下,其实这次3.0版本吸引我的地方除了各种性能提升的传说之外,主要有
cocos2d-x 3.0 制作横版格斗游戏
闲聊一下,其实这次3.0版本吸引我的地方除了各种性能提升的传说之外,主要有两点:取消了各种类、宏的CC前缀,例如CCSprite变为了Sprite,老实说我很不喜欢之前那种类似匈牙利的命名方式,总觉得既然有cocos2d这个namespace了干嘛还要画蛇添足的在每个类前面加个CC呢?我不知道是不是cocos2d-iPhone的传统(懒得研究),但真心不喜欢这个CC前缀。加入了2.5D的支持,这个纯属个人喜好,因为在我脑子了已经勾勒出了一两个游戏,而且都是2.5d才能更好的表达游戏的效果(3d人物+2d场景的效果很炫,目前正苦逼的练习手绘+photoshop)扯远了,进入正题,横版格斗游戏《PompaDroid》的制作过程新建工程使用create-multi-platform-projects.py纳尼!!!之前的install-templates-msvc.bat呢,没有模版怎么在vs里新建工程??查阅了一番文档后发现其实从2.1.4开始,所有cocos2dx项目都用create-multi-platform-projects.py脚本来创建了(唉,落伍啊)。于是乎:目录说明Classes用于存放游戏的各种类各种实现各种算法(总之我想说的是.h和.cpp)proj.xxx即是各个平台的相应工程项目Resources游戏中所用到的资源全在这里Wonderful!!我很喜欢这样的目录结构,简单明了。接下来先从Windows平台开始吧,进入proj.win32并打开PompaDroid.sln在解决方案中有6个项目:项目说明PompaDroid很明显,需要我们实现的游戏项目libBox2D物理引擎(这里用不到,不管它)libchipmunk貌似和box2d一样是物理引擎libcocos2d这个就不用解释了libCocosDenshion音频引擎(我没在游戏里加入音效)libExtensions扩展库,具体参考这里依照惯例,cocos2dx新工程里会有个HelloWorld实例,展开cyclone-kid\Classes就可以看到了,果断编译运行,一切ok!好了,删除HelloWorld.h和HelloWorld.cpp文件,开始实现CycloneKid的代码吧!首先点击这里下载所需的美术资源,解压放到Resources目录下。GameScene一款游戏中会有很多个场景(Scene):主菜单、游戏、游戏结束、过场动画等等,由于只是学习,我仅做了主游戏场景(GameScene),该场景包涵两个图层(Layer):游戏层和操作层。顾名思义,游戏层主要负责游戏的内容(地图的渲染,各个精灵的调度等);而操作层负责响应触摸(前进、后退、攻击等操作)。因此根据上图所示,我们需要建立三个类,GameScene?GameLayer?OptionLayer务必把类文件建立在Classes目录下,方便之后夸平台编译GameScene.h在编译中我的环境会出现两个警告:现默认库“LIBCMT”与其他库的使用冲突;请使用 /NODEFAULTLIB:library?在属性--&配置属性--&链接器--&输入--&忽略特定库:增加LIBCMT即可;忽略“/EDITANDCONTINUE”(由于“/SAFESEH”规范)?在属性--&配置属性--&链接器--&高级--&映像具有安全异常处理程序:改为“否”即可。1.2 动作精灵主角和机器人的资源包涵在pd_sprites.plist与pd_sprites.pvr.ccz当中,打开pd_sprites.plist文件可以看到很多hero_xx_xx.png和robot_xx_xx.png,这些便是主角英雄和机器人的动作序列图片。从plist文件中可以清楚的看到,不论是“hero”还是“robot”都具备了idle空闲、attack攻击、walk行走、knockout(被)击倒、hurt受伤这五个动作,因此需要建立一个动作类ActionSprite来统一实现动作播放。如上图所示,我们需要新建三个类ActionSprite、Hero、Robot,其中ActionSprite既然是“动作精灵”所以需要继承cocos2d的Sprite类。原本我想ActionSprite类只负责调用精灵每个动作的动画,精灵的“攻击力”、“生命值”、“移动”等属性方法放到另一个新的类当中管理,但为了省事我还是决定把这些内容完全塞到ActionSprite当中(尽管我知道这是一种灾难,但是。。。让bug来得更猛烈些吧!谁让我懒呢)。首先,新建三个类ActionSprite、Hero、RobotActionSprite.h1.2.2 机器人机器人与hero一样是动作精灵,不考虑AI(人工智能,之后实现),它的实现过程几乎和Hero一模一样,根据plist文件把相关的五个动作动画实现就行了。Robot.h2. 操作层在上面一节,hero和robots都已经出现了,不过只是一直出于idle状态,现在首先让hero“动”起来,具体实现OptionLayer,这个类负责响应触控操作。其实GameLayer与OptionLayer一样都继承于Layer,完全可以在GameLayer中实现这些功能,只不过我认为把“游戏”和“操作”解耦之后更有利于把这个“操作”复用到不同的“游戏”中,或者让这个“游戏”使用多种不同的“操作”方式(例如纯触控、虚拟按钮等)。原文就是用虚拟按钮的方式实现控制的!But,我比较反感在触屏上设置虚拟按钮的方式进行操控,主要的问题在于,用这种方式不仅占用了游戏空间,而且玩家在操作之间必须先把注意力集中到寻找按钮的位置上,这是中相当糟糕的体验,所以我更喜欢发挥出现代智能手机的潜力,直接用触控手势的方式取代虚拟按钮。回顾上面的内容可以看到,玩家能控制hero的动作也就只有attack、walk两种,那么可以把手机屏幕中间“一分为二”变为左屏和右屏,左半部分交给玩家的左手用来控制hero的walk,右半部分交给玩家的右手用来控制hero的attack。既然是左右屏,就有左右屏同时操作的可能,所以必须使用多点触控。2.1 触摸响应cocos2dx3.0中的多点触控接口貌似就这四个:接口说明ccTouchesBegan触控开始事件,手指碰到屏幕ccTouchesMoved触控移动事件,手指在屏幕上滑动ccTouchesEnded停止触控时间, 手指离开屏幕ccTouchesCancelled触控被取消事件,例如手指画出屏幕外真心觉得很悲催啊,如果cocos2dx能加入手势识别不是会方便很多,例如“拖拽”、“滑动”、“点击”什么的。。。。唉,算了,还是老老实实先把这个游戏的操作层实现吧。除了最后一个Cancelled不需要外(大部分场合都用不到它),其他三个我们都需要,思路基本是这样:左屏为一个隐藏的“摇杆”,响应玩家左手的“拖拽”手势,ccTouchesBegan记录按下的屏幕坐标为起点并激活“摇杆”,当ccTouchesMoved的时候根据新坐标与起点的偏移量获取拖拽的向量和距离,来控制hero移动的方向和速度;右屏为一个看不见的按钮,响应玩家右手的“点击”手势,触发hero的攻击动作;基本思路出来了,开始动工吧!新建OptionLayer类(如果没有的话),根据前面的分析,OptionLayer中大概需要这么几个东西:响应触摸的began、moved和ended这三个事件一个“摇杆”,游戏中为joystick及它的相关函数(激活、更新什么的)委托OptionDelegate类,定义onWalk、onAttack和onStop接口(由GameLayer实现)OptionLayer.h:2.3 来控制hero吧目前为止,控制hero的部分其实已经完成了,因为思路和上一节的“摇杆”是一样的,现在无非是调用一下“委托”通知GameLayer层,让它改变hero的相关属性,所以首先完善一下手上的代码。OptionLayer.cpp中在ccTouchesBegan函数的right右屏部分发起“攻击”信号,“左屏”部分暂时不用写什么(该写的前面都已经写了);在ccTouchesMoved中发起hero的“行走”信号,就之前的思路而言,只有“左屏”会产生“滑动”的手势;在ccTouchesEnded中发起“停止移动”信号,让hero恢复为“idle”状态。-。-!!!逻辑不对呀!有几个问题:摇杆往左,hero往右,摇杆往右,hero往左。。。walk的帧率貌似太高了,而attack的动画帧率又太低了摇杆刚开始在左,移动到右的时候,hero的朝向却没有跟着取反(这是个大bug)赶紧改改!!回忆下之前的ActionSprite类的实现,我参考把walk和attack两个方法都添加了参数,其目的就是当“角色”行走的时候会根据参数direction判断并修改朝向(问题1的错误也在此),当“角色”受伤的时候根据damage参数,减少“角色”的hp,当hp为0后自动出发角色“死亡”,这种小聪明走到现在发现还真有点画蛇添足!“操作层”发来的onWalk是持续的,意味着那个“direction”参数随时都在改变,可只要“操作层”没有发送onStop信号时hero将会一直处于ACTION_STATE_WALK状态,在ActionSprite类实现中有一点很重要:只要精灵已经处于要改变的状态时,就不会在做改变的处理,所以尽管“direction”一直在变,也一直在调用hero的“walk”方法,在出起“walk”之后,后来的“direction”已经毫无意义了(因为hero已经在walk了,根本不处理)。我想了想,ActionSprite归根结底只是个“动作精灵”,它只要管好自己动作的相关动画和“角色属性”就好了,至于怎么调用,怎么改变,它的生杀大权还是交给GameLayer吧!首先要做一个小小的重构,就是删除所有ActionSprite相关的“动作参数”,一律改为void action(void),ActionSprite.h修改如下:这样应该就没问题了,不过现在只会原地踏步,接下来进一步实现hero真正的移动。hero在地图上移动是一个持续的过程(当玩家手指滑动后,只要不离开屏幕,hero就不会停下walk的脚步!),处理持续的动作时常常会用到cocos2dx的一个接口update这个接口为在每帧渲染的时候都被调用一次(所以update的实现务必要快准狠,不然浪费资源)。那么在update中需要做些什么呢?目前为止其实只需要做一点,递增/减hero的坐标值。那这个增量的标准是什么?其实就是hero的移动速度。那移动速度从何而来?想象物理界是如何定义速度的:速度是描述物体运动快慢和方向的物理量,定义为位移与发生这个位移所用的时间之比,看前半句,速度无非要两个东西“快慢”和“方向”,再来看看onWalk吧,它是带有两个参数的direction、distance,顾名思义“方向”“偏移量”,我们姑且把偏移量看做快慢的标准,偏移量越大速度越快;再来看看后半句的定义,“位移”与“时间”之比。真实世界里描述速度一般都是xx米/秒或者xx公里/小时,游戏里则可以用xx像素/帧来衡量,而update每帧都会调用。所以重新回到最开始的问题,update中要做些什么?其实就是把hero当前坐标加上一个速度量。一切都搞清楚了,具体来实现吧。GameLayer.h中声明update函数,并再声明一个_heroVelocity给update用:确实可以了,不过总感觉哪里不对呀??肿么hero可以跑到墙上,不科学啊hero都走出屏幕了这是要闹哪样robot肿么把hero给挡住了,他可是男主角啊!。。。。。。。。。问章有点长了,剩下的部分留到下一篇写,主要包括完善hero的移动,整个GameLayer的移动,另外就是robot的人工智能部分,最后是碰撞检测。声明:本文主要参考《如何制作一个横版格斗过关游戏 Cocos2d-x 2.0.4》和《How To Make A Side-Scrolling Beat Em Up Game Like Scott Pilgrim with Cocos2D》本文游戏与上述两篇文章游戏实现上出入较大(拒绝复制粘贴),如有不对的地方还望指正,以免误导!美术素材下载:?下载地址阅读:1498次
本文由eseedo翻译。
原文参考:
欢迎继续之前的横版过关游戏教程系列,这一部分将是该系列的第二部分,同时也是终结篇。
在第一部分的内容里面,我们已经创建了会发呆和挥动拳头的英雄,并且把D-pad方向键摆在了游戏的左下角。
而在这部分的内容中,我们将会学到更多东西,比如添加运动,卷轴滚动,碰撞,敌人,AI,当然还有令人热血沸腾的音乐和音效。
在开始学习这部分的内容之前,请先下载第一部分结束时的项目示例代码()。
同时别忘了下载项目所需的资源文件()。
准备好了吗?来吧,让我们干掉这些可恶的androids机器人!哦,不好意思,口误,是droid机器人。
让英雄走两步
在第一部分教程的最后,我们创建了一个D-pad方向键区,并将其显示在屏幕中。但如果此时按下方向键,游戏会直接崩掉。赶紧来修复这个问题吧!
在Xcode中切换到Hero.m,并添加以下代码:
//在if ((self = [super initWithSpriteFrameName:@"hero_idle_00.png"]))循环语句中的攻击动作之后添加行走动画:
CCArray*walkFrames=[CCArray arrayWithCapacity:8];
for(i=0; i<8; i++){
CCSpriteFrame*frame =[[CCSpriteFrameCachesharedSpriteFrameCache]spriteFrameByName:[NSStringstringWithFormat:@"hero_walk_d.png", i]];
[walkFramesaddObject:frame];
CCAnimation*walkAnimation=[CCAnimationanimationWithSpriteFrames:[walkFramesgetNSArray] delay:1.0/12.0];
self.walkAction=[CCRepeatForeveractionWithAction:[CCAnimateactionWithAnimation:walkAnimation]];
对这种添加动画的代码你应该毫无鸭梨了。这里我们给行走动画添加了新的帧,然后用创建发呆东走类似的方式创建了行走动作。
接下来切换到ActionSprite.m,并添加以下方法:
-(void)walkWithDirection:(CGPoint)direction {
if (_actionState == kActionStateIdle) {
[self stopAllActions];
[self runAction:_walkAction];
_actionState = kActionStateW
if (_actionState == kActionStateWalk) {
_velocity = ccp(direction.x * _walkSpeed, direction.y * _walkSpeed);
if (_velocity.x >= 0) self.scaleX = 1.0;
else self.scaleX = -1.0;
在这段代码中,我们首先确认前一个动作是否是发呆,然后将动作切换到行走,运行行走动画。如果前一个动作本来就是行走,那么我们秩序根据walkSpeed值来更改精灵的移动速度。
同时这里还检查精灵的左右方向,并通过将scaleX值设置为-1或1来翻转精灵。
当然,为了让英雄的行走动作和D-pad方向键联系在一起,我们必须借助D-pad的代理:GameLayer。
切换到GameLayer.m,并实现以下方法来实现SimpleDPadDelegate的协议:
-(void)simpleDPad:(SimpleDPad *)simpleDPad didChangeDirectionTo:(CGPoint)direction {
[_hero walkWithDirection:direction];
-(void)simpleDPadTouchEnded:(SimpleDPad *)simpleDPad {
if (_hero.actionState == kActionStateWalk) {
[_hero idle];
-(void)simpleDPad:(SimpleDPad *)simpleDPad isHoldingDirection:(CGPoint)direction {
[_hero walkWithDirection:direction];
通过以上方法,每当SimpleDPad发送一个方向信息时都会触发英雄的移动方法,而每当SimpleDPad的触摸事件结束时则会触发英雄的发呆方法。
编译运行游戏,然后尝试着用方向键来操控英雄的行走。
好吧,酷哥英雄貌似在移动,但又貌似没有动。走,还是不走,这是个问题。
神马原因呢?让我们再回头看看刚才的walkWithDirection:方法,实际上这里什么也没做,只不过改变了英雄的速度。即便英雄可以达到超音速,宇宙速度,光速,但他如果不走的话,当然只会原地踏步了。
好吧,实际上更改英雄的位置应该由ActionSprite和GameLayer来共同负责。一个ActionSprite精灵永远不知道他现在地图的何处。因此,他并不知道现在已经达到地图的边缘,只知道想走到属于自己的位置。而GameLayer则应负责把他的目标位置转换为实际的位置。
此前我们曾为ActionSprite定义了一个名为desiredPosition的CGPoint类型变量。而这个位置就是ActionSprite应该用到的唯一位置值。
切换到ActionSprite.m,并添加以下方法:
-(void)update:(ccTime)dt {
if (_actionState == kActionStateWalk) {
_desiredPosition = ccpAdd(position_, ccpMult(_velocity, dt));
以上方法将在游戏刷新屏幕的时候调用,其作用是当英雄处于行走状态时不断更新其目标位置。这里使用了一个简单的计算公式,目标位置= 实际位置+(速度*时间)。理想状态下,不存在加速度的~
注意:这种计算位置的方法被称作欧拉积分法。虽然简单易行,但并不精确。欧拉积分是神马鬼东西?Don’t panic,查查维基百科吧:
当然,考虑到不是在进行物理模拟,欧拉积分法已经够用了。如果对数学无爱,看看就好。
切换到GameLayer.m,并进行以下操作:
//在init方法的if循环中添加以下代码: [selfscheduleUpdate];然后添加以下几个方法:
//add inside if ((self = [super init])) right after [self initTileMap];
[self scheduleUpdate];
//add these methods inside the @implementation
-(void)dealloc {
[self unscheduleUpdate];
-(void)update:(ccTime)dt {
[_hero update:dt];
[self updatePositions];
-(void)updatePositions {
float posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - _hero.centerToSides, MAX(_hero.centerToSides, _hero.desiredPosition.x));
float posY = MIN(3 * _tileMap.tileSize.height + _hero.centerToBottom, MAX(_hero.centerToBottom, _hero.desiredPosition.y));
_hero.position = ccp(posX, posY);
来大概解释下这几个方法的作用:
dealloc方法就不说了,万物有生有灭,有预定消息的方法,就需要取消预定消息的方法。
接下来的update:方法预定了GameLayer类的消息,会在游戏的主循环中实时更新。这里我们首先调用了英雄的消息预定来更新其目标位置,然后调用updatePositions来确定英雄是否在地图地板的边界范围内,并据此来更新实际的位置。
在updatePositions方法中,用到了地图的两个重要属性值:
mapSize:也就是地图中的瓦片数量。总共有10*100个瓦片,但地板上只有3*100个。
tileSize:每个瓦片的实际大小,这里是32*32像素。
这里还用到了ActionSprite的两个测量数值,centerToSides和centerToBottom来确保ActionSprite始终位于屏幕的场景之中。
如果ActionSprite的位置在地图边界内,就会让英雄去他想去的地方。反之如果超出了地图边界,就会让英雄停在当前位置。
注意:这里用到了MIN和MAX这两个函数。MIN函数会比较两个数值,并返回较小的数值,而MAX则会返回较大的数值。Cocos2d中还针对CGPoint提供了一个方便的函数:ccpClamp。
此时编译运行游戏,英雄可以在地图中真正开始探索了。好吧,走两步,走两步,走着走着你会发现英雄走出了屏幕的右边界,然后不见了。
神马情况?地图的边界在哪里?地图的边界可不是屏幕的边界,作为一个横版卷轴游戏,显然地图应该是可以基于英雄的位置来滚动的。如果你不太清楚怎么弄,可以回过头看看之前的教程(.cn/s/blog_4b55fg6.html):
在GameLayer.m中进行下面的操作:
//在update:方法中添加以下代码:
[selfsetViewpointCenter:_hero.position];
//然后在updatePositions方法之后添加以下方法:
//add this in update:(ccTime)dt, right after [self updatePositions];
[self setViewpointCenter:_hero.position];
//add this method
-(void)setViewpointCenter:(CGPoint) position {
CGSize winSize = [[CCDirector sharedDirector] winSize];
int x = MAX(position.x, winSize.width / 2);
int y = MAX(position.y, winSize.height / 2);
x = MIN(x, (_tileMap.mapSize.width * _tileMap.tileSize.width)
- winSize.width / 2);
y = MIN(y, (_tileMap.mapSize.height * _tileMap.tileSize.height)
- winSize.height/2);
CGPoint actualPosition = ccp(x, y);
CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
self.position = viewP
以上方法的作用是让英雄始终位于屏幕x轴的中心,当然,英雄在地图边界位置的情况除外。
关于这个方法的详细解释,还是回头看看那篇老教程吧(())。
编译运行游戏,现在英雄就不会再从你的视野中消失了。机器人神马的现身吧,爷准备好了
好吧,不要拐杖自由行走可能有点意思,不过英雄独嫌寂寞啊,让爷在空荡荡的走廊里晃来晃去实在是没意思。说好的Androids机器人呢?
好吧,战便战!
之前我们已经创建了精灵的基本模型ActionSprite。我们大可以重用该类来创造由游戏系统控制的角色。
这部分教程会一带而过,因为和创建英雄的代码没有太大区别。
点击command-n来创建一个新类,选择iOS\Cocos2D v2.x\CCNode Class模板,将其设置为ActionSprite的子类,并命名为Robot。
切换到Robot.h,并在文件顶部添加以下代码:#import"ActionSprite.h"然后切换到Robot.m,并添加以下方法:
-(id)init {
if ((self = [super initWithSpriteFrameName:@"robot_idle_00.png"])) {
//idle animation
CCArray *idleFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_idle_%02d.png", i]];
[idleFrames addObject:frame];
CCAnimation *idleAnimation = [CCAnimation animationWithSpriteFrames:[idleFrames getNSArray] delay:1.0/12.0];
self.idleAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:idleAnimation]];
//attack animation
CCArray *attackFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_attack_%02d.png", i]];
[attackFrames addObject:frame];
CCAnimation *attackAnimation = [CCAnimation animationWithSpriteFrames:[attackFrames getNSArray] delay:1.0/24.0];
self.attackAction = [CCSequence actions:[CCAnimate actionWithAnimation:attackAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
//walk animation
CCArray *walkFrames = [CCArray arrayWithCapacity:6];
for (i = 0; i < 6; i++) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_walk_%02d.png", i]];
[walkFrames addObject:frame];
CCAnimation *walkAnimation = [CCAnimation animationWithSpriteFrames:[walkFrames getNSArray] delay:1.0/12.0];
self.walkAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:walkAnimation]];
self.walkSpeed = 80;
self.centerToBottom = 39.0;
self.centerToSides = 29.0;
self.hitPoints = 100;
self.damage = 10;
和英雄一样,以上代码创建了一个具备三种动作状态的机器人:发呆,攻击和行走。我们同时还设置了一些基本属性和测量辅助变量。
当然,你会发现机器人的属性比英雄要低那么一点点。这是理所应当的,如果对方的小弟都比本家大哥牛,爷还怎么混江湖?
现在来添加一堆机器人小弟吧,你已经肾上腺激素分泌过度了,不是吗?
切换到GameLayer.h,并添加以下属性:@property(nonatomic,strong)CCArray*然后切换到GameLayer.m,并进行以下操作:
//在文件的顶部添加:
#import "Robot.h"
//然后在init方法中添加一行代码:
[selfinitRobots];
//接着添加initRobots方法的实现代码:
-(void)initRobots {
int robotCount = 50;
self.robots = [[CCArray alloc] initWithCapacity:robotCount];
for (int i = 0; i < robotC i++) {
Robot *robot = [Robot node];
[_actors addChild:robot];
[_robots addObject:robot];
int minX = SCREEN.width + robot.centerToS
int maxX = _tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToS
int minY = robot.centerToB
int maxY = 3 * _tileMap.tileSize.height + robot.centerToB
robot.scaleX = -1;
robot.position = ccp(random_range(minX, maxX), random_range(minY, maxY));
robot.desiredPosition = robot.
[robot idle];
以上方法的作用是:
创建了包含50个机器人的数组,并将它们添加到精灵表单中。2.
Defines.h中创建的宏定义来随即安放机器人的位置。注意这里将最小的随机数设置为大于屏幕宽度,以免英雄一开始就身陷重围。3.
让每个机器人站着发呆。编译运行游戏,英雄不再寂寞,一群2B小弟等着你来战。试着穿行在这些暂时毫无威胁性的机器人中坚,你会发现有点不对,当你走到机器人中间的时候,会发现机器人挡住了英雄的虎躯,这可不对。
为了解决这个问题,我们得明确告诉游戏应首先绘制哪些精灵。很简单,只需要调整z值就好了。
还记得GameLayer.m里面添加角色和背景的代码吗?
//you set the z-value of tileMap lower than actors, so anything drawn in actors appears in front of tileMap
[self addChild:_actors z:-5];
//this is your CCSpriteBatchNode
[self addChild:_tileMap z:-6];
//this is your CCTMXTiledMap
然后来看看我们是如何添加英雄和机器人的:
[_actorsaddChild:_hero];
[_actorsaddChild:robot];
这里有两处区别:
1. 精灵表单和瓦片地图都是直接添加为GameLayer的子节点,而英雄和机器人则是添加为精灵表单的子节点。GameLayer负责以合理的顺序绘制精灵表单和瓦片地图,而精灵表单则应负责以正确的顺序来绘制不同的角色。
这里没有明确说明英雄和机器人的z值。但默认情况下,后面添加的精灵对象会有更高的z值。这就是为什么机器人会挡住英雄的虎躯。为了解决这个问题,我们需要动态处理z值。每当精灵对象沿着屏幕垂直运动的时候,其z值都应改变。精灵在屏幕中的位置越高,其z值就应越低。
在GameLayer.m中进行以下操作:
添加一个新的方法:
//add this method inside the @implementation
-(void)reorderActors {
ActionSprite *
CCARRAY_FOREACH(_actors.children, sprite) {
[_actors reorderChild:sprite z:(_tileMap.mapSize.height * _tileMap.tileSize.height) - sprite.position.y];
//add this method inside update, right after [self updatePositions]
[self reorderActors];
然后在update:方法中添加一行代码:
[selfreorderActors];
每次更新精灵的位置时,以上方法都会让精灵表单重新设置每个子节点的z值,其依据是子节点离地图底部的距离。当子节点离地图底部更远时,其z值就会更低。
注意:每个CCNode节点都有一个zOrder属性,但直接修改该属性并不会带来同样的效果。这是因为角色是精灵表单的子节点,因此应该由精灵表单来设置子节点的z值。
编译运行游戏,现在角色的绘制顺序应该是正常的了。
征战四方- 碰撞检测
现在拳击对象也有了,英雄不再寂寞,但如果和他们一起发呆,实在没什么意思。现在该是让机器人吃点苦头的时候了。
为了让英雄可以实际伤害到机器人,我们需要找到一个方法来实现碰撞检测。这里我们会创建一个非常简单的碰撞检测系统。在这个游戏中,我们为每个角色定义了两个矩形/盒子:
挨打盒子(hit box):代表精灵的身体2.
攻击盒子(attack box):代表精灵的拳头如果某个ActionSprite的攻击盒子碰到另一个精灵的挨打盒子,就代表发生了一次亲密的身体接触。当然,我们也能就此知道是谁打了谁。
还记得之前Defines.h中所定义的包围盒吗?
typedefstruct _BoundingBox{
}BoundingB
每个包围盒都有两个矩形:实际的,和初始的
1. 初始矩形是每个精灵对象的基本矩形,一旦设置后就不会改变。
实际矩形则是在世界空间中的矩形。当精灵移动的时候,该矩形也会跟着一起移动。切换到ActionSprite.h,并添加以下代码:
@property(nonatomic,assign)BoundingBox hitB
@property(nonatomic,assign)BoundingBox attackB
-(BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin size:(CGSize)
以上代码定义了ActionSprite的两个包围盒:挨打盒,攻击盒。同时还定义了一个工厂方法用于创建包围盒,其作用是根据某个原点位置和矩形大小来创建包围盒。
切换到ActionSprite.m,并添加以下方法:
-(BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin size:(CGSize)size {
BoundingBox boundingB
boundingBox.original.origin =
boundingBox.original.size =
boundingBox.actual.origin = ccpAdd(position_, ccp(boundingBox.original.origin.x, boundingBox.original.origin.y));
boundingBox.actual.size =
return boundingB
-(void)transformBoxes {
_hitBox.actual.origin = ccpAdd(position_, ccp(_hitBox.original.origin.x * scaleX_, _hitBox.original.origin.y * scaleY_));
_hitBox.actual.size = CGSizeMake(_hitBox.original.size.width * scaleX_, _hitBox.original.size.height * scaleY_);
_attackBox.actual.origin = ccpAdd(position_, ccp(_attackBox.original.origin.x * scaleX_, _attackBox.original.origin.y * scaleY_));
_attackBox.actual.size = CGSizeMake(_attackBox.original.size.width * scaleX_, _attackBox.original.size.height * scaleY_);
-(void)setPosition:(CGPoint)position {
[super setPosition:position];
[self transformBoxes];
其中第一个方法用于创建一个新的包围盒,从而有助于ActionSprite的子类创建属于自己的包围盒。
第二个方法transformBoxes则根据精灵的位置和缩放来更新实际包围盒的远点和大小。这里之所以要用到缩放的概念,是因为scale可以帮助确定精灵的朝向。
切换到Hero.m,并在initf方法的if ((self = [super initWithSpriteFrameName])条件语句中创建新的包围盒:
//add inside if ((self = [super initWithSpriteFrameName)) after self.centerToSide
// Create bounding boxes
self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides, -self.centerToBottom) size:CGSizeMake(self.centerToSides * 2, self.centerToBottom * 2)];
self.attackBox = [self createBoundingBoxWithOrigin:ccp(self.centerToSides, -10) size:CGSizeMake(20, 20)];
类似的,切换到Robot.m,并在init方法的if ((self = [super initWithSpriteFrameName])条件语句中创建新的包围盒:
//add inside if ((self = [super initWithSpriteFrameName)) after self.centerToSide
// Create bounding boxes
self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides, -self.centerToBottom) size:CGSizeMake(self.centerToSides * 2, self.centerToBottom * 2)];
self.attackBox = [self createBoundingBoxWithOrigin:ccp(self.centerToSides, -5) size:CGSizeMake(25, 20)];
Ok,现在我们已经有了英雄和机器人的挨打盒和攻击盒。其视觉效果如下:每当某个attack box(红色)碰到hit box(蓝色)时,就代表发生了碰撞。
在编写检查包围盒碰撞的代码前,我们还要确保ActionSprite会对碰撞做出适当的反应。之前我们已经实现了发呆,攻击和行走动作,但还没有创建受伤和死亡的动作。
既然战斗,岂能没有伤痛?!切换到ActionSprite.m,并添加以下方法:
-(void)hurtWithDamage:(float)damage {
if (_actionState != kActionStateKnockedOut) {
[self stopAllActions];
[self runAction:_hurtAction];
_actionState = kActionStateH
_hitPoints -=
if (_hitPoints <= 0.0) {
[self knockout];
-(void)knockout {
[self stopAllActions];
[self runAction:_knockedOutAction];
_hitPoints = 0.0;
_actionState = kActionStateKnockedO
只要精灵对象还没死,那么受到伤害时就会将其状态切换到受伤,然后运行受伤的动画,并从精灵的血条中减去相应的血量。如果血量少于0,那么就会进入死亡状态。
为了完成这两个动作,我们还需要更改Hero和Robot类。
切换到Hero.m,然后在init方法中添加以下代码:
//hurt animation
CCArray *hurtFrames = [CCArray arrayWithCapacity:8];
for (i = 0; i < 3; i++) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_hurt_%02d.png", i]];
[hurtFrames addObject:frame];
CCAnimation *hurtAnimation = [CCAnimation animationWithSpriteFrames:[hurtFrames getNSArray] delay:1.0/12.0];
self.hurtAction = [CCSequence actions:[CCAnimate actionWithAnimation:hurtAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
//knocked out animation
CCArray *knockedOutFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_knockout_%02d.png", i]];
[knockedOutFrames addObject:frame];
CCAnimation *knockedOutAnimation = [CCAnimation animationWithSpriteFrames:[knockedOutFrames getNSArray] delay:1.0/12.0];
self.knockedOutAction = [CCSequence actions:[CCAnimate actionWithAnimation:knockedOutAnimation], [CCBlink actionWithDuration:2.0 blinks:10.0], nil];
然后切换到Robot.m,并在init方法中添加相应的动画代码:
//hurt animation
CCArray *hurtFrames = [CCArray arrayWithCapacity:8];
for (i = 0; i < 3; i++) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_hurt_%02d.png", i]];
[hurtFrames addObject:frame];
CCAnimation *hurtAnimation = [CCAnimation animationWithSpriteFrames:[hurtFrames getNSArray] delay:1.0/12.0];
self.hurtAction = [CCSequence actions:[CCAnimate actionWithAnimation:hurtAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
//knocked out animation
CCArray *knockedOutFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_knockout_%02d.png", i]];
[knockedOutFrames addObject:frame];
CCAnimation *knockedOutAnimation = [CCAnimation animationWithSpriteFrames:[knockedOutFrames getNSArray] delay:1.0/12.0];
self.knockedOutAction = [CCSequence actions:[CCAnimate actionWithAnimation:knockedOutAnimation], [CCBlink actionWithDuration:2.0 blinks:10.0], nil];
这些你应该都不陌生了。我们用创建其它动作类似的方式创建了受伤和死亡动作。当受伤动作结束时会切换到发呆状态,而死亡动作则会让精灵对象在动画后消失。
此时切换到GameLayer.m,并正式添加碰撞检测代码:
//在ccTouchesBegan方法中,在[_hero attack];这行代码后添加以下代码:
//add this inside ccTouchesBegan, right after [_hero attack];
if (_hero.actionState == kActionStateAttack) {
CCARRAY_FOREACH(_robots, robot) {
if (robot.actionState != kActionStateKnockedOut) {
if (fabsf(_hero.position.y - robot.position.y) < 10) {
if (CGRectIntersectsRect(_hero.attackBox.actual, robot.hitBox.actual)) {
[robot hurtWithDamage:_hero.damage];
以上代码通过三步来进行碰撞检测:
检查英雄的状态是否处于攻击状态,以及机器人是否处于非死亡状态下的其它状态。2.
检查英雄和机器人的位置的y坐标距离在10点以内,这就意味着他们站在同一位置。3.
检查英雄的attack box攻击盒是否和机器人的hit box交汇在一起,如果是,就让机器人受伤。一旦所有这些条件都符合了,那么当碰撞发生时,就会运行机器人的受伤动作。英雄的伤害值会作为参数传递,这样该方法就知道需要减去多少血。
编译运行游戏,开战吧!机器人的复仇(简单AI的实现)
爷不断挥拳,所向披靡,但这样似乎一点挑战性都没有?
同时,为了结束一个游戏,还需要游戏的赢输机制。现在你可以轻松单秒地图上的所有机器人,但什么也不会发生。既然是横版过关,那么肯定希望在单秒所有敌人或者被敌人秒了后能结束游戏。
为了让敌人能秒掉英雄(受虐倾向?),你得让机器人学会主动攻击英雄,这就涉及到了简单的AI系统。
这个简单的AI系统基于决策机制而来。在特定的时间间隔里,我们将给每个机器人一次机会来决定接下来该做什么。首先得让他们“活”过来,知道自己该何时做出选择。
切换到Robot.h,并添加以下属性:
@property(nonatomic,assign)doublenextDecisionT
然后切换到Robot.m,并在init方法的if ((self = [super initWithSpriteFrameName]))条件语句中初始化该属性值。
_nextDecisionTime=0;
然后切换到GameLayer.m,并添加以下方法:
-(void)updateRobots:(ccTime)dt {
int alive = 0;
float distanceSQ;
int randomChoice = 0;
CCARRAY_FOREACH(_robots, robot) {
[robot update:dt];
if (robot.actionState != kActionStateKnockedOut) {
if (CURTIME > robot.nextDecisionTime) {
distanceSQ = ccpDistanceSQ(robot.position, _hero.position);
if (distanceSQ
robot.position.x) {
robot.scaleX = 1.0;
robot.scaleX = -1.0;
[robot attack];
if (robot.actionState == kActionStateAttack) {
if (fabsf(_hero.position.y - robot.position.y) < 10) {
if (CGRectIntersectsRect(_hero.hitBox.actual, robot.attackBox.actual)) {
[_hero hurtWithDamage:robot.damage];
//end game checker here
[robot idle];
} else if (distanceSQ <= SCREEN.width * SCREEN.width) {
robot.nextDecisionTime = CURTIME + frandom_range(0.5, 1.0);
randomChoice = random_range(0, 2);
if (randomChoice == 0) {
CGPoint moveDirection = ccpNormalize(ccpSub(_hero.position, robot.position));
[robot walkWithDirection:moveDirection];
[robot idle];
//end game checker here
这段代码看起来有点吓人,不过别担心,我们还是一步步来解释吧。
对于游戏中的每个机器人:
我们使用一个变量来保存仍然存活的机器人数量。只要机器人不处于死亡状态,就代表它还活着。该变量将用于判断何时游戏可以结束。2.
判断当前系统时间是否超过了机器人的下次决策时间。如果是,就说明该让机器人做一个新的决策了。CURTIME是我们之前所定义的一个宏。3.
检查机器人离英雄的距离,以判断它是否有机会和英雄取得某种联系。如果是,则让机器人做一个随机决定,让其面向英雄挥拳,或继续沉思。4.
如果机器人决定攻击,则和之前检查英雄的攻击一样进行碰撞检测。只不过这一次挨打的是英雄。5.
如果机器人和英雄的距离小于屏幕宽度,那么机器人就需要决定究竟是走向英雄,还是继续沉思。机器人的运动路线取决于英雄的位置和机器人的位置共同得出的正交向量。每当机器人做出某个决策后,它的下一次决策时间将设置为未来的某个随机时间点。同时,它还需要继续执行上次决策时间所做出的决策动作。
仍然在GameLayer.m中,进行以下操作:
//在update:方法中添加以下代码:
[selfupdateRobots:dt];
在updatePositions方法中,在_hero.position = ccp(posX, posY);这行代码后添加以下代码:
//add inside update, right before [self updatePositions];
[self updateRobots:dt];
//add inside the updatePositions method, right after _hero.position = ccp(posX, posY);
// Update robots
CCARRAY_FOREACH(_robots, robot) {
posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToSides, MAX(robot.centerToSides, robot.desiredPosition.x));
posY = MIN(3 * _tileMap.tileSize.height + robot.centerToBottom, MAX(robot.centerToBottom, robot.desiredPosition.y));
robot.position = ccp(posX, posY);
在以上代码中,我们首先在update:方法中保证游戏循环会调用机器人的AI机制。接下来在updatePositions方法中我们遍历了所有的机器人,并让它们朝目标位置移动。
编译运行游戏,准备迎接机器人的疯狂反扑吧。继续玩游戏,直到所有的机器人都被消灭,或者英雄被秒掉。这时候游戏就卡住了。如果你看过之前的这篇游戏教程(/14302/how-to-make-a-game-like-fruit-ninja-with-box2d-and-cocos2d-part-1),就该知道我们需要添加一个按钮来重置游戏了。
在GameLayer.m中进行以下操作:
//在文件顶部添加以下代码;
#import"GameScene.h"
然后添加以下方法:
//add to top of file
#import "GameScene.h"
//add these methods inside @implementation
-(void)endGame {
CCLabelTTF *restartLabel = [CCLabelTTF labelWithString:@"RESTART" fontName:@"Arial" fontSize:30];
CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartGame)];
CCMenu *menu = [CCMenu menuWithItems:restartItem, nil];
menu.position = CENTER;
menu.tag = 5;
[_hud addChild:menu z:5];
-(void)restartGame {
[[CCDirector sharedDirector] replaceScene:[GameScene node]];
endGame方法用于创建一个Restart按钮,当触碰该按钮时就会触发restartGame方法。而restartGame方法的作用很简单,就是重新加载游戏场景。
然后回到updateRobots:方法,在第一个占位符//end game checker here处添加以下代码:
if (_hero.actionState == kActionStateKnockedOut && [_hud getChildByTag:5] == nil) {
[self endGame];
//然后在第二个占位符//end game checker here处添加以下代码:
if (alive == 0 && [_hud getChildByTag:5] == nil) {
[self endGame];
以上方法的作用都是检查游戏结束条件。第一个是判断英雄是否死亡,如果被秒了,当然就结束了。
而第二个方法的作用是判断还存活的机器人数量是否已经是0了,如果是,当然也结束了。
注意到还有一个判断条件是HudLayer层tag值为5的子节点是否为空,这是因为结束游戏的标签的tag值就是5。这样可以避免游戏陷入死循环。
编译运行游戏,随便发飙吧!
音乐和音效
现在一切就绪,但听不到机器人的凄惨嚎叫似乎满足不了你内心的某种渴望。该是添加一些音乐和音效的时候了!
本示例游戏的背景音乐要感谢来自Incompetech的Kevin Macleod(),而本人也用bfxr()自己创建了一些音效。
下面开始添加音乐和音效~
首先把资源包里面的Sounds文件夹中的文件拖到项目的Resources中,确保选中opy items into destination group’s folder和Create groups for any added folders。切换到GameLayer.m,并进行以下操作:
//在文件的顶部添加一行代码:
#import "SimpleAudioEngine.h"
//然后在init方法的if条件语句中添加以下代码: ((self = [super init])) in init
// Load audio
[[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:@"latin_industries.aifc"];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"latin_industries.aifc"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit0.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit1.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_herodeath.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_botdeath.caf"];
然后切换到ActionSprite.m,并进行以下操作:
//在文件顶部添加代码:
#import "SimpleAudioEngine.h"
//然后在hurtWithDamage方法的第一个if条件语句中(在第二个if条件语句前)添加以下代码:
int randomSound = random_range(0, 1);
[[SimpleAudioEngine sharedEngine] playEffect:[NSString stringWithFormat:@"pd_hit%d.caf", randomSound]];
然后切换到Hero.m,并进行以下操作:
//在文件的顶部添加一行代码:
#import "SimpleAudioEngine.h"
//然后添加以下方法:
-(void)knockout {
[super knockout];
[[SimpleAudioEngine sharedEngine] playEffect:@"pd_herodeath.caf"];
}最后切换到Robot.m,并进行以下操作:
//在文件的顶部添加一行代码:
#import "SimpleAudioEngine.h"
//然后添加以下方法:
-(void)knockout {
[super knockout];
[[SimpleAudioEngine sharedEngine] playEffect:@"pd_botdeath.caf"];
以上代码很简单,就是在特定的场合下播放音乐和音效而已。
OK,编译运行游戏,一切大功告成了!
这里是到目前为止的项目源代码()。
更多好消息:
如果你喜欢2D横版过关游戏,那么可以期待作者的Starter Kit,即将在发售。
其中包含了以下更多的内容:
1. 更多动作:3连击,跳跃,奔跑
2. 组合动作:跳跃攻击,奔跑攻击
4. 8向方向键
5. 动画D-pad按钮
6. 状态机应用到游戏事件中,比如敌人蜂拥而出时的战役事件
7. 更多关卡,使用plist文件来设置事件
8. 更多类型的敌人
更聪明的敌人AI11.
更好的碰撞检测机制,使用圆形替代矩形12.
根据动作来调整碰撞圆形,并将圆形绘制到屏幕中以方便调试13.
英雄将拥有新的武器:Gauntlet14.
可修建的地图对象15.
可视化的伤害16.
更多更多。。。期待作者的爆发吧。
相关文章推荐
泰然网手游开发官方群:
独立游戏人:

我要回帖

更多关于 cocos2d 的文章

 

随机推荐