如何製作一個橫版格鬥過關遊戲 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
2.新增遊戲場景類GameScene,派生自CCScene類。新增GameLayer類和HudLayer類,派生自CCLayer類。刪除HelloWorldScene.h和HelloWorldScene.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); |
1 | CREATE_FUNC(GameLayer); |
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; |
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(); |
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" |
1 | Hero *_hero; |
1 | _hero = NULL; |
1 | this->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(); } |
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; } } |
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)); |
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; } } |
1 | this->setTouchEnabled( true); |
1
2 3 4 |
void GameLayer::ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent)
{ _hero->attack(); } |
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( 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 |