cocos2d-x 如何製作一個類馬里奧的橫版平臺動作遊戲 1 獻給所有對動作遊戲有愛的朋友

     本文翻譯自國外著名IOS源碼教學商業網站raywenderlich 的IOS Game Start Kits三件套之一的Platformer Game/平臺動作遊戲的前奏曲,另一個是Beat'Em up Game/橫版格鬥遊戲,作者是國外著名遊戲開發專家Jake Gundersen,曾參與開發過SFC時代的洛克人X系列。 
原文網址:
http://www.raywenderlich.com/15230/how-to-make-a-platform-game-like-super-mario-brothers-part-1
開篇之前先懷舊一番吧!

    還記得超級馬里奧的青青草地藍天白雲嗎?還記得曾讓人愛恨交加又不屈不撓讓人不忍放棄的洛克人ZERO嗎,我們燃起小宇宙一招龍炎刃擊敗最終Boss的場面是曾多麼熱血澎湃!這些感動一代人的遊戲陪伴了我們80後兒時整整一個時代。橫版平臺動作遊戲,作爲FC時代最早的遊戲類型,以美麗精緻的遊戲畫面,曲折有趣的關卡設計,豐富流暢的動作設定深深地俘獲住了衆玩家的芳心,讓玩家過足了在遊戲世界裏冒險探索的癮。這個懸崖怎麼跳過去?這個機關怎麼破解?這個洞裏是不是還有隱藏寶物?下一關是啥樣的?最終Boss到底是誰該怎麼打?無數的問題讓玩家欲罷不能。現在IOS時代這種遊戲的光環已經漸漸褪散了,代之以沒劇情沒關卡只有背景無限滾動的無腦跑酷模式。這樣的快餐遊戲我已經不想再說什麼了,一句話,任何所謂的創新類型都無法超越經典,大家是不是已經燃起想學習的Cosmos了?那麼請看下面吧!
本教程所需要的美術資源:resources 源碼見文章未尾

創建一個簡單易用的2D物理引擎:
    並非所有的2D遊戲都需要像小鳥那樣複雜的物理引擎,如超級馬里奧,洛克人索尼克這種其實只需要着重模擬重力環境和平臺碰撞,以及行走時的動力和摩擦力即可,關鍵一點就是能模擬運動,即在重力環境下的走,跑,跳和下落,模擬的好的話動作會非常流暢和自然,否則就非常生硬,此外就是碰撞檢測。
我們的主角是一隻可愛的考拉(樹袋熊),在你下面要實現的簡單物理引擎裏,你的考拉有這幾個物理變量:當前速度(velocity),運動加速度(acceleration)和當前位置(position)以及其他。你將利用這些變量實現下面的算法:
1.玩家啓用了跳躍和和移動操作了嗎?
2.如果是就給考拉一個跳躍或移動的力
3.同時時刻不停地給考拉加以重力
4.計算出各種力作用下的考拉速度
5.根據計算出的速度計算出考拉當前位置,並更新
6.檢查考拉與牆和地等其他物體的碰撞
7.如果發生了碰撞,就通過將考拉移回碰撞發生前的位置來解決,直至碰撞不再發生,如果與怪物發生了碰撞,則可憐的考拉要承受傷害
你在每幀都要做這些操作,在遊戲世界裏,重力是一直都穩定存在的把考拉向下拉向地面,但是在碰撞解決步驟裏又會把考拉向上推至地面頂。

加載TMXTiledMap:
假設大家對Tiled map editor工具已經非常熟悉,讓我們打開下載的資源目錄裏的level1.tmx,就會看到:

你會發現地圖有三個層,分別是:

  • hazards: 這個層包含了一些objects我們的小考拉碰到了就會受傷-_-

  • walls:本層包含的tiles是考拉不能穿過的,也就是牆壁,地面這些物體

  • background: 本層是背景圖,只是些裝飾用的,如山呀雲呀樹之類,不會起到碰撞反應

