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



  本文实践自 Allen Tan 的文章《How To Make A Side-Scrolling Beat ‘Em Up Game Like Scott Pilgrim with Cocos2D – Part 1》,文中使用Cocos2D,我在这里使用Cocos2D-x 2.0.4进行学习和移植。在这篇文章,将会学习到如何制作一个简单的横版格斗过关游戏。在这当中,学习如何跟踪动画状态、碰撞盒、添加方向键、添加简单敌人AI和更多其它的。

步骤如下:
1.新建Cocos2d-win32工程,工程名为"PompaDroid",去除"Box2D"选项,勾选"Simple Audio Engine in Cocos Denshion"选项;
2.添加游戏场景类GameScene,派生自CCScene类。添加GameLayer类和HudLayer类,派生自CCLayer类。删除HelloWorldScene.hHelloWorldScene.cpp文件。
3.文件GameScene.h代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include  "cocos2d.h"
#include  "GameLayer.h"
#include  "HudLayer.h"

class GameScene :  public cocos2d::CCScene
{
public:
    GameScene( void);
    ~GameScene( void);

     virtual  bool init();
    CREATE_FUNC(GameScene);

    CC_SYNTHESIZE(GameLayer*, _gameLayer, GameLayer);
    CC_SYNTHESIZE(HudLayer*, _hudLayer, HudLayer);
};

文件GameScene.cpp代码如下:

1
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
#include  "GameScene.h"
using  namespace cocos2d;

GameScene::GameScene( void)
{
    _gameLayer =  NULL;
    _hudLayer =  NULL;
}

GameScene::~GameScene( void)
{
}

bool GameScene::init()
{
     bool bRet =  false;
     do 
    {
         CC_BREAK_IF(!CCScene::init());
         
         _gameLayer = GameLayer::create();
          this->addChild(_gameLayer,  0);
         _hudLayer = HudLayer::create();
          this->addChild(_hudLayer,  1);
         
         bRet =  true;
    }  while ( 0);

     return bRet;
}

4.HudLayer类增加一个方法:

1
CREATE_FUNC(HudLayer);

GameLayer类增加一个方法:

1
CREATE_FUNC(GameLayer);

5.修改AppDelegate.cpp文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//#include "HelloWorldScene.h"
#include  "GameScene.h"

bool AppDelegate::applicationDidFinishLaunching()
{
     //...

     // create a scene. it's an autorelease object
     //CCScene *pScene = HelloWorld::scene();
    CCScene *pScene = GameScene::create();

     //...
}

6.编译运行,此时只是空空的界面。
7.下载本游戏所需资源,将资源放置"Resources"目录下;

8.用Tiled工具打开pd_tilemap.tmx,就可以看到游戏的整个地图:

地图上有两个图层:Wall和Floor,即墙和地板。去掉每个图层前的打钩,可以查看层的组成。你会发现下数第四行是由两个图层一起组成的。每个tile都是32x32大小。可行走的地板tile位于下数三行。
9.打开GameLayer.h文件,添加如下代码:

1
2
3
4
bool init();
void initTileMap();

cocos2d::CCTMXTiledMap *_tileMap;

打开GameLayer.cpp,在构造函数,添加如下代码:

1
_tileMap =  NULL;

添加如下代码:

1
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
bool GameLayer::init()
{
     bool bRet =  false;
     do 
    {
        CC_BREAK_IF(!CCLayer::init());

         this->initTileMap();
        
        bRet =  true;
    }  while ( 0);

     return bRet;
}

void GameLayer::initTileMap()
{
    _tileMap = CCTMXTiledMap::create( "pd_tilemap.tmx");
    CCObject *pObject =  NULL;
    CCARRAY_FOREACH(_tileMap->getChildren(), pObject)
    {
        CCTMXLayer *child = (CCTMXLayer*)pObject;
        child->getTexture()->setAliasTexParameters();
    }
     this->addChild(_tileMap, - 6);
}

对所有图层进行setAliasTexParameters设置,该方法是关闭抗锯齿功能,这样就能保持像素风格。
10.编译运行,可以看到地图显示在屏幕上,如下图所示:

11.创建英雄。在大多数2D横版游戏中,角色有不同的动画代表不同类型的动作。我们需要知道什么时候播放哪个动画。这里采用状态机来解决这个问题。状态机就是某种通过切换状态来改变行为的东西。单一状态机在同一时间只能有一个状态,但可以从一种状态过渡到另一种状态。在这个游戏中,角色共有五种状态,空闲、行走、出拳、受伤、死亡,如下图所示:

为了有一个完整的状态流,每个状态应该有一个必要条件和结果。例如:行走状态不能突然转变到死亡状态,因为你的英雄在死亡前必须先受伤。
12.添加ActionSprite类,派生自CCSprite类,ActionSprite.h文件代码如下:

1
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
#pragma once
#include  "cocos2d.h"
#include  "Defines.h"

class ActionSprite :  public cocos2d::CCSprite
{
public:
    ActionSprite( void);
    ~ActionSprite( void);

     //action methods
     void idle();
     void attack();
     void hurtWithDamage( float damage);
     void knockout();
     void walkWithDirection(cocos2d::CCPoint direction);

     //scheduled methods
     void update( float dt);

     //actions
    CC_SYNTHESIZE_RETAIN(cocos2d::CCAction*, _idleAction, IdleAction);
    CC_SYNTHESIZE_RETAIN(cocos2d::CCAction*, _attackAction, AttackAction);
    CC_SYNTHESIZE_RETAIN(cocos2d::CCAction*, _walkAction, WalkAction);
    CC_SYNTHESIZE_RETAIN(cocos2d::CCAction*, _hurtAction, HurtAction);
    CC_SYNTHESIZE_RETAIN(cocos2d::CCAction*, _knockedOutAction, KnockedOutAction);

