本文实践自 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.新建Xcode工程,工程名为"PompaDroid",去除"Box2D"选项,勾选"Simple Audio Engine in Cocos Denshion"选项;
2.添加游戏场景类GameScene,派生自CCScene类。添加GameLayer类和HudLayer类,派生自CCLayer类。删除HelloWorldScene.h和HelloWorldScene.cpp文件。
3.HudLayer类增加一个方法:
1
|
CREATE_FUNC(HudLayer);
|
和GameLayer类增加一个方法:
1
|
CREATE_FUNC(GameLayer);
|
4.文件GameScene.h代码如下:
#include "cocos2d.h" #include "GameLayer.h" #include "HudLayer.h" USING_NS_CC; class GameScene:public CCScene { public: //构造函数 GameScene(); //析构函数 ~GameScene(); virtual bool init(); CREATE_FUNC(GameScene); //利用cocos2d-x中的宏定义实现Get和Set方法 CC_SYNTHESIZE(GameLayer*, _gameLayer, GameLayer); CC_SYNTHESIZE(HudLayer*, _hudLayer, HudLayer); };
文件GameScene.cpp代码如下:
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(); //将该层加在0的位置数越小越在下面 this->addChild(_gameLayer,0); //初始化游戏的虚拟手柄层 _hudLayer = HudLayer::create(); //将该层加在1的位置1比0大,所以手柄层在游戏层的上面 this->addChild(_hudLayer,1); bRet = true; } while (0); return bRet; }
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文件,添加如下代码:
//初始化方法 bool init(); //初始化地图的方法 void initTileMap(); //创建地图对象 cocos2d::CCTMXTiledMap *_tileMap;
打开GameLayer.cpp,在构造函数,添加如下代码:
1
|
_tileMap =
NULL;
|
添加如下代码:
bool GameLayer::init() { bool bRet = false; do { CC_BREAK_IF(!CCLayer::init()); //调用initTileMap()函数 this->initTileMap(); bRet = true; } while (0); return bRet; } void GameLayer::initTileMap() { //初始化地图 大家在导入文件时要注意在记得AddTarget工程 否则回找不到资源,如果这个程序崩溃,找不到资源,就把资源删了重新导入,记得AddTarget工程 _tileMap = CCTMXTiledMap::create("pd_tilemap.tmx"); //声明一个CCObject对象 , 用来接受地图中的对象 CCObject *pObject = NULL; //-X为我们提供的遍历的方法 CCARRAY_FOREACH(_tileMap->getChildren(), pObject) { //将地图中每一个子节点就相当于一个对象 取出 CCTMXLayer *child = (CCTMXLayer*)pObject; //取出的目的是为了 setAliasTexParameters()消除锯齿效果 child->getTexture()->setAliasTexParameters(); } //将瓦片地图加在-6的位置 this->addChild(_tileMap, -6); }对所有图层进行 setAliasTexParameters 设置,该方法是关闭抗锯齿功能,这样就能保持像素风格。
10.编译运行,可以看到地图显示在屏幕上,如下图所示:
11.创建英雄。在大多数2D横版游戏中,角色有不同的动画代表不同类型的动作。我们需要知道什么时候播放哪个动画。这里采用状态机来解决这个问题。状态机就是某种通过切换状态来改变行为的东西。单一状态机在同一时间只能有一个状态,但可以从一种状态过渡到另一种状态。在这个游戏中,角色共有五种状态,空闲、行走、出拳、受伤、死亡,如下图所示:
为了有一个完整的状态流,每个状态应该有一个必要条件和结果。例如:行走状态不能突然转变到死亡状态,因为你的英雄在死亡前必须先受伤。
12.新建一个头文件Defines.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; #endif /* defined(__HeroGame1__Defines__) */
添加ActionSprite类,派生自CCSprite类,ActionSprite.h文件代码如下:
#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 创建动作集合的set和get方法 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 //状态的枚举这个枚举定义在Defines.h中 要记得导入头文件 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文件,构造函数、析构函数、Update函数如下:
1
2 3 4 5 6 7 8 |
ActionSprite::ActionSprite(
void)
{ _idleAction = NULL; _attackAction = NULL; _walkAction = NULL; _hurtAction = NULL; _knockedOutAction = NULL; } |
ActionSprite::~ActionSprite() { } void ActionSprite::update(float dt) { }
Actions:这些是每种状态要执行的动作。这些动作是当角色切换状态时,执行精灵动画和其他触发的事件。
States:保存精灵的当前动作/状态,使用ActionState类型,这个类型待会我们将会进行定义。
Attributes:包含精灵行走速度值,受伤时减少生命点值,攻击伤害值。
Movement:用于计算精灵如何沿着地图移动。
Measurements:保存对精灵的实际图像有用的测量值。需要这些值,是因为你将要使用的这些精灵画布大小是远远大于内部包含的图像。
Action methods:不直接调用动作,而是使用这些方法触发每种状态。
Scheduled methods:任何事需要在一定的时间间隔进行运行,比如精灵位置和速度的更新,等等。
打开GameLayer.h文件,添加如下代码:
//创建动画表单集合对象 cocos2d::CCSpriteBatchNode *_actors;
打开GameLayer.cpp文件,在init函数里面添加如下代码:
//加载精灵表单,这个精灵表单包含我们的所有精灵 CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("pd_sprites.plist"); //创建一个CCSpriteBatchNode。 _actors = CCSpriteBatchNode::create("pd_sprites.pvr.ccz"); //setAliasTexParameters()消除锯齿效果 _actors->getTexture()->setAliasTexParameters(); //它的z值高于CCTMXTiledMap对象,这样才能出现在地图前。 Z轴为-5 this->addChild(_actors, -5);
加载精灵表单,创建一个CCSpriteBatchNode。这个精灵表单包含我们的所有精灵。它的z值高于CCTMXTiledMap对象,这样才能出现在地图前。
添加Hero类,派生自ActionSprite类,添加如下代码:
1
2 |
CREATE_FUNC(Hero);
bool init(); |
Hero类的init函数的实现如下所示:
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是一个精灵帧,可以简单理解为一个精灵图片 CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat("hero_idle_%02d.png", i)->getCString()); //将精灵帧放入数组 idleFrames->addObject(frame); } //创建动画对象 createWithSpriteFrames方法参数的意思第一个参数动画数组,第二个参数是每个图片停留的时间 ,就是说该数越小动画越快 CCAnimation *idleAnimation = CCAnimation::createWithSpriteFrames(idleFrames,1.0 / 12.0); //对该动作进行设置CCRepeatForever永远执行 CCAnimate执行的动画 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;
|
void initHero();打开 GameLayer.cpp 文件,在构造函数添加如下代码:
1
|
_hero =
NULL;
|
在init函数this->addChild(_actors, -5);后面添加如下代码:
1
|
this->initHero();
|
添加initHero方法,代码如下:
void GameLayer::initHero() { //创建英雄对象 _hero = Hero::create(); //让精灵显示在屏幕上 _actors->addChild(_hero); //设置英雄的位置 _hero->setPosition(ccp(_hero->getCenterToSides(), 80)); //设置目标位置,因为是初始位置,所以就是英雄当前位置 _hero->setDesiredPosition(_hero->getPosition()); //初始动画为idle() _hero->idle(); }创建了一个英雄实例,添加到了精灵表单,并设置了设置。调用 idle 方法,让其处于空闲状态,运行空闲动画。返回到 ActionSprite.cpp 文件,实现 idle 方法,代码如下:
void ActionSprite::idle() { //这个idle方法只有当ActionSprite不处于空闲状态才能调用。当它触发时,它会执行空闲动作,改变当前状态到kActionStateIdle,并且把速度置零。 if (_actionState != kActionStateIdle) { //让所有动作停止 this->stopAllActions(); //执行动作 this->runAction(_idleAction); //设置动作状态的标记 _actionState = kActionStateIdle; //速度置为零 _velocity = CCPointZero; } }
这个idle方法只有当ActionSprite不处于空闲状态才能调用。当它触发时,它会执行空闲动作,改变当前状态到kActionStateIdle,并且把速度置零。
13.编译运行,可以看到英雄处于空闲状态。如下图所示:
14.出拳动作。打开Hero.cpp文件,在init函数idle animation后面,添加如下代码:
//这个方法与上面的方法基本是一个意思,只是加载的图片名就好 //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); //这里有些不同 执行完动画后有一个回调方法CCCallFunc ,这个方法再次调用idle方法 this->setAttackAction(CCSequence::create(CCAnimate::create(attackAnimation), CCCallFunc::create(this, callfunc_selector(Hero::idle)),NULL));打开 ActionSprite.cpp 文件,实现 attack 方法,代码如下:
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方法,代码如下:
//添加这个方法后注意要ccTouch es 注意要有es 这个是多点触摸的回调方法 之后还会报错,因为没有在.h文件中声明,将该方法复制在.h文件中声明。 void GameLayer::ccTouchesBegan(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent) { _hero->attack(); }在GameLayer.h添加如下代码
void ccTouchesBegan(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);
代码例子 http://vdisk.weibo.com/s/BDn59yfnBVk57