終於可以coding了,讓我們先建立一個GameLevelLayer的類,就是遊戲層啦,具體代碼我就不全貼了,可以參考源碼,頭文件類似這個樣子:

class GameLevelLayer : public cocos2d::CCLayer
{
public:
	GameLevelLayer(void);
	~GameLevelLayer(void);
	CREATE_FUNC(GameLevelLayer);

	bool init();

	static cocos2d::CCScene* scene();
  protected:
       cocos2d::CCTMXTiledMap *map;
};

下面我們在init里加入地圖,如下:

//加載一個藍色背景當裝飾
		CCLayerColor *blueSky = CCLayerColor::create(ccc4(100, 100, 250, 255));
		this->addChild(blueSky);

		//加載地圖
		_map = CCTMXTiledMap::create("level1.tmx");
		this->addChild(_map);

我們先加了一個帶有藍色背景的CCLayerColor作爲藍天,下面兩行就是大家熟知的加載地圖了。
還有scene()的方法:

CCScene* GameLevelLayer::scene()
{
	CCScene *scene = CCScene::create();
	if(!scene)
		return NULL;

	GameLevelLayer *layer = GameLevelLayer::create();

	scene->addChild(layer);

	return scene;
}


好了現在運行遊戲可以看到地圖了,接着再加我們的主角考拉,在GameLevelLayer.h里弄一個前向聲明class Player;然後加一個成員變量Player* _player;
在GameLevelLayer.cpp里加上主角考拉:

//地圖上加載主角考拉熊
		_player = Player::create("koalio_stand.png");
		_player->setPosition(ccp(100, 50));
		_map->addChild(_player, 15);

類Player的頭文件Player.h

class Player : public cocos2d::CCSprite
{
public:
	Player(void);
	~Player(void);
	//以圖片初始化
	virtual bool initWithFile(const char *pszFilename);

	static Player* create(const char *pszFileName);
	void update(float delta);
}

其實就是用一張紋理貼圖初始化,給了考拉一個zorder,使它能顯示在地圖之上,player.cpp的關鍵代碼如下:

bool Player::initWithFile(const char *pszFilename)
{
	 CCAssert(pszFilename != NULL, "Invalid filename for Player");

	 //作些自己的初始化
	 bool bRet = CCSprite::initWithFile(pszFilename);
         _velocity = ccp(0.f, 0.f); //速度初始化
	 return bRet;
}
Player* Player::create(const char *pszFileName)
{
	Player *pobPlayer = new Player();
	if (pobPlayer && pobPlayer->initWithFile(pszFileName))
	{
		pobPlayer->autorelease();
		return pobPlayer;
	}
	CC_SAFE_DELETE(pobPlayer);
	return NULL;
}

運行遊戲,可以看到考拉已經出現在我們眼前:

考拉看上去懸在空中,下一步就是要給它添加重力支持了!

考拉的重力環境模擬
前面說了,考拉目前所處的世界非常簡單,就是重力把考拉向下拉,而地面將考拉向上託,在經典牛頓力學裏,決定一個物體運動的主要是速度,加速度和施加在物體上的力這幾個量,我們一一分析:

  • Velocity: 速度,決定了物體在給定的方向上移動的有多快

  • Acceleration:加速度,一定時間內物體的速度變化量

  • force:力,使物體改變運動速度和方向的決定因素

當一個力施加在物體上時(即重力),物體就不會勻速運動(當沒有任何力影響時物體要麼靜止要麼勻速運動),而是以一定的加速度的方式不斷加速度運動,直到有其他的力改變。因爲我們做的是2D遊戲,cocos2dx很好的支持這種運動模擬,不論位置position和速度我們都可以用CCPoint代替,加速度可以考慮用ccpAdd來計算加速度,這些會使編碼更容易。


首先我們在GameLevelLayer的init方法裏啓用update:
this->scheduleUpdate();
GameLevelLayer類的update方法目前就只是調用Player的update,如下:

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