     //states
    CC_SYNTHESIZE(ActionState, _actionState, ActionState);

     //attributes
    CC_SYNTHESIZE( float, _walkSpeed, WalkSpeed);
    CC_SYNTHESIZE( float, _hitPoints, HitPoints);
    CC_SYNTHESIZE( float, _damage, Damage);

     //movement
    CC_SYNTHESIZE(cocos2d::CCPoint, _velocity, Velocity);
    CC_SYNTHESIZE(cocos2d::CCPoint, _desiredPosition, DesiredPosition);

     //measurements
    CC_SYNTHESIZE( float, _centerToSides, CenterToSides);
    CC_SYNTHESIZE( float, _centerToBottom, CenterToBottom);
};

打开ActionSprite.cpp文件,构造函数如下:

1
2
3
4
5
6
7
8
ActionSprite::ActionSprite( void)
{
    _idleAction =  NULL;
    _attackAction =  NULL;
    _walkAction =  NULL;
    _hurtAction =  NULL;
    _knockedOutAction =  NULL;
}

各个方法实现暂时为空。以上代码声明了基本变量和方法,可以分为以下几类:

  • Actions:这些是每种状态要执行的动作。这些动作是当角色切换状态时,执行精灵动画和其他触发的事件。
    States:保存精灵的当前动作/状态,使用ActionState类型,这个类型待会我们将会进行定义。
    Attributes:包含精灵行走速度值,受伤时减少生命点值,攻击伤害值。
    Movement:用于计算精灵如何沿着地图移动。
    Measurements:保存对精灵的实际图像有用的测量值。需要这些值,是因为你将要使用的这些精灵画布大小是远远大于内部包含的图像。
    Action methods:不直接调用动作,而是使用这些方法触发每种状态。
    Scheduled methods:任何事需要在一定的时间间隔进行运行,比如精灵位置和速度的更新,等等。

新建一个头文件Defines.h,代码如下:

1
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
#pragma once
#include  "cocos2d.h"

// 1 - convenience measurements
#define SCREEN CCDirector::sharedDirector()->getWinSize()
#define CENTER ccp(SCREEN.width /  2, SCREEN.height /  2)
#define CURTIME  do {                                                        \
    timeval time;                                                           \
    gettimeofday(&time,  NULL);                                              \
     unsigned  long millisecs = (time.tv_sec *  1000) + (time.tv_usec /  1000); \
     return ( float)millisecs;                                                \
while ( 0)

// 2 - convenience functions
#define random_range(low, high) (rand() % (high - low +  1)) + low
#define frandom ( float)rand() / UINT64_C(0x100000000)
#define frandom_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::CCRect actual;
    cocos2d::CCRect original;
} BoundingBox;

简要说明下:
①.定义了一些便利的宏,如直接使用SCREEN获取屏幕大小;
②.定义了一些便利的函数,随机返回整型或者浮点型;
③.定义ActionState类型,这个是ActionSprite可能处在不同状态的类型枚举;
④.定义BoundingBox结构体,将用于碰撞检测。
打开GameLayer.h文件,添加如下代码:

1
cocos2d::CCSpriteBatchNode *_actors;

打开GameLayer.cpp文件,在init函数里面添加如下代码:

1
2
3
4
CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile( "pd_sprites.plist");
_actors = CCSpriteBatchNode::create( "pd_sprites.pvr.ccz");
_actors->getTexture()->setAliasTexParameters();
this->addChild(_actors, - 5);

加载精灵表单,创建一个CCSpriteBatchNode。这个精灵表单包含我们的所有精灵。它的z值高于CCTMXTiledMap对象,这样才能出现在地图前。
添加Hero类,派生自ActionSprite类,添加如下代码:

1
2
CREATE_FUNC(Hero);
bool init();

Hero类的init函数的实现如下所示:

1
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
bool Hero::init()
{
     bool bRet =  false;
     do 
    {
        CC_BREAK_IF(!ActionSprite::initWithSpriteFrameName( "hero_idle_00.png"));

         int i;
         //idle animation
        CCArray *idleFrames = CCArray::createWithCapacity( 6);
         for (i =  0; i <  6; i++)
        {
            CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat( "hero_idle_%02d.png", i)->getCString());
            idleFrames->addObject(frame);
        }
        CCAnimation *idleAnimation = CCAnimation::createWithSpriteFrames(idleFrames,  1. 0 /  12. 0);
         this->setIdleAction(CCRepeatForever::create(CCAnimate::create(idleAnimation)));

         this->setCenterToBottom( 39. 0);
         this->setCenterToSides( 29. 0);
         this->setHitPoints( 100. 0);
         this->setDamage( 20. 0);
         this->setWalkSpeed( 80. 0);
        
        bRet =  true;
    }  while ( 0);

     return bRet;
}

我们用初始空闲精灵帧创建了英雄角色,配备了一个CCArray数组包含所有的属于空闲动画的精灵帧,然后创建一个CCAction动作播放来这个动画。以每秒12帧的速率进行播放。接下去,为英雄设置初始属性,包括精灵中心到边到底部的值。如下图所示:

英雄的每个精灵帧都在280x150像素大小的画布上创建,但实际上英雄精灵只占据这个空间的一部分。所以需要两个测量值,以便更好的设置精灵的位置。需要额外的空间,是因为每个动画精灵绘制的方式是不同的,而有些就需要更多的空间。
打开GameLayer.h文件,添加头文件声明:

1
#include  "Hero.h"

GameLayer类添加如下代码:

1
Hero *_hero;

打开GameLayer.cpp文件,在构造函数添加如下代码:

1
_hero =  NULL;

init函数this->addChild(_actors, -5);后面添加如下代码:

1
this->initHero();

添加initHero方法,代码如下:

1
2
3
4
5
6
7
8
void GameLayer::initHero()
{
    _hero = Hero::create();
    _actors->addChild(_hero);
    _hero->setPosition(ccp(_hero->getCenterToSides(),  80));
    _hero->setDesiredPosition(_hero->getPosition());
    _hero->idle();
}

创建了一个英雄实例,添加到了精灵表单,并设置了设置。调用idle方法,让其处于空闲状态,运行空闲动画。返回到ActionSprite.cpp文件,实现idle方法,代码如下:

1
2
3
4
5
6
7
8
9
10
void ActionSprite::idle()
{
     if (_actionState != kActionStateIdle)
    {
         this->stopAllActions();
         this->runAction(_idleAction);
        _actionState = kActionStateIdle;
        _velocity = CCPointZero;
    }
}

这个idle方法只有当ActionSprite不处于空闲状态才能调用。当它触发时,它会执行空闲动作,改变当前状态到kActionStateIdle,并且把速度置零。
13.编译运行,可以看到英雄处于空闲状态。如下图所示:

14.出拳动作。打开Hero.cpp文件,在init函数idle animation后面,添加如下代码:

1
2
3
4
5
6
7
8
9
//attack animation
CCArray *attackFrames = CCArray::createWithCapacity( 3);
for (i =  0; i <  3; i++)
{
    CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat( "hero_attack_00_%02d.png", i)->getCString());
    attackFrames->addObject(frame);
}
CCAnimation *attackAnimation = CCAnimation::createWithSpriteFrames(attackFrames,  1. 0 /  24. 0);
this->setAttackAction(CCSequence::create(CCAnimate::create(attackAnimation), CCCallFunc::create( this, callfunc_selector(Hero::idle)),  NULL));

打开ActionSprite.cpp文件,实现attack方法,代码如下:

1
2
3
4
5
6
7
8
9
void ActionSprite::attack()
{
     if (_actionState == kActionStateIdle || _actionState == kActionStateAttack || _actionState == kActionStateWalk)
    {
         this->stopAllActions();
         this->runAction(_attackAction);
        _actionState = kActionStateAttack;
    }
}

英雄只有在空闲、攻击、行走状态才能进行出拳。确保英雄正在受伤时、或者死亡时不能进行攻击。为了触发attack方法,打开GameLayer.cpp文件,在init函数添加如下代码:

1
this->setTouchEnabled( true);

重载ccTouchesBegan方法,代码如下:

1
2
3
4
void GameLayer::ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent)
{
    _hero->attack();
}

15.编译运行,点击屏幕进行出拳,如下图所示:

16.创建8个方向的方向键。我们需要创建虚拟的8个方向的方向键来让英雄在地图上进行移动。添加SimpleDPad类,派生自CCSprite类,SimpleDPad.h文件代码如下:

1
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
#pragma once
#include  "cocos2d.h"

class SimpleDPad;

class SimpleDPadDelegate
{
public:
     virtual  void didChangeDirectionTo(SimpleDPad *simpleDPad, cocos2d::CCPoint direction) =  0;
     virtual  void isHoldingDirection(SimpleDPad *simpleDPad, cocos2d::CCPoint direction) =  0;
     virtual  void simpleDPadTouchEnded(SimpleDPad *simpleDPad) =  0;
};

class SimpleDPad :  public cocos2d::CCSprite,  public cocos2d::CCTargetedTouchDelegate
{
public:
    SimpleDPad( void);
    ~SimpleDPad( void);

     static SimpleDPad* dPadWithFile(cocos2d::CCString *fileName,  float radius);
     bool initWithFile(cocos2d::CCString *filename,  float radius);

     void onEnterTransitionDidFinish();
     void onExit();
     void update( float dt);

     virtual  bool ccTouchBegan(cocos2d::CCTouch *pTouch, cocos2d::CCEvent *pEvent);
     virtual  void ccTouchMoved(cocos2d::CCTouch *pTouch, cocos2d::CCEvent *pEvent);
     virtual  void ccTouchEnded(cocos2d::CCTouch *pTouch, cocos2d::CCEvent *pEvent);

     void updateDirectionForTouchLocation(cocos2d::CCPoint location);

    CC_SYNTHESIZE(SimpleDPadDelegate*, _delegate, Delegate);
    CC_SYNTHESIZE( bool, _isHeld, IsHeld);

protected:
     float _radius;
    cocos2d::CCPoint _direction;
};

对以上的一些声明,解释如下:

  • radius:圆形方向键的半径。
    direction:当前所按下的方向。这是一个矢量,(-1.0, -1.0)是左下方向,(1.0, 1.0)是右上方向。
    delegate:方向键的委托,后续进行介绍。
    isHeld:布尔值表示玩家触摸着方向键。

对于SimpleDPad类,使用了委托模式。意味着一个委托类(并非SimpleDPad),将会处理由被委托类(SimpleDPad)启动的任务。在某些你指定的点上,主要是当涉及到处理任何游戏相关的东西,SimpleDPad将会将职责传递给委托类。这使得SimpleDPad无需知道任何游戏逻辑,从而允许你在开发任何其他游戏时,可以进行重用。如下图所示:

当SimpleDPad检测到在方向键内的触摸,它会计算触摸的方向,然后发送消息到委托类指明方向。在这之后的任何事情都不是SimpleDPad所关心的了。为了实施这个模式,SimpleDPad需要至少了解其委托的有关信息,特别是将触摸方向传递给委托的方法。这是另一种设计模式:协议。可以看到SimpleDPad的委托定义了所需的方法,在这种方式中,SimpleDPad强制其委托有三个指定的方法,以便确保每当它想传递东西放到委托中时,它能调用这些方法中的任何一种。事实上,SimpleDPad也遵循一种协议,即CCTargetedTouchDelegate。当SimpleDPad被触摸时,进行处理触摸事件,而GameLayer将不会得到触摸。否则的话,在触摸方向键的时候,英雄就会出拳攻击,显然,这不是希望看到的。打开SimpleDPad.cpp文件,添加如下代码:

1
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include  "SimpleDPad.h"
using  namespace cocos2d;

SimpleDPad::SimpleDPad( void)
{
    _delegate =  NULL;
}

SimpleDPad::~SimpleDPad( void)
{
}

SimpleDPad* SimpleDPad::dPadWithFile(CCString *fileName,  float radius)
{
    SimpleDPad *pRet =  new SimpleDPad();
     if (pRet && pRet->initWithFile(fileName, radius))
    {
         return pRet;
    }
     else
    {
         delete pRet;
        pRet =  NULL;
         return  NULL;
    }
}

bool SimpleDPad::initWithFile(CCString *filename,  float radius)
{
     bool bRet =  false;
     do 
    {
        CC_BREAK_IF(!CCSprite::initWithFile(filename->getCString()));
        
        _radius = radius;
        _direction = CCPointZero;
        _isHeld =  false;
         this->scheduleUpdate();

        bRet =  true;
    }  while ( 0);

     return bRet;
}

void SimpleDPad::onEnterTransitionDidFinish()
{
    CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate( this1true);
}

void SimpleDPad::onExit()
{
    CCDirector::sharedDirector()->getTouchDispatcher()->removeDelegate( this);
}

void SimpleDPad::update( float dt)
{
     if (_isHeld)
    {
        _delegate->isHoldingDirection( this, _direction);
    }
}

bool SimpleDPad::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent)
{
    CCPoint location = pTouch->getLocation();

     float distanceSQ = ccpDistanceSQ(location,  this->getPosition());
     if (distanceSQ <= _radius * _radius)
    {
         this->updateDirectionForTouchLocation(location);
        _isHeld =  true;
         return  true;
    }
     return  false;
}

void SimpleDPad::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent)
{
    CCPoint location = pTouch->getLocation();
     this->updateDirectionForTouchLocation(location);
}

void SimpleDPad::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent)
{
    _direction = CCPointZero;
    _isHeld =  false;
    _delegate->simpleDPadTouchEnded( this);
}

void SimpleDPad::updateDirectionForTouchLocation(CCPoint location)
{
     float radians = ccpToAngle(ccpSub(location,  this->getPosition()));
     float degrees = - 1 * CC_RADIANS_TO_DEGREES(radians);

     if (degrees <=  22. 5 && degrees >= - 22. 5
    {
         //right
        _direction = ccp( 1. 00. 0);
    }
     else  if (degrees >  22. 5 && degrees <  67. 5)
    {
         //bottomright
        _direction = ccp( 1. 0, - 1. 0);
    }
     else  if (degrees >=  67. 5 && degrees <=  112. 5)
    {
         //bottom
        _direction = ccp( 0. 0, - 1. 0);
    }
     else  if (degrees >  112. 5 && degrees <  157. 5)
    {
         //bottomleft
        _direction = ccp(- 1. 0, - 1. 0);
    }
     else  if (degrees >=  157. 5 || degrees <= - 157. 5)
    {
         //left
        _direction = ccp(- 1. 00. 0);
    }
     else  if (degrees < - 22. 5 && degrees > - 67. 5)
    {
         //topright
        _direction = ccp( 1. 01. 0);
    }
     else  if (degrees <= - 67. 5 && degrees >= - 112. 5)
    {
         //top
        _direction = ccp( 0. 01. 0);
    }
     else  if (degrees < - 112. 5 && degrees > - 157. 5)
    {
         //topleft
        _direction = ccp(- 1. 01. 0);
    }
    _delegate->didChangeDirectionTo( this, _direction);
}

以上方法中,onEnterTransitionDidFinish注册SimpleDPad委托类,onExit移除SimpleDPad委托类,update方法是当方向键被触摸时,传递方向值到委托类。ccTouchBegan方法检测触摸位置是否在方向键圆内,如果是,则将isHeld置为true,并更新方向值,返回true以拥有触摸事件优先权。ccTouchMoved当触摸点移动时,更新方向值。ccTouchEnded将isHeld置为false,重置方向,并通知委托触摸结束。updateDirectionForTouchLocation方法计算触摸点到方向键中心距离值,转换成角度,得到正确的方向值,然后传递值到委托。

打开HudLayer.h文件,添加头文件声明:

1
#include  "SimpleDPad.h"

添加如下代码:

1
2
bool init();
CC_SYNTHESIZE(SimpleDPad*, _dPad, DPad);

打开HudLayer.cpp文件,添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HudLayer::HudLayer( void)
{
    _dPad =  NULL;
}

bool HudLayer::init()
{
     bool bRet =  false;
     do 
    {
        CC_BREAK_IF(!CCLayer::init());

        _dPad = SimpleDPad::dPadWithFile(CCString::create( "pd_dpad.png"),  64);
        _dPad->setPosition(ccp( 64. 064. 0));
        _dPad->setOpacity( 100);
         this->addChild(_dPad);

        bRet =  true;
    }  while ( 0);

     return bRet;
}

以上代码实例化SimpleDPad,并且添加到HudLayer上。现在GameScene同时控制GameLayer和HudLayer,但有时候想直接通过HudLayer访问GameLayer。打开GameLayer.h文件,添加头文件声明:

1
2
#include  "SimpleDPad.h"
#include  "HudLayer.h"

GameLayer类声明修改成如下:

1
class GameLayer :  public cocos2d::CCLayer,  public SimpleDPadDelegate

并添加以下声明:

1
2
3
4
5
virtual  void didChangeDirectionTo(SimpleDPad *simpleDPad, cocos2d::CCPoint direction);
virtual  void isHoldingDirection(SimpleDPad *simpleDPad, cocos2d::CCPoint direction);
virtual  void simpleDPadTouchEnded(SimpleDPad *simpleDPad);

CC_SYNTHESIZE(HudLayer*, _hud, Hud);

以上方法的实现暂时为空。这样我们就在GameLayer中添加了HudLayer的引用,同时还让GameLayer遵循SimpleDPad所创建的协议。打开GameScene.cpp文件,在init函数this->addChild(_hudLayer, 1);后面,添加如下代码:

1
2
_hudLayer->getDPad()->setDelegate(_gameLayer);
_gameLayer->setHud(_hudLayer);

17.编译运行,可以看到左下角的虚拟方向键,如下图所示:

别试着压下方向键,英雄不会有任何反应,因为还未实现协议方法,这在第二部分将完成。

参考资料:
1.How To Make A Side-Scrolling Beat ‘Em Up Game Like Scott Pilgrim with Cocos2D – Part 1http://www.raywenderlich.com/24155/how-to-make-a-side-scrolling
2.如何使用cocos2d制作类似Scott Pilgrim的2D横版格斗过关游戏part1(翻译) http://blog.sina.com.cn/s/blog_4b55f6860101a9b7.html
3.如何使用Cocos2d-x做一DNF类的游戏-part1 http://blog.csdn.net/jyzgo/article/details/8471306

非常感谢以上资料,本例子源代码附加资源下载地址http://download.csdn.net/detail/akof1314/5038013
如文章存在错误之处,欢迎指出,以便改正。


在第一篇《如何制作一个横版格斗过关游戏》基础上,增加角色运动、碰撞、敌人、AI和音乐音效,原文《How To Make A Side-Scrolling Beat ‘Em Up Game Like Scott Pilgrim with Cocos2D – Part 2》,在这里继续以Cocos2d-x进行实现。有关源码、资源等在文章下面给出了地址。
步骤如下:
1.使用上一篇的工程;
2.移动英雄。在第一部分我们创建了虚拟方向键,但是还未实现按下方向键移动英雄,现在让我们进行实现。打开Hero.cpp文件,在init函数attack animation后面,添加如下代码:

1
2
3
4
5
6
7
8
9
//walk animation
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)));
打开 ActionSprite.cpp文件,实现 walkWithDirection方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void ActionSprite::walkWithDirection(CCPoint direction)
{
     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);
        }
    }
}
这段代码,检查前置动作状态是否空闲,若是的话切换动作到行走。在行走状态时,根据 _walkSpeed值改变精灵速度。同时检查精灵的左右方向,并通过将精灵 scaleX设置为1或-1来翻转精灵。要让英雄的行走动作跟方向键联系起来,需要借助方向键的委托: GameLayer类。打开 GameLayer.cpp文件,实现如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void GameLayer::didChangeDirectionTo(SimpleDPad *simpleDPad, CCPoint direction)
{
    _hero->walkWithDirection(direction);
}

