本文出自 “无幻” 博客,请务必保留此出处 http://blog.csdn.net/akof1314/article/details/8549150
本文实践自 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.h 和HelloWorldScene.cpp 文件。
3.文件 GameScene.h 代码如下:
#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 代码如下:
#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 类增加一个方法:
CREATE_FUNC(HudLayer);
和 GameLayer 类增加一个方法:
CREATE_FUNC(GameLayer);
5.修改 AppDelegate.cpp 文件,代码如下:
//#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都是32×32大小。可行走的地板tile位于下数三行。
9.打开 GameLayer.h 文件,添加如下代码:
bool init(); void initTileMap(); cocos2d::CCTMXTiledMap *_tileMap;
打开 GameLayer.cpp ,在构造函数,添加如下代码:
_tileMap = NULL;
添加如下代码:
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 文件代码如下:
#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 文件,构造函数如下:
ActionSprite::ActionSprite(void) { _idleAction = NULL; _attackAction = NULL; _walkAction = NULL; _hurtAction = NULL; _knockedOutAction = NULL; }
各个方法实现暂时为空。以上代码声明了基本变量和方法,可以分为以下几类:
新建一个头文件 Defines.h ,代码如下:
#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 文件,添加如下代码:
cocos2d::CCSpriteBatchNode *_actors;
打开 GameLayer.cpp 文件,在 init 函数里面添加如下代码:
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 类,添加如下代码:
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 *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帧的速率进行播放。接下去,为英雄设置初始属性,包括精灵中心到边到底部的值。如下图所示:
英雄的每个精灵帧都在280×150像素大小的画布上创建,但实际上英雄精灵只占据这个空间的一部分。所以需要两个测量值,以便更好的设置精灵的位置。需要额外的空间,是因为每个动画精灵绘制的方式是不同的,而有些就需要更多的空间。
打开 GameLayer.h 文件,添加头文件声明:
#include "Hero.h"
GameLayer 类添加如下代码:
Hero *_hero;
打开 GameLayer.cpp 文件,在构造函数添加如下代码:
_hero = NULL;
在 init 函数this->addChild(_actors, -5);后面添加如下代码:
this->initHero();
添加 initHero 方法,代码如下:
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 方法,代码如下:
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后面,添加如下代码:
//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 方法,代码如下:
void ActionSprite::attack() { if (_actionState == kActionStateIdle || _actionState == kActionStateAttack || _actionState == kActionStateWalk) { this->stopAllActions(); this->runAction(_attackAction); _actionState = kActionStateAttack; } }
英雄只有在空闲、攻击、行走状态才能进行出拳。确保英雄正在受伤时、或者死亡时不能进行攻击。为了触发 attack 方法,打开 GameLayer.cpp 文件,在 init 函数添加如下代码:
this->setTouchEnabled(true);
重载 ccTouchesBegan 方法,代码如下:
void GameLayer::ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent) { _hero->attack(); }
15.编译运行,点击屏幕进行出拳,如下图所示:
16.创建8个方向的方向键。我们需要创建虚拟的8个方向的方向键来让英雄在地图上进行移动。添加 SimpleDPad 类,派生自 CCSprite 类, SimpleDPad.h 文件代码如下:
#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; };
对以上的一些声明,解释如下:
对于 SimpleDPad 类,使用了委托模式。意味着一个委托类(并非SimpleDPad),将会处理由被委托类(SimpleDPad)启动的任务。在某些你指定的点上,主要是当涉及到处理任何游戏相关的东西,SimpleDPad将会将职责传递给委托类。这使得SimpleDPad无需知道任何游戏逻辑,从而允许你在开发任何其他游戏时,可以进行重用。如下图所示:
当SimpleDPad检测到在方向键内的触摸,它会计算触摸的方向,然后发送消息到委托类指明方向。在这之后的任何事情都不是SimpleDPad所关心的了。为了实施这个模式,SimpleDPad需要至少了解其委托的有关信息,特别是将触摸方向传递给委托的方法。这是另一种设计模式:协议。可以看到SimpleDPad的委托定义了所需的方法,在这种方式中,SimpleDPad强制其委托有三个指定的方法,以便确保每当它想传递东西放到委托中时,它能调用这些方法中的任何一种。事实上,SimpleDPad也遵循一种协议,即 CCTargetedTouchDelegate 。当SimpleDPad被触摸时,进行处理触摸事件,而GameLayer将不会得到触摸。否则的话,在触摸方向键的时候,英雄就会出拳攻击,显然,这不是希望看到的。打开 SimpleDPad.cpp 文件,添加如下代码:
#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(this, 1, true); } 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.0, 0.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.0, 0.0); } else if (degrees < -22.5 && degrees > -67.5) { //topright _direction = ccp(1.0, 1.0); } else if (degrees <= -67.5 && degrees >= -112.5) { //top _direction = ccp(0.0, 1.0); } else if (degrees < -112.5 && degrees > -157.5) { //topleft _direction = ccp(-1.0, 1.0); } _delegate->didChangeDirectionTo(this, _direction); }
以上方法中, onEnterTransitionDidFinish 注册SimpleDPad委托类, onExit 移除SimpleDPad委托类, update 方法是当方向键被触摸时,传递方向值到委托类。ccTouchBegan 方法检测触摸位置是否在方向键圆内,如果是,则将isHeld置为true,并更新方向值,返回true以拥有触摸事件优先权。 ccTouchMoved 当触摸点移动时,更新方向值。 ccTouchEnded 将isHeld置为false,重置方向,并通知委托触摸结束。 updateDirectionForTouchLocation 方法计算触摸点到方向键中心距离值,转换成角度,得到正确的方向值,然后传递值到委托。
打开 HudLayer.h 文件,添加头文件声明:
#include "SimpleDPad.h"
添加如下代码:
bool init();
CC_SYNTHESIZE(SimpleDPad*, _dPad, DPad);
打开 HudLayer.cpp 文件,添加如下代码:
以上代码实例化 SimpleDPad ,并且添加到 HudLayer 上。现在GameScene同时控制GameLayer和HudLayer,但有时候想直接通过HudLayer
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.0, 64.0)); _dPad->setOpacity(100); this->addChild(_dPad); bRet = true; } while (0); return bRet; }
访问GameLayer。打开 GameLayer.h 文件,添加头文件声明:
#include "SimpleDPad.h" #include "HudLayer.h"
将 GameLayer 类声明修改成如下:
class GameLayer : public cocos2d::CCLayer, public SimpleDPadDelegate
并添加以下声明:
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);后面,添加如下代码:
_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 1 http://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
如文章存在错误之处,欢迎指出,以便改正。