好了,看下Player類的update方法:

void Player::update(float dt)
{//2
  CCPoint gravity = ccp(0.f, -450.f);
 //3
  CCPoint gravityStep = ccpMult(gravity, dt);
 //4
  this->_velocity = ccpAdd(this->_velocity, gravityStep);
 //5
  this->setPosition(ccpAdd(this->getPosition(), stepVelocity));
}

我們來仔細解釋下:

1.在init方法裏我們將Player的_velocity初始化爲(0,0)

2.我們聲明瞭一個代表重力的向量gravity (0,-450.f)y 值爲負表示方向是垂直向下指向地的,這個力使考拉每幀加速度移動450像素,假設一幀時間是1秒,則第一幀考拉由0下落了450個像素,第二幀內會下落900個像素,這裏不是物理學上的9.8因爲計算機上像素和物理學的米單位差別是很大的

3. 我們用ccpMult來計算重力加速度,因爲每幀時間是update參數裏的dt,所以重力加速度值就是gravity*此幀時間dt
4. 計算下一幀速度velocity, 計算加速後的速度值物理學上的計算方法就是當前速度+加速度,如代碼所示
5. 知道了速度我們就可以計算出物體位置了,先要計算出物體在本幀內的位移,就是速度*時間間隔,然後下一幀位置當前就是當前位置+位移了,如第5步所示

好了,大功告成,重力模擬就靠這幾句簡單的代碼就已經模擬了,現在編譯運行,看看效果吧!

不過....可悲的是,考拉的確能做自由落體下落運動了,不過也不出意外的掉出了屏幕,程序Down掉!

不要在黑夜裏瞎摸 --- 碰撞檢測

   看來只有重力模擬是遠遠不能代表遊戲裏的物理世界,在任何物理引擎裏,碰撞檢測與處理都是最核心和基本的部分。檢測碰撞首要解決的問題是要計算出遊戲角色的包圍盒,不過可喜的是cocos2d-x已經提供了這樣的函數boundingBox,是根據提供的資源紋理來計算包圍盒的,紋理多大包圍盒就多大,實際使用中還需要修正。因爲美術給的紋理圖片不可避免的周圍會留下空白透明部分,所以需要適當放縮。

在Player.h裏,加入這一個方法:
cocos2d::CCRect collisionBoundingBox(); //返回考拉的包圍盒
Player.cpp實現如下:

CCRect Player::collisionBoundingBox()
{
	//這裏要將包圍盒寬度-2個單位,但中心點不變
	CCRect collisionBox = Tools::CCRectInset(this->boundingBox(), 3, 0);
	
	return returnBoundingBox;
}

在IOS版本的cocos2d-iphone裏有CGRectInset方法,能使一個矩形在x軸和y軸上放縮指定像素大小,正值爲縮小負值爲放大,但可惜cocos2d-x版本沒有提供此方法,需要自己實現,我寫在了一個Tools類的一個靜態方法,代碼如下:



CCRect Tools::CCRectInset(CCRect &rect, float dx, float dy)
{
	rect.origin.x += dx;
	rect.size.width -= dx * 2;
	rect.origin.y -= dy;    //縮小時y軸應該向下,IOS的座標系與cocos2d-x不一樣,y原點是在左下角而非左上角
	rect.size.height -= dy * 2;
	return rect;
}

此代碼完全是按照原MAC版CGRectInset方法來的,效果跟它一樣,可以放心使用

舉重開始!
我們的考拉學會了下落,下一步就要教它舉重了(什麼?你想說我很胖?)這裏的舉重不是考拉舉,當然是地了
我們需要寫一些方法來實現地面與考拉的碰撞檢測,如下:

  • 實現找到考拉周圍8個tile磚塊的方法,這8個tiles分別是左右上下和四個對角的tile,正好包圍住我們的主角

  • 實現決定哪些tile是能產生碰撞的tile,有些tile是沒有物理屬性的,沒雲,樹等背景,這些是不需要檢測的

  • 一個來解決碰撞的方法