void GameLayer::isHoldingDirection(SimpleDPad *simpleDPad, CCPoint direction)
{
    _hero->walkWithDirection(direction);
}

void GameLayer::simpleDPadTouchEnded(SimpleDPad *simpleDPad)
{
     if (_hero->getActionState() == kActionStateWalk)
    {
        _hero->idle();
    }
}

此时,编译运行程序的话,通过方向键移动英雄,发现英雄只是原地踏步。改变英雄的位置是ActionSprite和GameLayer共同的责任。一个ActionSprite永远不会知道它在地图上的位置。因此,它并不知道已经到达了地图的边缘,它只知道它想去哪里。而GameLayer的责任就是将它的期望位置转换成实际的位置。打开ActionSprite.cpp文件,实现以下方法:

1
2
3
4
5
6
7
void ActionSprite::update( float dt)
{
     if (_actionState == kActionStateWalk)
    {
        _desiredPosition = ccpAdd( this->getPosition(), ccpMult(_velocity, dt));
    }
}
这个方法在每次游戏更新场景的时候都会进行调用,当精灵处于行走状态时,它更新精灵的期望位置。位置+速度*时间,实际上就是意味着每秒移动X和Y点。打开 GameLayer.cpp文件,在 init函数 this->initTileMap();后面添加如下代码:
1
this->scheduleUpdate();
在析构函数,添加如下代码:
1
2
3
4
GameLayer::~GameLayer( void)
{
     this->unscheduleUpdate();
}
增加如下两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void GameLayer::update( float dt)
{
    _hero->update(dt);
     this->updatePositions();
}

void GameLayer::updatePositions()
{
     float posX = MIN(_tileMap->getMapSize().width * _tileMap->getTileSize().width - _hero->getCenterToSides(),
        MAX(_hero->getCenterToSides(), _hero->getDesiredPosition().x));
     float posY = 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的两个测量值,centerToSidescenterToBottom,因为ActionSprite要想保持在场景内,它的位置不能超过实际的精灵边界。假如ActionSprite的位置在已经设置的边界内,则GameLayer让英雄达到期望位置,否则GameLayer会让英雄留停在原地。
3.编译运行,此时点击方向键,移动英雄,如下图所示:

但是,很快你就会发现英雄可以走出地图的右边界,然后就这样从屏幕上消失了。
4.以上的问题,可以通过基于英雄的位置进行滚动地图,这个方法在文章《如何制作一个基于Tile的游戏》中有描述过。打开GameLayer.cpp文件,在update函数里最后添加如下代码:

1
this->setViewpointCenter(_hero->getPosition());
添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void GameLayer::setViewpointCenter(CCPoint position)
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();

     int x = MAX(position.x, winSize.width /  2);
     int y = MAX(position.y, winSize.height /  2);
    x = MIN(x, (_tileMap->getMapSize().width * _tileMap->getTileSize().width) - winSize.width /  2);
    y = MIN(y, (_tileMap->getMapSize().height * _tileMap->getTileSize().height) - winSize.height /  2);
    CCPoint actualPosition = ccp(x, y);

    CCPoint centerOfView = ccp(winSize.width /  2, winSize.height /  2);
    CCPoint viewPoint = ccpSub(centerOfView, actualPosition);
     this->setPosition(viewPoint);
}

