cocos2d-x 如何製作一個類馬里奧的橫版平臺動作遊戲續 2


     歡迎回來,上篇我們講到了物理引擎中重力環境模擬以及主角考拉與地面牆壁的碰撞,相信大家已經對2D世界的物理模擬有了一定的瞭解,現在我們接着講如何讓考拉動起來吧!

     

讓考拉動起來!
    這裏控制考拉移動變得非常簡單,它只有向前和跳兩個能力(源碼中我加了考拉向後走功能,建議大家自己加幾個虛擬按鍵來實現更非富的功能)如果你按着屏幕左半部考拉會向前走,按住右半部考拉會跳起來(原文設定考拉不會後退-_-)。
    我們需要在Player.h里加兩個成員變量:
    bool _forwardMarch;  //是否向前走
    bool _mightAsWellJump; //可以跳躍嗎?
在player.cpp的init方法或在構造函數裏把它們設置爲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);  //設置可觸摸
然後寫註冊觸摸方法

void GameLevelLayer::registerWithTouchDispatcher()
{
	CCDirector* pDirector = CCDirector::sharedDirector();
	pDirector->getTouchDispatcher()->addStandardDelegate(this, 0); //註冊多點觸摸
}

現在,讓我們看看那三個觸摸事件吧!

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點位置轉換爲本地座標
		if (touchLocation.x > 240)   //在屏幕最右邊點,就是跳
		{
			_player->bMightAsWellJump = true;
		}
		else 
		{
			_player->bForwardMarch = true;
		}

	}
}

void GameLevelLayer::ccTouchesEnded(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent)
{
	
	_player->bForwardMarch = false; //鬆開按鍵時,設置爲不可跳也不是向前狀態
	_player->bMightAsWellJump = false;
}

代碼一目瞭然,ccTouchesBegan時根據玩家按的位置決定了考拉狀態是前進還是跳躍,鬆開按鍵時將這兩個狀態變量重置爲false。


真正的角色移動是在player的update方法裏進行的,看代碼:

void Player::update(float delta)
{
	CCPoint gravity = ccp(0.f, -450.f);  //考拉每秒下降450個單位
	CCPoint gravityStep = ccpMult(gravity, delta); //計算在重力影響下delta時間內具體下降了多少, 即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); //當前期望要去的位置=當前位置+當前速度
}

讓我們來詳細地看一下新增部分:


  1. 當玩家觸摸前進的時候和重力模擬類似,我們加了一個向前的推進力800.f,處理方式與重力相同

  2. 第2步橫向速度乘以0.9是模擬摩擦力效果,考慮下當有向前推力時玩家會向前移動,沒有了呢?是不是立即停下來?那樣看起來太生硬了,所以這裏x軸速度每幀乘以0.9就起到了均勻減速的效果,就像摩擦力一樣

  3. 檢查一下玩家是否按了前,按前進時速度上就要加上向前的推動力,就起到了向前進的效果了

  4. 這一步是調用了clamp是限定速度前後上下不要太大,給它一個最大值。之前的那句設定下落速度最大值可以去掉了。

好了運行一下,我們可以看到我們的主角小考拉現在可以前進了!

               

讓考拉跳起來!
 跳躍是動作遊戲裏最明顯的一個特徵。我們希望角色跳的平滑逼真,現在讓我們來實現它。
 到player類的update方法裏,在if(this->_forwardMarch)語句之前加上下面代碼:

CCPoint jumpForce = ccp(0.f, 310.f);

if(this->_mightAsWellJump && this->_onGround)
{
  this->_velocity = ccpAdd(this->_velocity, jumpForce);
}

   只要加一個向上的力,角色就可以跳起來了。
    如果你止步於此,你會得到一個老式的雅代利式跳躍,即每次跳躍都是相同的高度,每次你都施給玩家一個同樣的力,然後等着重力把你拉回地面來。
     這麼做似乎沒什麼不妥,如果你要求不高的話,但是仔細觀察一下各種流行的平臺遊戲,如超級馬里奧,索尼克,洛克人,水上魂鬥羅等,似乎玩家能夠通過按鍵的力度來控制跳躍的高度,達到更靈活的效果。那是怎麼做到的呢?
其實實現這個效果也很簡單,所謂玩家按鍵力度不過就是按鍵時間的長久,按的時間長也就是施加跳躍力的時間就長,跳的當然高了,半路上如果玩家不給力了,當然會跳到一半掉鏈子,不過玩家有種錯覺就是想跳得高就使勁按着跳躍鍵,不想跳了鬆開鍵就是-_-

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) //玩家沒有按住跳躍鍵,並且向上的速度已經超過了設定的值,就限定向上跳躍速度
	{
		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);
	//設定一下地圖的位置
	_map->setPosition(viewPoint);
}

     方法參數就是玩家考拉當前位置。這個方法可以不但能左右跟隨還能上下跟隨主角,非常好用。這個方法原理在很多博客都有提到,原理其實就是地圖在跟玩家做返方向運動,大家可以查閱一下其他文章解釋它的原理,不過我在這裏不想再多說了,不是一兩句能說清,我們只要會用這個方法就行了。
    我們要做的就是在GameLevelLayer類的update方法裏調用就行了,可以放在update方法結尾之前,如下:
    this->setViewpointCenter(_player->getPosition());
    現在build再run一下,這回我們的小考拉再也不會跑出屏幕外了!