你還需要在GameLevelLayer類裏創建兩個工具函數便於解決問題:

  • 計算考拉在地圖中的tile座標的方法

  • 根據地圖上某一tile座標返回它的tile的Rect包圍盒的方法

這兩個方法很好實現,代碼如下:

CCPoint GameLevelLayer::tileCoordForPosition(cocos2d::CCPoint position)
{
	float x = floor(position.x / _map->getTileSize().width); //位置x值/地圖一塊tile的寬度即可得到x座標
	float levelHeightInPixels = _map->getMapSize().height * _map->getTileSize().height; //地圖的實際高度
	float y = floor((levelHeightInPixels - position.y)/_map->getTileSize().height);  //地圖的原點在左上角,與cocos2d-x是不同的(2dx原點在左下角)
	return ccp(x, y);
}
CCRect GameLevelLayer::tileRectFromTileCoords(cocos2d::CCPoint tileCoords)
{
	float levelHeightInPixels = _map->getMapSize().height * _map->getTileSize().height; //地圖的實際高度
	//把地圖座標tileCoords轉化爲實際遊戲中的座標
	CCPoint origin = ccp(tileCoords.x * _map->getTileSize().width, levelHeightInPixels - ((tileCoords.y+1)*_map->getTileSize().height));
	return CCRectMake(origin.x, origin.y, _map->getTileSize().width, _map->getTileSize().height);
}

註釋寫的很清楚,大家很容易理解.方法1裏求y時是用地圖高度-座標高度,這個是因爲遊戲裏採用OpenGL座標系左下角爲原點,而tileMap的座標系的原點在左上角,所以要減一下。第二個方法返回指定tile座標處tile的Rect,因爲每個tile都是有大小的,而這個tile包圍盒後面要用到,所以要計算一下,計算方法與第一個大同小異。


我被Tiles包圍啦!
現在我們要真正實現第一個方法,計算求出考拉周圍的8個tile,因爲只有求出包圍在主角身邊的包圍tile,這些tile可能是牆,可能是地,纔好利用這些tile來和主角碰撞檢測。在這個方法裏我們要構建一個數組來返回這8個tile的GID,還要有這個tile的頂點origin,還有包圍盒CCRect信息。
還有這個數組包含的tiles必須要有優先級排好序以利於我們解決碰撞。舉個例子,我們總是希望先檢查考拉的左,右,下,上這4個tile,然後再考慮相對來說不是很重要的對角線,下面是這個方法,也是放在GameLevelLayer裏

CCArray* GameLevelLayer::getSurroundingTilesAtPosition(cocos2d::CCPoint position, cocos2d::CCTMXLayer* layer)
{
	CCPoint plPos = this->tileCoordForPosition(position); //1 返回此處的tile座標
	//存gid的數組
	CCArray* gids = CCArray::create();//2
	gids->retain();
	//3 我們的目的是想取出環繞在精靈四周的8個tile,這裏就從上至下每行三個取9個tile(中間一個不算)仔細畫畫圖就知代碼的意義
	for (int i=0; i<9; i++)
	{
		int c = i % 3;   //相當於當前i所處的列
		int r = (int)(i/3); //相當於當前i所處的行
		
		CCPoint tilePos = ccp(plPos.x + (c-1), plPos.y + (r-1));
		
		//4 取出包圍tile的gid
		int tgid = layer->tileGIDAt(tilePos);
                //5
		CCRect tileRect = this->tileRectFromTileCoords(tilePos);  //包圍盒
		float x = tileRect.origin.x;  //位置
		float y = tileRect.origin.y;
		//取出這個tile的各個屬性,放到CCDictionary裏
		CCDictionary *tileDict = CCDictionary::create();
		CCString* str_tgid = CCString::createWithFormat("%d",tgid);
		CCString* str_x = CCString::createWithFormat("%f", x);
		CCString* str_y = CCString::createWithFormat("%f", y);
		tileDict->setObject(str_tgid, "gid");
		tileDict->setObject(str_x, "x");
		tileDict->setObject(str_y, "y");
		tileDict->setObject((CCObject *)&tilePos, "tilePos");
		//6
		gids->addObject(tileDict);
	}
	//去掉中間(即自身結點tile)
	gids->removeObjectAtIndex(4);
	gids->insertObject(gids->objectAtIndex(2), 6);
	gids->removeObjectAtIndex(2);
	gids->exchangeObjectAtIndex(4, 6);
	gids->exchangeObjectAtIndex(0, 4);//7

	CCDictionary* d = NULL;
	CCObject *obj = NULL;
	CCARRAY_FOREACH(gids, obj)
	{
		d = (CCDictionary*)obj;
		CCLog("%d", d);//8
	}

	return gids;
}