以上代码让英雄处于屏幕中心位置,当然,英雄在地图边界时的情况除外。编译运行,效果如下图所示:

5.创建机器人。我们已经创建了精灵的基本模型:ActionSprite。我们可以重用它来创建游戏中电脑控制的角色。新建Robot类,派生自ActionSprite类,增加如下方法:

1
2
CREATE_FUNC(Robot);
bool init();
打开 Robot.cpp文件, init函数代码如下:

1
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
bool Robot::init()
{
     bool bRet =  false;
     do 
    {
        CC_BREAK_IF(!ActionSprite::initWithSpriteFrameName( "robot_idle_00.png"));
        
         int i;
         //idle animation
        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)));

         //attack animation
        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));

         //walk animation
        CCArray *walkFrames = CCArray::createWithCapacity( 6);
         for (i =  0; i <  6; i++)
        {
            CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(
                CCString::createWithFormat( "robot_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)));

         this->setWalkSpeed( 80. 0);
         this->setCenterToBottom( 39. 0);
         this->setCenterToSides( 29. 0);
         this->setHitPoints( 100. 0);
         this->setDamage( 10. 0);

        bRet =  true;
    }  while ( 0);

     return bRet;
}

跟英雄一样,以上代码创建一个带有3个动作的机器人:空闲、出拳、行走。它也有两个测量值:centerToBottomcenterToSides。注意到机器人的属性比英雄低一点,这是合乎逻辑的,不然英雄永远打不赢机器人。让我们开始添加一些机器人到游戏中去。打开GameLayer.h文件,添加如下代码:

1
CC_SYNTHESIZE_RETAIN(cocos2d::CCArray*, _robots, Robots);
打开 GameLayer.cpp文件,添加头文件如下:
1
#include  "Robot.h"
在构造函数里,添加如下代码:
1
_robots =  NULL;
init函数 this->initTileMap();的后面添加如下代码:
1
this->initRobots();
添加如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void GameLayer::initRobots()
{
     int robotCount =  50;
     this->setRobots(CCArray::createWithCapacity(robotCount));

     for ( int i =  0; i < robotCount; i++)
    {
        Robot *robot = Robot::create();
        _actors->addChild(robot);
        _robots->addObject(robot);

         int minX = SCREEN.width + robot->getCenterToSides();
         int maxX = _tileMap->getMapSize().width * _tileMap->getTileSize().width - robot->getCenterToSides();
         int minY = robot->getCenterToBottom();
         int maxY =  3 * _tileMap->getTileSize().height + robot->getCenterToBottom();
        robot->setScaleX(- 1);
        robot->setPosition(ccp(random_range(minX, maxX), random_range(minY, maxY)));
        robot->setDesiredPosition(robot->getPosition());
        robot->idle();
    }
}

这些代码做了以下事情:

  • 创建一个包含50个机器人的数组,并把它们添加到精灵表单中。
  • 使用Defines.h里面的随机函数随机放置50个机器人到地图地板上。同时,让最小随机值大于屏幕宽度,以确保不会有任何机器人出现在起点处。
  • 让每个机器人都处于空闲状态。

编译运行,让英雄向前走,直到看到地图上的机器人,如下图所示:

试着走到机器人区域中,你会发现机器人的绘制有些不对。如果英雄是在机器人的下面,那么他应该被绘制在机器人的前面,而不是在后面。我们需要明确的告诉游戏,哪个对象先绘制,这就是Z轴来进行控制的。添加英雄和机器人时,并没有明确指定其Z轴,默认下,后面添加的对象会比前面的对象Z轴值高,这就是为什么机器人挡住了英雄。为了解决这个问题,我们需要动态的处理Z轴顺序。每当精灵在屏幕上垂直移动时,它的Z轴值应该有所改变。屏幕上越高的精灵,其Z轴值应越低。打开GameLayer.cpp文件,添加如下方法:

1
2
3
4
5
6
7
8
9
void GameLayer::reorderActors()
{
    CCObject *pObject =  NULL;
    CCARRAY_FOREACH(_actors->getChildren(), pObject)
    {
        ActionSprite *sprite = (ActionSprite*)pObject;
        _actors->reorderChild(sprite, (_tileMap->getMapSize().height * _tileMap->getTileSize().height) - sprite->getPosition().y);
    }
}
然后在 update函数 this->updatePositions();的后面,添加如下代码:
1
this->reorderActors();
每当精灵的位置更新,这个方法会让 CCSpriteBatchNode重新设置它的每个子节点Z轴值,其根据子节点离地图底部的距离,当子节点离底部更高时,其Z轴值就会下降。编译运行,可以看到正确的绘制顺序,如下图所示:

6.出拳猛击机器人,碰撞检测。为了让英雄能够出拳,并且能够实际上打在了机器人身上,需要实现一种方式的碰撞检测。在这篇文章中,我们使用矩形创建一个非常简单的碰撞检测系统。在这个系统中,我们为每个角色定义两种矩形/盒子:

  • Hit box:代表精灵的身体
  • Attack box:代表精灵的手

假如某个ActionSprite的Attack box碰撞到另一个ActionSprite的Hit box,那么这就是一次碰撞发生。这两个矩形之间的区别,将帮助我们知道谁打了谁。Defines.h文件中的BoundingBox定义,包含两种矩形:实际的,和原始的:
①原始矩形,每个精灵的基本矩形,一旦设置后就不会改变。
②实际矩形,这是位于世界空间中的矩形,当精灵移动时,实际的矩形也跟着变动。
打开ActionSprite.h文件,添加如下代码:

1
2
3
4
CC_SYNTHESIZE(BoundingBox, _hitBox, Hitbox);
CC_SYNTHESIZE(BoundingBox, _attackBox, AttackBox);

BoundingBox createBoundingBoxWithOrigin(cocos2d::CCPoint origin, cocos2d::CCSize size);

以上创建了ActionSprite的两个包围盒:Hit box和Attack box。还定义了一个方法,用于根据给定的原点和大小来创建一个BoundingBox结构体。打开ActionSprite.cpp文件,添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BoundingBox ActionSprite::createBoundingBoxWithOrigin(CCPoint origin, CCSize size)
{
    BoundingBox boundingBox;
    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;
     return boundingBox;
}

void ActionSprite::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));
}