嘗一下受傷的滋味!
現在我們可以着手做遊戲過關和GameOver的功能了。
地圖裏有個hazards層,這個層裏放置一些考拉碰上就會掛的object。其實本質也上碰撞檢測,看代碼:

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))  //如果有釘刺並且玩家與它發生碰撞了,gameOver
		{
			this->gameOver(false);
		}
	}
}

代碼看上去是不是很熟悉呀,你沒記錯,其實就是從checkAndResolveCollisions方法裏拷來的,只不過沒分那麼多情況而且處理方式也簡單了只是調用了gameOver方法,gameOver的布爾值參數爲true表示遊戲勝利,爲false就是失敗
_hazards在GameLevelLayer類也是個CCTMXLayer* 地圖層類型的成員變量,在此類的init方法裏初始化它:
_hazards = _map->layerNamed("hazards");
然後在update方法裏調用這個方法,現在update方法是這個樣子:

void GameLevelLayer::update(float delta)
{
	_player->update(delta);

	this->handleHazardCollisions(_player);
	
	this->checkForAndResolveCollisions(_player);
	this->setViewpointCenter(_player->getPosition());
}

現在實現gameOver方法了,當玩家跳到harads層裏的刺上時,我們會調用這個方法遊戲結束,或者當玩家走到終點時,我也會調用這個方法,這個方法會顯示出一個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,此變量表示遊戲是否結束。這個變量作用是你在update方法裏開頭判斷一下,如果bGameOver==true,則直接返回不要做任何事了。如下:

void GameLevelLayer::update(float delta)
{
	if(bGameOver)
		return;

	_player->update(delta);

	this->handleHazardCollisions(_player);
	
	this->checkForAndResolveCollisions(_player);
	this->setViewpointCenter(_player->getPosition());
}

現在你再編譯運行,碰上釘板試試,會出現杯具性的結局...

不要嘗試的太多,小心動物保護組織會找上你家門來(-_-這就是所謂的美式幽默嗎?)


修正一個致命BUG
還記得前面出現過當考拉掉出地圖出現的事情嗎?在遊戲地圖裏有些坑我們本意是考拉掉下去遊戲會結束,但事實上程序崩掉了。凡是帶有TILED地圖的都會出現這樣的問題,只要角色走出地圖邊界遊戲就會崩掉,這個問題困擾了好多人,也包括我。其實解決這個BUG很簡單,關鍵在tileGIDAt方法。
此方法你打開源碼實現就知道,它有個CCASSERT宏,判斷出地圖就會拋出異常。本來嗎此方法是取地圖上某處tile的gid,你都出地圖了還取什麼當然要拋異常啦。這裏我們就在getSurroundingTilesAtPosition方法里加個判斷,注意當然要在調用tileGIDAt方法之前,不讓考拉出地圖:

if (tilePos.y > (_map->getMapSize().height-1))
{
	this->gameOver(false);
	return NULL;
}

在checkForAndResolveCollisions方法裏有調用getSurroundingTilesAtPosition方法,如果它返回個空數組可就不妙了,所以在checkForAndResolveCollisions也要改一下,在 CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _walls);一句之後,加上

if (bGameOver) //可能玩家掉坑裏,就不處理了
	{
		return;
	}

編譯再運行,把考拉掉進坑裏試試,發現程序不再DOWN了!



Winner!
   在最後,我們處理下考拉走到終點時顯示勝利的情況。
      這裏我們簡單點,只是判斷下考拉向右走了多遠就取勝

void GameLevelLayer::checkForWin()
{
	if (_player->getPositionX()>3130.0)
	{
		this->gameOver(true);
	}
}

真實遊戲裏我們通常在終點放置一個object,如門呀城堡小旗什麼的,主角碰到就勝利了可進入下一關,有興趣的自己嘗試!
這個方法也要放到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)  //不要反覆調用 
	{
		return;
	}
	bGameOver = true;
	SimpleAudioEngine::sharedEngine()->playEffect("hurt.wav");

編譯運行,試一試你親手製作的帶有音樂感的遊戲吧!
源碼下載地址:下載

剩下我們還能做什麼?
要做的還有什麼,角色動畫沒有吧,此外還有怪物,AI, 關卡,角色狀態機等等。這些都在IOS GAME三件套 Platformer Start Kit裏,前面說過,本教程只是這個Start Kit的前奏曲,那真正的Platformer game是什麼樣的呢?

學完此教程後,你可以學到









怎麼樣?心動了吧,涉於版權問題不便在此公開,有興趣的可到此看看: 橫版平臺遊戲源碼
此外大名鼎鼎的三件套之一橫版格鬥遊戲 Beat 'Em Up Game Starter Kit 也是非常精彩哦!網上公開的源碼教程還不到裏面的二十分之一-_-












有興趣可到此看看: 橫版格鬥遊戲源碼












發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章