代碼很長也很多,不用擔心,我們會一點一點給它解釋清楚
在開始之前,注意到這個方法有一個CCTMXLayer*參數,之前提到我們檢測衝突時有些是不希望檢測到的,如背景,所以在tilemap地圖裏分好了三個層,一個hazards是放敵人和陷阱的,當考拉碰到會受傷害,一個是walls層,就是現在我們主要討論的地和牆,考拉碰上了一般是退回原位。還有一個就是backgrounds背景層,只是起個裝飾作用,就不需要考慮怎麼碰撞了。
開始分析代碼:
1. 第一件要做的事就是將傳入的position(考拉在遊戲層中的位置)轉換爲地圖中的tile座標


2. 接着,我們create了一個數組用來返回8個tiles所需要的信息

3.然後開始循環9次 包括8個包圍考拉的tile還有考拉自己所站的位置。一一計算出這9個tile的地圖tile座標放在tilePos變量裏。

4.第4步是調用tileGIDAt方法返回tilePos位置的tile的GID,如果當前位置沒有tile,則GID返回0,我們可以據此判斷當前位置有沒有tile

5.接着用我們定義好的方法計算出tilePos處的tile的包圍盒CCRect,以及頂點位置屬性,我們會把存入一個CCDictionary裏

6.然後在第7步,我們把考拉所在的tile從數組中移除出去,並把這些數組元素按優先級重新排序。我們想先解決考拉身邊下,上,左,右這四個是最優先的,也是遊戲中最容易發生碰撞的,其他纔是對角線的tile

下面這張圖先顯示了這些tiles原先在數組中的次序,接着排序過後的位置,你會發現排過序後位於下,上,左,右的四個tiles最先被處理,瞭解這些次序有助於你以後設置什麼時候考拉接觸到地的標誌位。

方法寫好了,只等調用了,在這之前,先要做些準備工作,就是先要獲取walls層,才能調用這個方法,在GameLevelLayer類的init方法裏,在addChild(_player)之後,加入下面代碼:
_walls = _map->layerNamed("walls");  //_walls就定義爲GameLevelLayer類裏的一個CCTMXLayer*變量吧

然後在update方法裏,加入下面代碼:
this->getSurroundingTilesAtPosition(_player->getPosition(), _walls);
現在編譯執行仍會報錯,因爲我們的考拉還是會掉出地圖外,後面我們會講到如何修正這個問題,但現在要做的是如何讓我們的主角考拉站在地上! 輸出信息如下:


剝奪下考拉的特權!
    what?我只是一隻被人隨意擺佈的小熊,哪有什麼Privileges?!錯,目前爲止這隻小熊有一項了不起的技能就是可以隨意設置自己的position,不受任何偉大的牛頓經典力學控制,就跟超人一般,想飛到哪就飛到哪,你說牛不牛?不過牛歸牛看上去太假了可不行,這裏稍稍做一下更正。
    如果考拉更新它的位置時GameLevelLayers檢測出來了碰撞(雖然目前我們還沒實現這個功能),我們想要考來回退到碰撞發生前的位置而不是像一隻不撞死南牆不回頭的笨貓。
    這時我們就需要考拉類定義一個新的變量 CCPoint _desiredPosition (期望想去的位置)
    爲什麼好好的自己的Position不用非要多此一舉?因爲我們在後面的計算考拉行走,跳躍等狀態時用一系列方法計算出了考拉的位置,但是碰撞檢測系統還要檢測這個位置是否發生了碰撞,是否需要修正,所以這個計算出的這個位置不一定有效,何況還要update過一幀纔是真正的位置,所以我們需要增加這個期望值來方便計算,等所有的碰撞檢測處理完畢後再用這個_desiredPosition設置考拉真正的位置.
好了,我們先在Player.h裏增加這個成員變量 CCPoint _desiredPosition;
並且修改下Player.cpp裏的計算碰撞盒方法:


CCRect Player::collisionBoundingBox()
{
	//這裏要將包圍盒寬度-2個單位,但中心點不變
	CCRect collisionBox = Tools::CCRectInset(this->boundingBox(), 3, 0);
	CCPoint diff = ccpSub(this->_desiredPosition, this->getPosition()); //玩家當前距離與目的地的差距
	CCRect returnBoundingBox = Tools::CCRectOffset(collisionBox, diff.x, diff.y); //計算調整後的碰撞盒,即包圍盒x,y軸方向上移動diff.x, diff.y個單位
	return returnBoundingBox;
}

跟以前相比碰撞盒是基於_desiredPosition的,遊戲層會用它來做碰撞檢測.
還有一點要修改的是在Player.cpp裏的update方法裏最後一句不能直接setPosition了,要修改的是desiredPosition
this->_desiredPosition = ccpAdd(this->getPosition(), stepVelocity); //當前期望要去的位置=當前位置+當前速度


讓我們解決一些碰撞!
是時候解決碰撞了,在GameLevelLayer類里加入下面代碼

void GameLevelLayer::checkForAndResolveCollisions(Player* player)
{
	CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _walls); //1

	CCObject* obj = NULL;
	CCDictionary* dic = NULL;
	CCARRAY_FOREACH(tiles, obj)
	{
		dic = (CCDictionary*)obj;
		CCRect playerRect = player->collisionBoundingBox();  //2 玩家的包圍盒
		int gid = dic->valueForKey("gid")->intValue();  //3 從CCDictionary中取得玩家附近tile的gid值
		if (gid)
		{
			float rect_x = dic->valueForKey("x")->floatValue();
			float rect_y = dic->valueForKey("y")->floatValue();
			float width = _map->getTileSize().width;
			float height = _map->getTileSize().height;
                        //4 取得這個tile的Rect
			CCRect tileRect = CCRectMake(rect_x, rect_y, width, height);

			if (tileRect.intersectsRect(playerRect)) //如果玩家包圍盒與tile包圍盒相撞
			{
                                //5 取得相撞部分
				CCRect intersection = Tools::intersectsRect(playerRect, tileRect); 

				int tileIndx = tiles->indexOfObject(dic); //6 取得dic的下標索引
				  
				if (tileIndx == 0)
				{
					//tile在koala正下方 考拉落到了tile上
					player->_desiredPosition = ccp(player->_desiredPosition.x, player->_desiredPosition.y + intersection.size.height);
				} 
				else if (tileIndx == 1) //考拉頭頂到tile
				{
					//在koala上面的tile,要讓主角向上移移
					player->_desiredPosition = ccp(player->_desiredPosition.x, player->_desiredPosition.y - intersection.size.height);
				}
				else if (tileIndx == 2)
				{
					//左邊的tile
					player->_desiredPosition = ccp(player->_desiredPosition.x+intersection.size.width, player->_desiredPosition.y);
				}
				else if (tileIndx == 3)
				{
					//右邊的tile
					player->_desiredPosition = ccp(player->_desiredPosition.x-intersection.size.width, player->_desiredPosition.y);
				}
				else
				{
					//7 如果碰撞的水平面大於豎直面,說明角色是上下碰撞
					if (intersection.size.width > intersection.size.height)
					{
						//tile is diagonal, but resolving collision vertically
						float intersectionHeight;
						if (tileIndx>5) //說明是踩到斜下的磚塊,角色應該向上去
						{
							intersectionHeight = intersection.size.height;
						}
						else  //說明是頂到斜上的磚塊,角色應該向下託
						{
							intersectionHeight = -intersection.size.height;
						}
						player->_desiredPosition = ccp(player->_desiredPosition.x, player->_desiredPosition.y + intersectionHeight);
					}
					else //如果碰撞的水平面小於豎直面,說明角色是左右撞到
					{
						
						float resolutionWidth;
						if (tileIndx == 6 || tileIndx == 4) //角色碰到斜左邊的tile 角色應該向右去
						{
							resolutionWidth = intersection.size.width;
						}
						else //角色碰到斜右邊的tile, 角色應該向左去
						{
							resolutionWidth = -intersection.size.width;
						}
						player->_desiredPosition = ccp(player->_desiredPosition.x + resolutionWidth, player->_desiredPosition.y ); 
					}
					
				}
			}
		}
	}
	player->setPosition(player->_desiredPosition); //7 把主角位置設定到它期望去的地方
}

