步骤如下:
1.使用上一篇的工程;
2.移动英雄。在第一部分我们创建了虚拟方向键,但是还未实现按下方向键移动英雄,现在让我们进行实现。打开 Hero.cpp文件,在 init函数 attack animation后面,添加如下代码:
2 3 4 5 6 7 8 9 |
//walkanimation CCArray*walkFrames=CCArray::createWithCapacity( 8); for(i= 0;i< 8;i++) { CCSpriteFrame*frame=CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat( "hero_walk_%02d.png",i)->getCString()); walkFrames->addObject(frame); } CCAnimation*walkAnimation=CCAnimation::createWithSpriteFrames(walkFrames,float( 1. 0/ 12. 0)); this->setWalkAction(CCRepeatForever::create(CCAnimate::create(walkAnimation))); |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
voidActionSprite::walkWithDirection(CCPointdirection)
{ if(_actionState==kActionStateIdle) { this->stopAllActions(); this->runAction(_walkAction); _actionState=kActionStateWalk; } if(_actionState==kActionStateWalk) { _velocity=ccp(direction.x*_walkSpeed,direction.y*_walkSpeed); if(_velocity.x>= 0) { this->setScaleX( 1. 0); } else { this->setScaleX(- 1. 0); } } } |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
voidGameLayer::didChangeDirectionTo(SimpleDPad*simpleDPad,CCPointdirection)
{ _hero->walkWithDirection(direction); } voidGameLayer::isHoldingDirection(SimpleDPad*simpleDPad,CCPointdirection) { _hero->walkWithDirection(direction); } voidGameLayer::simpleDPadTouchEnded(SimpleDPad*simpleDPad) { if(_hero->getActionState()==kActionStateWalk) { _hero->idle(); } } |
此时,编译运行程序的话,通过方向键移动英雄,发现英雄只是原地踏步。改变英雄的位置是ActionSprite和GameLayer共同的责任。一个ActionSprite永远不会知道它在地图上的位置。因此,它并不知道已经到达了地图的边缘,它只知道它想去哪里。而GameLayer的责任就是将它的期望位置转换成实际的位置。打开ActionSprite.cpp文件,实现以下方法:
2 3 4 5 6 7 |
voidActionSprite::update(
floatdt)
{ if(_actionState==kActionStateWalk) { _desiredPosition=ccpAdd( this->getPosition(),ccpMult(_velocity,dt)); } } |
|
this->scheduleUpdate();
|
2 3 4 |
GameLayer::~GameLayer(
void)
{ this->unscheduleUpdate(); } |
2 3 4 5 6 7 8 9 10 11 12 13 14 |
voidGameLayer::update(
floatdt)
{ _hero->update(dt); this->updatePositions(); } voidGameLayer::updatePositions() { floatposX=MIN(_tileMap->getMapSize().width*_tileMap->getTileSize().width-_hero->getCenterToSides(), MAX(_hero->getCenterToSides(),_hero->getDesiredPosition().x)); floatposY=MIN( 3*_tileMap->getTileSize().height+_hero->getCenterToBottom(), MAX(_hero->getCenterToBottom(),_hero->getDesiredPosition().y)); _hero->setPosition(ccp(posX,posY)); } |
设定GameLayer的更新方法,每次循环时,GameLayer让英雄更新它的期望位置,然后通过以下这些值,将期望位置进行检查是否在地图地板的范围内:
- mapSize:地图tile数量。总共有10x100个tile,但只有3x100属于地板。
tileSize:每个tile的尺寸,在这里是32x32像素。
GameLayer还使用到了ActionSprite的两个测量值,centerToSides和centerToBottom,因为ActionSprite要想保持在场景内,它的位置不能超过实际的精灵边界。假如ActionSprite的位置在已经设置的边界内,则GameLayer让英雄达到期望位置,否则GameLayer会让英雄留停在原地。
3.编译运行,此时点击方向键,移动英雄,如下图所示:
但是,很快你就会发现英雄可以走出地图的右边界,然后就这样从屏幕上消失了。
4.以上的问题,可以通过基于英雄的位置进行滚动地图,这个方法在文章《如何制作一个基于Tile的游戏》中有描述过。打开GameLayer.cpp文件,在update函数里最后添加如下代码:
以上代码让英雄处于屏幕中心位置,当然,英雄在地图边界时的情况除外。编译运行,效果如下图所示:
5.创建机器人。我们已经创建了精灵的基本模型:ActionSprite。我们可以重用它来创建游戏中电脑控制的角色。新建Robot类,派生自ActionSprite类,增加如下方法:
2 |
CREATE_FUNC(Robot);
boolinit(); |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
boolRobot::init()
{ boolbRet= false; do { CC_BREAK_IF(!ActionSprite::initWithSpriteFrameName( "robot_idle_00.png")); inti; //idleanimation CCArray*idleFrames=CCArray::createWithCapacity( 5); for(i= 0;i< 5;i++) { CCSpriteFrame*frame=CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName( CCString::createWithFormat( "robot_idle_%02d.png",i)->getCString()); idleFrames->addObject(frame); } CCAnimation*idleAnimation=CCAnimation::createWithSpriteFrames(idleFrames,float( 1. 0/ 12. 0)); this->setIdleAction(CCRepeatForever::create(CCAnimate::create(idleAnimation))); //attackanimation CCArray*attackFrames=CCArray::createWithCapacity( 5); for(i= 0;i< 5;i++) { CCSpriteFrame*frame=CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName( CCString::createWithFormat( "robot_attack_%02d.png",i)->getCString()); attackFrames->addObject(frame); } CCAnimation*attackAnimation=CCAnimation::createWithSpriteFrames(attackFrames,float( 1. 0/ 24. 0)); this->setAttackAction(CCSequence::create(CCAnimate::create(attackAnimation),CCCallFunc::create( this,callfunc_selector(Robot::idle)),NULL)); //walkanimation CCArray*walkFrames=CCArray::createWithCapacity( 6); for(i= 0;i< 6;i++) { CCSpriteFrame*frame=CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName( CCString::createWithFormat( "robot_walk_%02d.png",float( 1. 0/ 12. 0)); this->setWalkAction(CCRepeatForever::create(CCAnimate::create(walkAnimation))); this->setWalkSpeed( 80. 0); this->setCenterToBottom( 39. 0); this->setCenterToSides( 29. 0); this->setHitPoints( 100. 0); this->setDamage( 10. 0); bRet= true; } while( 0); returnbRet; } |
跟英雄一样,以上代码创建一个带有3个动作的机器人:空闲、出拳、行走。它也有两个测量值:centerToBottom和centerToSides。注意到机器人的属性比英雄低一点,这是合乎逻辑的,不然英雄永远打不赢机器人。让我们开始添加一些机器人到游戏中去。打开GameLayer.h文件,添加如下代码:
这些代码做了以下事情:
- 创建一个包含50个机器人的数组,并把它们添加到精灵表单中。
- 使用Defines.h里面的随机函数随机放置50个机器人到地图地板上。同时,让最小随机值大于屏幕宽度,以确保不会有任何机器人出现在起点处。
- 让每个机器人都处于空闲状态。
编译运行,让英雄向前走,直到看到地图上的机器人,如下图所示:
试着走到机器人区域中,你会发现机器人的绘制有些不对。如果英雄是在机器人的下面,那么他应该被绘制在机器人的前面,而不是在后面。我们需要明确的告诉游戏,哪个对象先绘制,这就是Z轴来进行控制的。添加英雄和机器人时,并没有明确指定其Z轴,默认下,后面添加的对象会比前面的对象Z轴值高,这就是为什么机器人挡住了英雄。为了解决这个问题,我们需要动态的处理Z轴顺序。每当精灵在屏幕上垂直移动时,它的Z轴值应该有所改变。屏幕上越高的精灵,其Z轴值应越低。打开GameLayer.cpp文件,添加如下方法:
6.出拳猛击机器人,碰撞检测。为了让英雄能够出拳,并且能够实际上打在了机器人身上,需要实现一种方式的碰撞检测。在这篇文章中,我们使用矩形创建一个非常简单的碰撞检测系统。在这个系统中,我们为每个角色定义两种矩形/盒子:
假如某个ActionSprite的Attack Box碰撞到另一个ActionSprite的Hit Box,那么这就是一次碰撞发生。这两个矩形之间的区别,将帮助我们知道谁打了谁。Defines.h文件中的BoundingBox定义,包含两种矩形:实际的,和原始的:
①原始矩形,每个精灵的基本矩形,一旦设置后就不会改变。
②实际矩形,这是位于世界空间中的矩形,当精灵移动时,实际的矩形也跟着变动。
打开ActionSprite.h文件,添加如下代码:
以上创建了ActionSprite的两个包围盒:Hit Box和Attack Box。还定义了一个方法,用于根据给定的原点和大小来创建一个BoundingBox结构体。打开ActionSprite.cpp文件,添加如下方法:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
BoundingBoxActionSprite::createBoundingBoxWithOrigin(CCPointorigin,CCSizesize)
{ BoundingBoxboundingBox; boundingBox.original.origin=origin; boundingBox.original.size=size; boundingBox.actual.origin=ccpAdd( this->getPosition(),ccp(boundingBox.original.origin.x,boundingBox.original.origin.y)); boundingBox.actual.size=size; returnboundingBox; } voidActionSprite::transformBoxes() { _hitBox.actual.origin=ccpAdd( this->getPosition(),ccp(_hitBox.original.origin.x,_hitBox.original.origin.y)); _attackBox.actual.origin=ccpAdd( this->getPosition(),ccp(_attackBox.original.origin.x+ ( this->getScaleX()==- 1?(-_attackBox.original.size.width-_hitBox.original.size.width): 0), _attackBox.original.origin.y)); } voidActionSprite::setPosition(CCPointposition) { CCSprite::setPosition(position); this->transformBoxes(); } |
第一个方法创建一个新的包围盒,这有助于ActionSprite的子类创建属于它们自己的包围盒。第二个方法,基于精灵的位置、比例因子,和包围盒原本的原点和大小来更新每个包围盒实际测量的原点和大小。之所以要用到比例因子,是因为它决定着精灵的方向。位于精灵右侧的盒子,当比例因子设置为-1时,将会翻转到左侧。打开Hero.cpp文件,在init函数后面添加如下代码:
2 3 |
无论何时,当一个attack Box(红色)跟一个hit Box(蓝色)交叉,即一次碰撞发生。在开始编写代码,检测包围盒交叉前,需要确保 ActionSprite能够对被击中有所反应。我们已经添加了空闲、出拳、行走动作,但还未创建受伤和死亡动作。打开 ActionSprite.cpp文件,实现如下方法:
只要精灵还未死亡,被击中时状态将会切换到受伤状态,执行受伤动画,并且精灵的生命值将会减去相应的伤害值。如果生命值少于0,那么死亡的动作将会触发。为了完成这两个动作,我们还需更改Hero类和Robot类。打开Hero.cpp文件,在init函数walk animation后面添加如下代码:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//hurtanimation CCArray*hurtFrames=CCArray::createWithCapacity( 3); for(i= 0;i< 3;i++) { CCSpriteFrame*frame=CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat( "hero_hurt_%02d.png",i)->getCString()); hurtFrames->addObject(frame); } CCAnimation*hurtAnimation=CCAnimation::createWithSpriteFrames(hurtFrames,float( 1. 0/ 12. 0)); this->setHurtAction(CCSequence::create(CCAnimate::create(hurtAnimation),callfunc_selector(Hero::idle)),NULL)); //knockedoutanimation CCArray*knockedOutFrames=CCArray::createWithCapacity( 5); for(i= 0;i< 5;i++) { CCSpriteFrame*frame=CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat( "hero_knockout_%02d.png",i)->getCString()); knockedOutFrames->addObject(frame); } CCAnimation*knockedOutAnimation=CCAnimation::createWithSpriteFrames(knockedOutFrames,float( 1. 0/ 12. 0)); this->setKnockedOutAction(CCSequence::create(CCAnimate::create(knockedOutAnimation),CCBlink::create( 2. 0,10. 0),NULL)); |
打开Robot.cpp文件,在init函数walk animation后面添加如下代码:
以上代码应该不陌生了。我们用创建其他动作同样的方式创建了受伤和死亡动作。受伤动作结束时,会切换到空闲状态。死亡动作结束时,精灵进行闪烁。打开GameLayer.cpp文件,添加碰撞处理,在ccTouchesBegan函数后面添加如下代码:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
if(_hero->getActionState()==kActionStateAttack)
{ CCObject*pObject= NULL; CCARRAY_FOREACH(_robots,pObject) { Robot*robot=(Robot*)pObject; if(robot->getActionState()!=kActionStateKnockedOut) { if(fabsf(_hero->getPosition().y-robot->getPosition().y)< 10) { if(_hero->getAttackBox().actual.intersectsRect(robot->getHitBox().actual)) { robot->hurtWithDamage(_hero->getDamage()); } } } } } |
①.检测英雄是否处于攻击状态,以及机器人是否处于非死亡状态。
②.检测英雄的位置和机器人的位置垂直相距在10个点以内。这表明它们在同一平面上站立。
③.检测英雄的attack Box是否与机器人的hit Box进行交叉。
如果这些条件都成立,那么则一次碰撞发生,机器人执行受伤动作。英雄的伤害值作为参数进行传递,这样该方法就会知道需要减去多少生命值。
7.编译运行,出拳攻击机器人吧,效果如下图所示:
8.简单机器人AI的实现。为了使机器人能够移动,并且能够使用我们为它们所创建的动作,就需要开发一个简单的AI(人工智能)系统。这个AI系统基于决策机制。在特定的时间间隔里,我们给每个机器人一个机会来决定接下来该做什么。它们需要知道的第一件事情就是何时做出选择。打开 Robot.h文件,添加如下代码:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#pragmaonce
#include "cocos2d.h" //1-conveniencemeasurements #defineSCREENCCDirector::sharedDirector()->getWinSize() #defineCENTERccp(SCREEN.width/ 2,SCREEN.height/ 2) #defineCURTIMEGetCurTime() //2-conveniencefunctions #ifndefUINT64_C #defineUINT64_C(val)val##ui64 #endif #definerandom_range(low,high)(rand()%(high-low+ 1))+low #definefrandom( float)rand()/UINT64_C(0x100000000) #definefrandom_range(low,high)((high-low)*frandom)+low //3-enumerations typedef enum_ActionState{ kActionStateNone= 0, kActionStateIdle, kActionStateAttack, kActionStateWalk, kActionStateHurt, kActionStateKnockedOut }ActionState; //4-structures typedef struct_BoundingBox{ cocos2d::CCRectactual; cocos2d::CCRectoriginal; }BoundingBox; inline floatGetCurTime(){ timevaltime; gettimeofday(&time,NULL); unsigned longmillisecs=(time.tv_sec* 1000)+(time.tv_usec/ 1000); return( float)millisecs; }; |
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
voidGameLayer::updateRobots(
floatdt)
{ intalive= 0; floatdistanceSQ; intrandomChoice= 0; CCObject*pObject= NULL; CCARRAY_FOREACH(_robots,pObject) { Robot*robot=(Robot*)pObject; robot->update(dt); if(robot->getActionState()!=kActionStateKnockedOut) { //1 alive++; //2 if(CURTIME>robot->getNextDecisionTime()) { distanceSQ=ccpDistanceSQ(robot->getPosition(),_hero->getPosition()); //3 if(distanceSQ<= 50* 50) { robot->setNextDecisionTime(CURTIME+frandom_range( 0. 1,0. 5)* 1000); randomChoice=random_range( 0,1); if(randomChoice== 0) { if(_hero->getPosition().x>robot->getPosition().x) { robot->setScaleX( 1. 0); } else { robot->setScaleX(- 1. 0); } //4 robot->setNextDecisionTime(robot->getNextDecisionTime()+frandom_range( 0. 1,0. 5)* 2000); robot->attack(); if(robot->getActionState()==kActionStateAttack) { if(fabsf(_hero->getPosition().y-robot->getPosition().y)< 10) { if(_hero->getHitBox().actual.intersectsRect(robot->getAttackBox().actual)) { _hero->hurtWithDamage(robot->getDamage()); //endgamecheckerhere } } } } else { robot->idle(); } } else if(distanceSQ<=SCREEN.width*SCREEN.width) { //5 robot->setNextDecisionTime(CURTIME+frandom_range( 0. 5,1. 0)* 1000); randomChoice=random_range( 0,2); if(randomChoice== 0) { CCPointmoveDirection=ccpNormalize(ccpSub(_hero->getPosition(),robot->getPosition())); robot->walkWithDirection(moveDirection); } else { robot->idle(); } } } } } //endgamecheckerhere } |
这是一个漫长的代码片段。将代码分解为一段段。对于游戏中的每个机器人:
①.使用一个计数来保存仍然存活着的机器人数量。一个机器人只要不是死亡状态,就被认为仍然存活着。这将用于判断游戏是否应该结束。
②.检查当前应用程序时间的推移是否超过了机器人的下一次决定时间。如果超过了,意味着机器人需要作出一个新的决定。
③.检查机器人是否足够接近英雄,以便于有机会出拳攻击落在英雄身上。如果接近英雄了,那么就进行一个随机选择,看是要朝着英雄出拳,还是要继续空闲着。
④.假如机器人决定攻击,我们就用之前检测英雄攻击时相同的方式来进行检测碰撞。只是这一次,英雄和机器人的角色互换了。
⑤.如果机器人和英雄之间的距离小于屏幕宽度,那么机器人将作出决定,要么朝着英雄移动,要么继续空闲。机器人的移动基于英雄位置和机器人位置产生的法向量。
每当机器人作出决定,它的下一个决定的时间被设定为在未来的一个随机时间。在此期间,它将继续执行上次作出决定时所做出的动作。接着在update函数里,this->updatePositions();前添加如下代码:
2 3 4 5 6 7 8 9 10 |
CCObject*pObject=
NULL;
CCARRAY_FOREACH(_robots,pObject) { Robot*robot=(Robot*)pObject; posX=MIN(_tileMap->getMapSize().width*_tileMap->getTileSize().width-robot->getCenterToSides(), MAX(robot->getCenterToSides(),robot->getDesiredPosition().x)); posY=MIN( 3*_tileMap->getTileSize().height+robot->getCenterToBottom(), MAX(robot->getCenterToBottom(),robot->getDesiredPosition().y)); robot->setPosition(ccp(posX,posY)); } |
9.编译运行,将会看到沿着走廊过来的机器人。效果如下图所示:
10.为游戏添加重新开始的按钮。打开 GameLayer.cpp文件,添加头文件引用:
第一个方法创建显示一个重新开始的按钮,当按下它时,触发第二个方法。后者只是命令导演用新的GameScene实例替换当前场景。接着在updateRobots函数里面,在第一个end game checker here注释后面,添加如下代码:
11.编译运行,可以看到游戏结束时的样子,如下图所示:
12.音乐和音效。打开GameLayer.cpp文件,添加头文件引用:
2 3 4 5 |
voidHero::knockout()
{ ActionSprite::knockout(); CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect( "pd_herodeath.wav"); } |
参考资料:
1.How To Make A Side-Scrolling Beat ‘Em Up Game Like Scott Pilgrim with Cocos2D – Part 2http://www.raywenderlich.com/24452/how-to-make-a-side-scrolling-beat-em-up-game-like-scott-pilgrim-with-cocos2d-part-2
2.如何使用cocos2d制作类似Scott Pilgrim的2D横版格斗过关游戏part2(翻译) http://blog.sina.com.cn/s/blog_4b55f6860101aaav.html
非常感谢以上资料,本例子源代码附加资源下载地址:http://download.csdn.net/detail/akof1314/5056794
如文章存在错误之处,欢迎指出,以便改正
扩展:
对此示例的内存泄露修正说明:《Cocos2d-x 2.0.4 小心隐藏的retain》