void ActionSprite::setPosition(CCPoint position)
{
    CCSprite::setPosition(position);
     this->transformBoxes();
}

第一个方法创建一个新的包围盒,这有助于ActionSprite的子类创建属于它们自己的包围盒。第二个方法,基于精灵的位置、比例因子,和包围盒原本的原点和大小来更新每个包围盒实际测量的原点和大小。之所以要用到比例因子,是因为它决定着精灵的方向。位于精灵右侧的盒子,当比例因子设置为-1时,将会翻转到左侧。打开Hero.cpp文件,在init函数后面添加如下代码:

1
2
3
this->setHitbox( this->createBoundingBoxWithOrigin(ccp(- this->getCenterToSides(), - this->getCenterToBottom()),
    CCSizeMake( this->getCenterToSides() *  2this->getCenterToBottom() *  2)));
this->setAttackBox( this->createBoundingBoxWithOrigin(ccp( this->getCenterToSides(), - 10), CCSizeMake( 2020)));
打开 Robot.cpp文件,在 init函数后面添加如下代码:
1
2
3
this->setHitbox( this->createBoundingBoxWithOrigin(ccp(- this->getCenterToSides(), - this->getCenterToBottom()),
    CCSizeMake( this->getCenterToSides() *  2this->getCenterToBottom() *  2)));
this->setAttackBox( this->createBoundingBoxWithOrigin(ccp( this->getCenterToSides(), - 5), CCSizeMake( 2520)));
现在我们已经有了英雄和机器人各自的Hit box和Attack box。如果是可视化的箱子,它们会像下面这样:

无论何时,当一个attack box(红色)跟一个hit box(蓝色)交叉,即一次碰撞发生。在开始编写代码,检测包围盒交叉前,需要确保 ActionSprite能够对被击中有所反应。我们已经添加了空闲、出拳、行走动作,但还未创建受伤和死亡动作。打开 ActionSprite.cpp文件,实现如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void ActionSprite::hurtWithDamage( float damage)
{
     if (_actionState != kActionStateKnockedOut)
    {
         this->stopAllActions();
         this->runAction(_hurtAction);
        _actionState = kActionStateHurt;
        _hitPoints -= damage;

         if (_hitPoints <=  0)
        {
             this->knockout();
        }
    }
}

void ActionSprite::knockout()
{
     this->stopAllActions();
     this->runAction(_knockedOutAction);
    _hitPoints =  0;
    _actionState = kActionStateKnockedOut;
}

只要精灵还未死亡,被击中时状态将会切换到受伤状态,执行受伤动画,并且精灵的生命值将会减去相应的伤害值。如果生命值少于0,那么死亡的动作将会触发。为了完成这两个动作,我们还需更改Hero类和Robot类。打开Hero.cpp文件,在init函数walk animation后面添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//hurt animation
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), CCCallFunc::create( this, callfunc_selector(Hero::idle)),  NULL));

//knocked out animation
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. 010. 0),  NULL));

打开Robot.cpp文件,在init函数walk animation后面添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//hurt animation
CCArray *hurtFrames = CCArray::createWithCapacity( 3);
for (i =  0; i <  3; i++)
{
    CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat( "robot_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), CCCallFunc::create( this, callfunc_selector(Robot::idle)),  NULL));

//knocked out animation
CCArray *knockedOutFrames = CCArray::createWithCapacity( 5);
for (i =  0; i <  5; i++)
{
    CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat( "robot_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. 010. 0),  NULL));

以上代码应该不陌生了。我们用创建其他动作同样的方式创建了受伤和死亡动作。受伤动作结束时,会切换到空闲状态。死亡动作结束时,精灵进行闪烁。打开GameLayer.cpp文件,添加碰撞处理,在ccTouchesBegan函数后面添加如下代码:

1
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文件,添加如下代码:
1
CC_SYNTHESIZE( float, _nextDecisionTime, NextDecisionTime);
打开 Robot.cpp文件,在 init函数后面,添加如下代码:
1
_nextDecisionTime =  0;
这个属性保存下一次机器人可以作出决定的时间。打开 Defines.h文件,修改成如下代码:

1
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
#pragma once
#include  "cocos2d.h"

// 1 - convenience measurements
#define SCREEN CCDirector::sharedDirector()->getWinSize()
#define CENTER ccp(SCREEN.width /  2, SCREEN.height /  2)
#define CURTIME GetCurTime()

// 2 - convenience functions
#ifndef UINT64_C
#define UINT64_C(val) val##ui64
#endif
#define random_range(low, high) (rand() % (high - low +  1)) + low
#define frandom ( float)rand() / UINT64_C(0x100000000)
#define frandom_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::CCRect actual;
    cocos2d::CCRect original;
} BoundingBox;

inline  float GetCurTime(){
    timeval time;
    gettimeofday(&time,  NULL);
     unsigned  long millisecs = (time.tv_sec *  1000) + (time.tv_usec /  1000);
     return ( float)millisecs;
};

打开GameLayer.cpp文件,添加如下方法:

1
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
void GameLayer::updateRobots( float dt)
{
     int alive =  0;
     float distanceSQ;
     int randomChoice =  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. 10. 5) *  1000);
                    randomChoice = random_range( 01);

                     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. 10. 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());

                                     //end game checker here
                                }
                            }
                        }
                    }
                     else
                    {
                        robot->idle();
                    }
                }
                 else  if (distanceSQ <= SCREEN.width * SCREEN.width)
                {
                     //5
                    robot->setNextDecisionTime(CURTIME + frandom_range( 0. 51. 0) *  1000);
                    randomChoice = random_range( 02);
                     if (randomChoice ==  0)
                    {
                        CCPoint moveDirection = ccpNormalize(ccpSub(_hero->getPosition(), robot->getPosition()));
                        robot->walkWithDirection(moveDirection);
                    } 
                     else
                    {
                        robot->idle();
                    }
                }
            }
        }
    }

     //end game checker here
}