又是洋洋灑灑一大段,讓我們好好看看剛纔寫下的是什麼
1.首先我們調用getSurroundingTilesAtPosition方法返回在walls層考拉四周的tiles數組。接着你就針對數組裏每個tile進行循環,檢測主角包圍是否與這些tile相撞,如果發生了碰撞則我們通過改變desiredPosition的辦法來解決
2.在每次循環,我們首先計算出考拉的包圍盒,就像剛剛我們提到的,這個desiredPosition是計算包圍盒的基礎,每當碰撞發生時,我們就要改變desiredPosition值直到不再和tile發生碰撞。
3.下一步是我們要從數組元素中取出GID,這是我們之前存在dictionary裏的。很可能當前tile座標下沒有任何tile存在,這時GID就爲0,如果是這樣,當前循環就可以直接轉到下一次循環。
4.如果當前位置有tile存在,你就需要計算出那個tile的CCRect(就是tile的包圍盒),存入tileRect變量裏,現在你有了主角考拉和tile的包圍盒,就可以檢測它們是否發生碰撞了
5. 爲了檢測碰撞,我們調用了intersectsRect方法檢測是否發生了碰撞,如果有碰撞還要再計算出這個碰撞部分的真實大小CCRect,後面會有用。可惜cocos2d-x版沒有提供計算兩個矩形相交的方法,而cocos2d-iphone版裏卻有CGRectIntersection方法能計算出,汗一個!沒辦法只有自己實現了,我自己實現了一個Tools::intersectsRect(rectA, rectB)可以計算出兩矩形相交部分,目測用到現在好幾年了沒出過問題。


停下來考慮一個比較棘手的問題...
   碰撞是能檢測出來,接下來就是如何解決它們,剛纔我們提到好多遍,將考拉移回碰撞發生前的位置就可以了,但事實果真是如此簡單嗎?看下圖就一目瞭然了。

如圖,這是非常見的情況,考拉前跳後落在地上與地面發生碰撞,我們期望的當然是它能站在地上,但碰撞前是在地面的斜上方,如果照我們之前的說法解決碰撞的辦法是移回前一步所在的位置, 那麼我們看到的是考拉不但向上而且後退了,這顯然不是我們想要的。

同理還有上圖這種跳下撞牆的情況,結果也是大同小異,下跳碰撞到牆後我們希望它是靠在牆邊,而不能還往斜上方移動。
那我們該怎麼決定考拉是上移還是左移呢?其實上面兩種類似的情況隱含了一個決定性的不一樣的地方,就是碰撞發生時的碰撞盒,一個是水平碰撞,一個是垂直碰撞,看下圖:


