歡迎回來,上篇我們講到了物理引擎中重力環(huán)境模擬以及主角考拉與地面墻壁的碰撞,相信大家已經(jīng)對2D世界的物理模擬有了一定的了解,現(xiàn)在我們接著講如何讓考拉動起來吧!
讓考拉動起來!
這里控制考拉移動變得非常簡單,它只有向前和跳兩個能力(源碼中我加了考拉向后走功能,建議大家自己加幾個虛擬按鍵來實現(xiàn)更非富的功能)如果你按著屏幕左半部考拉會向前走,按住右半部考拉會跳起來(原文設(shè)定考拉不會后退-_-)。
我們需要在Player.h里加兩個成員變量:
bool _forwardMarch; //是否向前走
bool _mightAsWellJump; //可以跳躍嗎?
在player.cpp的init方法或在構(gòu)造函數(shù)里把它們設(shè)置為false
在GameLevelLayer類里加上觸摸,在.h文件里加上:
virtual void registerWithTouchDispatcher(); void ccTouchesBegan(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent); void ccTouchesMoved(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent); void ccTouchesEnded(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);
在GameLevelLayer.cpp的init里加上(加載地圖代碼后)
setTouchEnabled(true); //設(shè)置可觸摸
然后寫注冊觸摸方法
void GameLevelLayer::registerWithTouchDispatcher() { CCDirector* pDirector = CCDirector::sharedDirector(); pDirector->getTouchDispatcher()->addStandardDelegate(this, 0); //注冊多點觸摸 }
現(xiàn)在,讓我們看看那三個觸摸事件吧!
void GameLevelLayer::ccTouchesBegan(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent) { CCSetIterator iter = pTouches->begin(); for (; iter!=pTouches->end(); iter++) { CCTouch* pTouch = (CCTouch*)(*iter); CCPoint touchLocation = this->convertTouchToNodeSpace(pTouch); //把touch點位置轉(zhuǎn)換為本地坐標 if (touchLocation.x > 240) //在屏幕最右邊點,就是跳 { _player->bMightAsWellJump = true; } else { _player->bForwardMarch = true; } } } void GameLevelLayer::ccTouchesEnded(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent) { _player->bForwardMarch = false; //松開按鍵時,設(shè)置為不可跳也不是向前狀態(tài) _player->bMightAsWellJump = false; }
代碼一目了然,ccTouchesBegan時根據(jù)玩家按的位置決定了考拉狀態(tài)是前進還是跳躍,松開按鍵時將這兩個狀態(tài)變量重置為false。
真正的角色移動是在player的update方法里進行的,看代碼:
void Player::update(float delta) { CCPoint gravity = ccp(0.f, -450.f); //考拉每秒下降450個單位 CCPoint gravityStep = ccpMult(gravity, delta); //計算在重力影響下delta時間內(nèi)具體下降了多少, 即dt時間后下落速度為多少 CCPoint forwardMove = ccp(800.f, 0.f); //前進速度,每秒前進800.f CCPoint forwardStep = ccpMult(forwardMove, delta); // 1 this->_velocity = ccpAdd(this->_velocity, gravityStep); //當前速度=當前速度+重力加速度 this->_velocity = ccp(this->_velocity.x *0.9f, this->_velocity.y); //2 if (this->bForwardMarch) { this->_velocity = ccpAdd(this->_velocity, forwardStep); //當前速度要加上向前的速度矢量 }// 3 CCPoint minMovement = ccp(-120.f, -350.f); CCPoint maxMovement = ccp(120.0f, 250.f); this->_velocity = ccpClamp(this->_velocity, minMovement, maxMovement); //4 CCPoint stepVelocity = ccpMult(this->_velocity, delta); //計算下此速度下主角移動了多少 this->_desiredPosition = ccpAdd(this->getPosition(), stepVelocity); //當前期望要去的位置=當前位置+當前速度 }
讓我們來詳細地看一下新增部分:
當玩家觸摸前進的時候和重力模擬類似,我們加了一個向前的推進力800.f,處理方式與重力相同
第2步橫向速度乘以0.9是模擬摩擦力效果,考慮下當有向前推力時玩家會向前移動,沒有了呢?是不是立即停下來?那樣看起來太生硬了,所以這里x軸速度每幀乘以0.9就起到了均勻減速的效果,就像摩擦力一樣
檢查一下玩家是否按了前,按前進時速度上就要加上向前的推動力,就起到了向前進的效果了
這一步是調(diào)用了clamp是限定速度前后上下不要太大,給它一個大值。之前的那句設(shè)定下落速度大值可以去掉了。
好了運行一下,我們可以看到我們的主角小考拉現(xiàn)在可以前進了!
讓考拉跳起來!
跳躍是動作游戲里最明顯的一個特征。我們希望角色跳的平滑逼真,現(xiàn)在讓我們來實現(xiàn)它。
到player類的update方法里,在if(this->_forwardMarch)語句之前加上下面代碼:
CCPoint jumpForce = ccp(0.f, 310.f); if(this->_mightAsWellJump && this->_onGround) { this->_velocity = ccpAdd(this->_velocity, jumpForce); }
只要加一個向上的力,角色就可以跳起來了。
如果你止步于此,你會得到一個老式的雅代利式跳躍,即每次跳躍都是相同的高度,每次你都施給玩家一個同樣的力,然后等著重力把你拉回地面來。
這么做似乎沒什么不妥,如果你要求不高的話,但是仔細觀察一下各種流行的平臺游戲,如超級馬里奧,索尼克,洛克人,水上魂斗羅等,似乎玩家能夠通過按鍵的力度來控制跳躍的高度,達到更靈活的效果。那是怎么做到的呢?
其實實現(xiàn)這個效果也很簡單,所謂玩家按鍵力度不過就是按鍵時間的長久,按的時間長也就是施加跳躍力的時間就長,跳的當然高了,半路上如果玩家不給力了,當然會跳到一半掉鏈子,不過玩家有種錯覺就是想跳得高就使勁按著跳躍鍵,不想跳了松開鍵就是-_-
CCPoint jumpForce = ccp(0.f, 310.f); //向上的跳躍力 玩家一直按著跳躍鍵時的跳躍力 float jumpCutOff = 150.f; //玩家沒有按住跳躍鍵時的跳躍力 if(this->bMightAsWellJump && this->onGround) //如果當前玩家按了跳躍鍵并且在地上 { this->_velocity = ccpAdd(this->_velocity, jumpForce); //跳躍就是加上一個向上的速度 } else if (!this->bMightAsWellJump && this->_velocity.y > jumpCutOff) //玩家沒有按住跳躍鍵,并且向上的速度已經(jīng)超過了設(shè)定的值,就限定向上跳躍速度 { this->_velocity = ccp(this->_velocity.x, jumpCutOff); }
注釋解釋的很清楚,就不多解釋了。
好了,build一下run下我們的游戲吧,看吧,我們的小考拉可以自由歡騰地上下翻飛了。
跳是跳的很歡了,不過悲劇的是,它跳到最右邊就跳出屏幕,看不見了。
來修正這個問題,這個問題其實就是視點跟隨,在cocos2dx上有解決這一問題的標準算法,貼出代碼:
void GameLevelLayer::setViewpointCenter(cocos2d::CCPoint pos) { CCSize winSize = CCDirector::sharedDirector()->getWinSize(); //限定角色不能超過半屏 int x = MAX(pos.x, winSize.width/2); int y = MAX(pos.y, winSize.height/2); //限定角色不能跑出屏幕 x = MIN(x, (_map->getMapSize().width * _map->getTileSize().width) - winSize.width/2); y = MIN(y, (_map->getMapSize().height * _map->getTileSize().height) - winSize.height/2); CCPoint actualPosition = ccp(x, y); CCPoint centerOfView = ccp(winSize.width/2, winSize.height/2); CCPoint viewPoint = ccpSub(centerOfView, actualPosition); //設(shè)定一下地圖的位置 _map->setPosition(viewPoint); }
方法參數(shù)就是玩家考拉當前位置。這個方法可以不但能左右跟隨還能上下跟隨主角,非常好用。這個方法原理在很多博客都有提到,原理其實就是地圖在跟玩家做返方向運動,大家可以查閱一下其他文章解釋它的原理,不過我在這里不想再多說了,不是一兩句能說清,我們只要會用這個方法就行了。
我們要做的就是在GameLevelLayer類的update方法里調(diào)用就行了,可以放在update方法結(jié)尾之前,如下:
this->setViewpointCenter(_player->getPosition());
現(xiàn)在build再run一下,這回我們的小考拉再也不會跑出屏幕外了!
嘗一下受傷的滋味!
現(xiàn)在我們可以著手做游戲過關(guān)和GameOver的功能了。
地圖里有個hazards層,這個層里放置一些考拉碰上就會掛的object。其實本質(zhì)也上碰撞檢測,看代碼:
void GameLevelLayer::handleHazardCollisions(Player* player) { CCArray *tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _hazards); CCDictionary* dic = NULL; CCObject* obj = NULL; float x=0.f; float y = 0.f; int gid = 0; CCARRAY_FOREACH(tiles, obj) { dic = (CCDictionary*)obj; x = dic->valueForKey("x")->floatValue(); y = dic->valueForKey("y")->floatValue(); CCRect tileRect = CCRectMake(x, y, _map->getTileSize().width, _map->getTileSize().height); CCRect pRect= player->collisionBoundingBox(); gid = dic->valueForKey("gid")->intValue(); if (gid && tileRect.intersectsRect(pRect)) //如果有釘刺并且玩家與它發(fā)生碰撞了,gameOver { this->gameOver(false); } } }
代碼看上去是不是很熟悉呀,你沒記錯,其實就是從checkAndResolveCollisions方法里拷來的,只不過沒分那么多情況而且處理方式也簡單了只是調(diào)用了gameOver方法,gameOver的布爾值參數(shù)為true表示游戲勝利,為false就是失敗
_hazards在GameLevelLayer類也是個CCTMXLayer* 地圖層類型的成員變量,在此類的init方法里初始化它:
_hazards = _map->layerNamed("hazards");
然后在update方法里調(diào)用這個方法,現(xiàn)在update方法是這個樣子:
void GameLevelLayer::update(float delta) { _player->update(delta); this->handleHazardCollisions(_player); this->checkForAndResolveCollisions(_player); this->setViewpointCenter(_player->getPosition()); }
現(xiàn)在實現(xiàn)gameOver方法了,當玩家跳到harads層里的刺上時,我們會調(diào)用這個方法游戲結(jié)束,或者當玩家走到終點時,我也會調(diào)用這個方法,這個方法會顯示出一個restart按鈕,并在屏幕上打印出一些信息,告訴玩家你死了或者你贏了之類的-_-
void GameLevelLayer::gameOver(bool bWon) { bGameOver = true; CCString* gameText; if (bWon) { gameText = CCString::create("You Won!"); } else gameText = CCString::create("You have Died!"); CCLabelTTF* diedLabel = CCLabelTTF::create(gameText->getCString(), "Marker Felt", 40); diedLabel->setPosition(ccp(240, 200)); CCMoveBy *slideIn = CCMoveBy::create(1.f, ccp(0, 250)); CCMenuItemImage* replay = CCMenuItemImage::create("replay.png","replay.png","replay.png", this, menu_selector(GameLevelLayer::restartGame)); CCArray *menuItems = CCArray::create(); menuItems->addObject(replay); CCMenu *menu = CCMenu::create(); menu->addChild(replay); menu->setPosition(ccp(240, -100)); this->addChild(menu); this->addChild(diedLabel); menu->runAction(slideIn); }
方法開頭的變量bGameOver也是GameLevelLayer類新定義的一個成員變量,在init里要初始化為false,此變量表示游戲是否結(jié)束。這個變量作用是你在update方法里開頭判斷一下,如果bGameOver==true,則直接返回不要做任何事了。如下:
void GameLevelLayer::update(float delta) { if(bGameOver) return; _player->update(delta); this->handleHazardCollisions(_player); this->checkForAndResolveCollisions(_player); this->setViewpointCenter(_player->getPosition()); }
現(xiàn)在你再編譯運行,碰上釘板試試,會出現(xiàn)杯具性的結(jié)局...
不要嘗試的太多,小心動物保護組織會找上你家門來(-_-這就是所謂的美式幽默嗎?)
修正一個致命BUG
還記得前面出現(xiàn)過當考拉掉出地圖出現(xiàn)的事情嗎?在游戲地圖里有些坑我們本意是考拉掉下去游戲會結(jié)束,但事實上程序崩掉了。凡是帶有TILED地圖的都會出現(xiàn)這樣的問題,只要角色走出地圖邊界游戲就會崩掉,這個問題困擾了好多人,也包括我。其實解決這個BUG很簡單,關(guān)鍵在tileGIDAt方法。
此方法你打開源碼實現(xiàn)就知道,它有個CCASSERT宏,判斷出地圖就會拋出異常。本來嗎此方法是取地圖上某處tile的gid,你都出地圖了還取什么當然要拋異常啦。這里我們就在getSurroundingTilesAtPosition方法里加個判斷,注意當然要在調(diào)用tileGIDAt方法之前,不讓考拉出地圖:
if (tilePos.y > (_map->getMapSize().height-1)) { this->gameOver(false); return NULL; }
在checkForAndResolveCollisions方法里有調(diào)用getSurroundingTilesAtPosition方法,如果它返回個空數(shù)組可就不妙了,所以在checkForAndResolveCollisions也要改一下,在 CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _walls);一句之后,加上
if (bGameOver) //可能玩家掉坑里,就不處理了 { return; }
編譯再運行,把考拉掉進坑里試試,發(fā)現(xiàn)程序不再DOWN了!
Winner!
在最后,我們處理下考拉走到終點時顯示勝利的情況。
這里我們簡單點,只是判斷下考拉向右走了多遠就取勝
void GameLevelLayer::checkForWin() { if (_player->getPositionX()>3130.0) { this->gameOver(true); } }
真實游戲里我們通常在終點放置一個object,如門呀城堡小旗什么的,主角碰到就勝利了可進入下一關(guān),有興趣的自己嘗試!
這個方法也要放到GameLevelLayer的update方法里,如下:
void GameLevelLayer::update(float delta) { if(bGameOver) return; _player->update(delta); this->handleHazardCollisions(_player); this->checkForWin(); this->checkForAndResolveCollisions(_player); this->setViewpointCenter(_player->getPosition()); }
運行一下,走到終點試試:
添加音效
勝利了之后沒有音樂怎么行,一個無聲的世界可真是會令人絕望呀,來我們加些音效吧!
在GameLevelLayer.cpp和 player.cpp加上頭文件如下:
#include "SimpleAudioEngine.h"
using namespace CocosDenshion;
在GamelevelLayer的init方法里加上:
SimpleAudioEngine::shareEngine()->playBackgroundMusic("level1.mp3");
在player.cpp里的update方法里判斷玩家跳的地方加上音效:
if(this->bMightAsWellJump && this->onGround) //如果當前玩家按了跳躍鍵并且在地上 { this->_velocity = ccpAdd(this->_velocity, jumpForce); //跳躍就是加上一個向上的速度 SimpleAudioEngine::sharedEngine()->playEffect("jump.wav"); }
在GameLevelLayer.cpp的gameOver方法里也加上音效:
void GameLevelLayer::gameOver(bool bWon) { if (bGameOver) //不要反復調(diào)用 { return; } bGameOver = true; SimpleAudioEngine::sharedEngine()->playEffect("hurt.wav");
編譯運行,試一試你親手制作的帶有音樂感的游戲吧!
×××地址:下載
剩下我們還能做什么?
要做的還有什么,角色動畫沒有吧,此外還有怪物,AI, 關(guān)卡,角色狀態(tài)機等等。這些都在IOS GAME三件套 Platformer Start Kit里,前面說過,本教程只是這個Start Kit的前奏曲,那真正的Platformer game是什么樣的呢?
學完此教程后,你可以學到
怎么樣?心動了吧,涉于版權(quán)問題不便在此公開,有興趣的可到此看看: 橫版平臺游戲源碼
此外大名鼎鼎的三件套之一橫版格斗游戲 Beat 'Em Up Game Starter Kit 也是非常精彩哦!網(wǎng)上公開的源碼教程還不到里面的二十分之一-_-
有興趣可到此看看: 橫版格斗游戲源碼
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機、免備案服務(wù)器”等云主機租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價比高”等特點與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。