这是一个漫长的代码片段。将代码分解为一段段。对于游戏中的每个机器人:
①.使用一个计数来保存仍然存活着的机器人数量。一个机器人只要不是死亡状态,就被认为仍然存活着。这将用于判断游戏是否应该结束。
②.检查当前应用程序时间的推移是否超过了机器人的下一次决定时间。如果超过了,意味着机器人需要作出一个新的决定。
③.检查机器人是否足够接近英雄,以便于有机会出拳攻击落在英雄身上。如果接近英雄了,那么就进行一个随机选择,看是要朝着英雄出拳,还是要继续空闲着。
④.假如机器人决定攻击,我们就用之前检测英雄攻击时相同的方式来进行检测碰撞。只是这一次,英雄和机器人的角色互换了。
⑤.如果机器人和英雄之间的距离小于屏幕宽度,那么机器人将作出决定,要么朝着英雄移动,要么继续空闲。机器人的移动基于英雄位置和机器人位置产生的法向量。
每当机器人作出决定,它的下一个决定的时间被设定为在未来的一个随机时间。在此期间,它将继续执行上次作出决定时所做出的动作。接着在update函数里,this->updatePositions();前添加如下代码:

1
this->updateRobots(dt);
updatePositions函数后面,添加如下代码:
1
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));
}
确保每次游戏循环时,机器人AI方法都被调用。遍历每个机器人,并让它们朝着期望的位置进行移动。
9.编译运行,将会看到沿着走廊过来的机器人。效果如下图所示:

10.为游戏添加重新开始的按钮。打开 GameLayer.cpp文件,添加头文件引用:
1
#include  "GameScene.h"
添加如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void GameLayer::endGame()
{
    CCLabelTTF *restartLabel = CCLabelTTF::create( "RESTART""Arial"30);
    CCMenuItemLabel *restartItem = CCMenuItemLabel::create(restartLabel,  this, menu_selector(GameLayer::restartGame));
    CCMenu *menu = CCMenu::create(restartItem,  NULL);
    menu->setPosition(CENTER);
    menu->setTag( 5);
    _hud->addChild(menu,  5);
}

void GameLayer::restartGame(CCObject* pSender)
{
    CCDirector::sharedDirector()->replaceScene(GameScene::create());
}

第一个方法创建显示一个重新开始的按钮,当按下它时,触发第二个方法。后者只是命令导演用新的GameScene实例替换当前场景。接着在updateRobots函数里面,在第一个end game checker here注释后面,添加如下代码:

1
2
3
4
if (_hero->getActionState() == kActionStateKnockedOut && _hud->getChildByTag( 5) ==  NULL)
{
     this->endGame();
}
在第二个 end game checker here注释后面,添加如下代码:
1
2
3
4
if (alive ==  0 && _hud->getChildByTag( 5) ==  NULL)
{
     this->endGame();
}
这些语句都是检测游戏结束的条件。第一个检测英雄被机器人攻击后,是否还存活着。如果英雄死亡了,那么游戏就结束了。第二个检测是否所有的机器人都死亡了。如果都死亡了,那么游戏也结束了。另外,在 endGame方法里,可以看到游戏结束菜单的 tag值为5。因为检测是在循环里面,需要确保游戏结束菜单之前没被创建过。否则的话,将会一直创建游戏结束菜单。
11.编译运行,可以看到游戏结束时的样子,如下图所示:

12.音乐和音效。打开GameLayer.cpp文件,添加头文件引用:

1
#include  "SimpleAudioEngine.h"
init函数里, CC_BREAK_IF(!CCLayer::init());后面添加如下代码:
1
2
3
4
5
6
7
// Load audio
CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadBackgroundMusic( "latin_industries.aifc");
CocosDenshion::SimpleAudioEngine::sharedEngine()->playBackgroundMusic( "latin_industries.aifc");
CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadEffect( "pd_hit0.wav");
CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadEffect( "pd_hit1.wav");
CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadEffect( "pd_herodeath.wav");
CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadEffect( "pd_botdeath.wav");
打开 ActionSprite.cpp文件,添加头文件引用:
1
#include  "SimpleAudioEngine.h"
hurtWithDamage函数,第一个条件语句里添加如下代码:
1
2
int randomSound = random_range( 01);
CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect(CCString::createWithFormat( "pd_hit%d.wav", randomSound)->getCString());
打开 ActionSprite.h文件,将 knockout方法声明修改如下:
1
virtual  void knockout();
打开 Hero.cpp文件,添加头文件引用:
1
#include  "SimpleAudioEngine.h"
添加如下方法:
1
2
3
4
5
void Hero::knockout()
{
    ActionSprite::knockout();
    CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect( "pd_herodeath.wav");
}
打开 Robot.cpp文件,添加头文件引用:
1
#include  "SimpleAudioEngine.h"
添加如下方法:
1
2
3
4
5
void Robot::knockout()
{
    ActionSprite::knockout();
    CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect( "pd_botdeath.wav");
}
13.编译运行,现在游戏将有配乐,效果图:

参考资料:
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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值