原文在這裏講的很囉嗦,這裏我說的簡單一點。從圖可以一目瞭然,決定性的不一樣之處就是紅色部分,即考拉與牆壁tile的碰撞矩形。左邊是紅框寬度比高度大,就是考拉落在地上,我們需要的是上移或下移,右圖紅色框是寬度小於高度,就是考拉左右撞牆了,我們需要將考拉左移或右移,就這麼簡單。

回到代碼裏...
回頭我們剛纔討論的checkForAndResolveCollisions方法

6.在第6步 int tileIndx = tiles->indexOfObject(dic) 裏我們取到了當前tile在數組裏的索引號,這個索引號就告訴我們當前tile是在考拉上下左右哪個位置,這就好辦了,根據位置的不同和碰撞盒的寬度和高度的大小比較我們就將考拉前移後移上移下移碰撞盒寬或高的位置,當碰撞是發生在對角線部分時,同樣方法我們可以根據碰撞部分是寬度大還是高度大還決定是前後移還是上下移,這個過程可以看代碼註釋,可能會更好理解一些
7. 最用我們可以調用setPosition來設置考拉真正的位置啦!

讓我們用一下這個方法,在GameLevelLayer的update方法裏,在player->update()一句後面加上這一句:
this->checkForAndResolveCollisions(_player);
好了,編譯執行,看看是不是真的有效果了?


?!開始的時候考拉是落在地上了,但不過1秒又沉到了地面下去!
你能猜到這是爲什麼嗎?我們到底遺漏了什麼?
回憶一下我們在player的update裏做的事,我們不停地施加重力在考拉身上,那個重力就讓考拉不停地向下加速,這樣即使它碰到地地也把它向上擡了,但考拉加速度最終仍會增大到地能承受的地步最終沉下去。
所以當我們解決完碰撞衝突時,不要忘了把考拉速度重置爲0,不論它是上碰還是下落還是左右撞牆,速度一律重設爲0.
首先在player.cpp的update方法裏,在最後一句設置_desiredPosition前面,要限定一下下落的最大速度,防止考拉下落速度過大掉到牆裏。

if(_velocity.y<-kVelocityYMax)
		_velocity = ccp(this->_velocity.x, -kVelocityYMax);

這個kVelocityYMax你可以在player.h裏#define kVelocityYMax 500 //Y方向的最大速度
再回到那個checkForAndResolveCollisions方法,加以改動:
在開頭處:
CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _walls); 
player->_onGround = false;  ///這裏加上這一句
在if(tileIndx==0)裏,設完desiredPosition後,加上下面兩句
player->_velocity = ccp(player->_velocity.x, 0.f);
player->_onGround = true; 
注意你要在player類里加個bool _onGround這個成員變量,表示考拉是否在地上,在player的init方法或構造函數裏設初始值爲false;
接着在if(tileIndx==1)裏,設完desiredPosition後,加上
player->_velocity = ccp(player->_velocity.x, 0.f);  //此時考拉是向上跳頂到磚塊,不是在地上
接着在else分支裏,第一個判斷 if (intersection.size.width > intersection.size.height)裏面第一行加上
player->_velocity = ccp(player->_velocity.x, 0.f); 
緊接着下面:
if (tileIndx>5) 
{
    intersectionHeight = intersection.size.height;
    player->_onGround = true;   ///注意加上這一句
}


好了大功告成,這樣每次我們在考拉下落到地面或上頂到tile時都會把y軸速度重置爲0,並且設是否在地面標誌,這下編譯運行下游戲看看吧!

看到了吧?考拉穩穩地站在了地上!


好了第一部分有關物理引擎的部分講完了,我們寫了好多內容,大家先消化消化,在下一節裏我會接着講考拉的移動,跳躍,陷阱還有過關判定給內容!並把完整源碼奉上!
此外,歡迎大家光臨我的淘寶小店,裏面有很多價廉物美的精品源碼獻給大家!
http://shop66085899.taobao.com



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