用C++語言實現貪喫蛇遊戲

寫在前面
用C++語言寫遊戲再適合不過了,當然不是因爲用它寫起來簡單,(相反那並不簡單),但是其性能絕對是其他語言沒法比的。所以這裏我會用C++實現一個貪喫蛇的遊戲。當然我可能有意隱瞞了你,因爲我們不僅僅是用C++純語言來幹這件事,那會很彆扭,因爲我們需要圖像渲染、聲音、甚至是碰撞檢測(我最喜歡的一個版塊)!所以僅僅用語言是不夠的。
(注:在文章最後我會給出兩個版本的貪喫蛇源碼及涉及到的一些資源)
寫在最前面就是爲了說明我們會用其他的一些工具:DirectX(9.0)、Windows的窗口編程,這些真的沒那麼簡單!如果你之前沒聽說過這些,也不要太過於擔心,因爲我主要是介紹貪喫蛇實現的核心邏輯,嚴格的說,你可以當成數據結構的知識來學,因爲整條蛇是以鏈表爲基礎的!
另外,我用純C語言也實現過一個貪喫蛇的玩意(如果你覺得是的話),先看看遊戲的運行效果:
貪喫蛇版本1:
這裏寫圖片描述
不要小瞧它!它有音樂,也有碰撞,雖然體驗實在是不咋滴,不過他的遊戲編寫過程和遊戲元素的構成還是對之後進一步編寫更棒的遊戲提供了十足的基礎。因爲他是用純語言做的,不需要其他庫等等的支持,所以很適合我們學習借鑑!

至於第二個版本的貪喫蛇就有很大的改變,儘管還有很多地方需要改進和優化,但是他已經超越了第一個版本很多!下面看看遊戲運行效果:
貪喫蛇版本2:
這裏寫圖片描述
是不是有擺脫Dos找到新大陸的感覺,他加入了新的計分模塊。下面我就第二個版本的核心實現做出解釋。
版本二遊戲核心代碼實現
1,蛇身的單個節點實現:

//蛇身單個節點
struct SNAKE {
	bool IsSurvivor;			//當前結點是否存在(被畫)
	int coor_x;					//節點橫座標
	int coor_y;					//節點縱座標
	SNAKE *link;				//指向下一個節點的指針
	//構造函數
	SNAKE(int x, int y, bool survivor = true,SNAKE *link = NULL) {
		//初始化座標值,賦值方式爲tail派生
		coor_x = x;
		coor_y = y;
	}
};

的確,一個十分明白的結構體,我無需做出任何解釋!
2,蛇的整個類實現(基於鏈表)

//蛇精靈類定義——基於單鏈表實現蛇身
class SnakeSprite {
public:
	SnakeSprite(int x = 300, int y = 200);
	~SnakeSprite() { delete snakeHead; }
	bool addTail();								//蛇尾增加長度
	void drawThisSnake();						//繪製當前蛇身
	void positionAction();						//完成蛇身移動(更新每個節點的座標)
	void turnLeft();							//蛇頭的基本轉向
	void turnRight();
	void turnUp();
	void turnDown();
	void recordCurrentDirection(int d = LEFT);	//記錄蛇的當前運動方向,藉助枚舉類
	int getDirection();
	bool IsDeath();								//是否碰撞草叢,是蛇死亡返回true,否則返回false
	void getCurrentPosRect(RECT &rect);
	void getCurrentCoor(int &x, int &y);
protected:
	int len;									//蛇身長度_以塊爲單位
	SNAKE *snakeHead;							//蛇頭指針
	SNAKE *tail;								//蛇尾指針
	SNAKE *beforeTail;							//尾巴節點的前一個節點,方便移動
	int directions;
};

似乎也沒什麼特別之處,但是有幾個地方需要注意,我會在下面着重強調。
3,部分函數實現的解釋
我想強調的就在這裏,貪喫蛇整個遊戲的確簡單,但是真正編寫的時候則需要考慮全面,因爲遊戲的邏輯還是特別強的。
1》整個蛇動起來的立足點:
我們必須記住,遊戲中的蛇並不是你想象的那樣在隨着你的控制而‘遊動’,他是電腦在以飛快的速率刷新屏幕,而你只是改變了蛇的節點座標,而人的眼睛是存在視覺暫留的,這樣就會給你一種遊戲精靈在走動的效果!
2》怎樣用鍵盤控制蛇?

//輸入控制
	if (Key_Down(DIK_UP) && !Key_Down(DIK_RIGHT) 
		&& !Key_Down(DIK_LEFT) && !Key_Down(DIK_DOWN)) {
		theSnake.turnUp();
		if (DOWN != theSnake.getDirection()) {
			theSnake.recordCurrentDirection(UP);
		}
	}
	if (Key_Down(DIK_RIGHT) && !Key_Down(DIK_UP)
		&& !Key_Down(DIK_LEFT) && !Key_Down(DIK_DOWN)) {
		theSnake.turnRight();
		if (LEFT != theSnake.getDirection()) {
			theSnake.recordCurrentDirection(RIGHT);
		}
	}
	if (Key_Down(DIK_LEFT) && !Key_Down(DIK_UP)
		&& !Key_Down(DIK_RIGHT) && !Key_Down(DIK_DOWN)) {
		theSnake.turnLeft();
		if (RIGHT != theSnake.getDirection()) {
			theSnake.recordCurrentDirection(LEFT);
		}
	}
	if (Key_Down(DIK_DOWN) && !Key_Down(DIK_UP) 
		&& !Key_Down(DIK_RIGHT) && !Key_Down(DIK_LEFT)) {
		theSnake.turnDown();
		if (UP != theSnake.getDirection()) {
			theSnake.recordCurrentDirection(DOWN);
		}
	}

//蛇類的成員函數
void SnakeSprite::turnDown() {
	//向下轉頭
	if (directions != UP) {
		snakeHead->coor_y += 1;
	}
}

void SnakeSprite::turnLeft() {
	//想左轉頭
	if (directions != RIGHT) {
		snakeHead->coor_x -= 1;
	}
}

void SnakeSprite::turnRight() {
	//向右轉頭
	if (directions != LEFT) {
		snakeHead->coor_x += 1;
	}
}

void SnakeSprite::turnUp() {
	if (directions != DOWN) {
		snakeHead->coor_y -= 1;
	}
}

應該是你想象的那樣,我每檢測到玩家按下相應的方向鍵,我會調用snake class的轉彎的成員函數(這就是用class的好處,多麼統一的代碼!),然後緊接着判斷玩家是否企圖直接來個180°的大逆轉(這在貪喫蛇遊戲中是違背規則的),如果真的是這樣我就在函數中不做任何處理,玩家休想達到這種陰謀!但是如果是合法的轉彎(也就是90°),我會改變頭結點(就是蛇的頭部)的座標變化趨勢,就是代碼中那樣做。這樣蛇就任我們控制擺佈了。

3》怎樣實現蛇的移動?

//蛇類的成員函數
void SnakeSprite::positionAction() {
	//實現蛇的自動運動,即依次更新每個節點內座標的值
	if (UP == directions) {
		snakeHead->coor_y--;
	}
	if (DOWN == directions) {
		snakeHead->coor_y++;
	}
	if (LEFT == directions) {
		snakeHead->coor_x--;
	}
	if (RIGHT == directions) {
		snakeHead->coor_x++;
	}
	SNAKE *current = snakeHead;
	int LEN = len;
	for (int i = 1; i < len; i ++) {
		current = snakeHead;
		for (int j = 1; j < LEN - 1 && len >= 3; j ++) {
			//令current循環到指定位置
			current = current->link;
		}
		current->link->coor_x = current->coor_x;
		current->link->coor_y = current->coor_y;
		LEN--;
	}
}

這是一個相當重要的功能,因爲只有可以動起來纔有遊戲的感覺。就是上面這個簡單的函數實現了蛇的移動,他在主函數中是循環調用的,所以他的核心功能就是改變蛇頭節點的座標,讓蛇頭節點可以沿着當前的運動方向一直移動下去。你也許會問,那蛇的身子是怎麼跟着蛇頭運動的呢?那就是函數中最後的一個雙重循環,內層循環會通過一個指針沿着蛇身鏈表找到蛇的尾巴的前一個節點,然後把此節點內的座標值給尾巴節點,這樣就實現了尾巴‘跟着動’的效果。第二次進入後內層循環會找到蛇尾巴前一個節點的前一個節點,然後把他的座標給了尾巴的前一個節點,這樣倒數第二個節點也跟上了!之後便一直重複上述循環,其實就是在用每個節點的座標來刷新其後一個節點的座標,這樣不就讓每一段蛇身都與蛇頭形影不離了嗎!如果還是感覺理解上有困難,可以看看下面的模擬圖:
這裏寫圖片描述
這裏應該十分注意賦值的順序!,我爲何要‘多此一舉’地用循環先找到倒數第二個節點,而不是直接從頭部開始,因爲那樣會讓蛇的座標提前丟失,導致我們沒法把真正有效的座標值更新到對應的節點中,從而只看到蛇頭在移動!不信?你可以在本子上比劃比劃。
4》蛇的死亡碰撞事件檢測!

bool SnakeSprite::IsDeath() {
	//判斷是否超出規定範圍 67<x<468/87<y<470
	if (snakeHead->coor_x > 67 && snakeHead->coor_x < 460
		&& snakeHead->coor_y > 87 && snakeHead->coor_y < 470) {
		return false;
	}
	else {
		return true;
	}
}

嗯,這取決於你在屏幕上蛇的移動場地面積的尺寸,當檢測的某個方向的座標超過了對應方向上的場地長度,那就GAMEOVER吧!

食物類的實現代碼:

//FOOD CLASS
class Food {
protected:
	int coor_x;				//食物出現的橫座標
	int coor_y;				//縱座標
public:
	Food(int x = 100, int y = 100);
	bool drawThisFood(bool &again);				//繪製當前食物
	bool checkFoodPosition();					//檢查當前食物出現的位置是否合法(即不能與蛇體重合)
	void getRandCoor(int & x, int & y);			//食物的隨機座標生成
};

//APPLE CLASS
class Apple : public Food {	//蘋果是Food的一種
private:
	int color;				//擴展功能,標定當前Apple的顏色
public:
	bool beenCollision(RECT snakeRect); //檢測apple是否被碰撞到,是返回true否則返回false
	void getCurrentPosRect(RECT &rect);			//得到當前的位置矩形
	//bool beenCollision2(int x, int y);
};

Food::Food(int x, int y) : coor_x(x), coor_y(y)
{}

void Apple::getCurrentPosRect(RECT &rect) {
	RECT currentRect = {coor_x, coor_y, coor_x + 19, coor_y + 22};
	rect = currentRect;
}

bool Apple::beenCollision(RECT snakeRect) {
	RECT rect_apple, rect;
	getCurrentPosRect(rect_apple);
	if (IntersectRect(&rect, &rect_apple, &snakeRect)) {
		return true;
	}
	else {
		return false;
	}
}

bool Food::drawThisFood(bool &again) {
	//繪製當前食物到屏幕
	int posX , posY ;
	if (again) {
		getRandCoor(posX, posY);
		again = false;
	}
	
	RECT rectApple = { 0, 0, 19, 22 };
	D3DXVECTOR3 position(coor_x, coor_y, 0);
	D3DCOLOR red = D3DCOLOR_XRGB(255, 255, 255);
	spriteoj->Draw(apple, &rectApple, NULL, &position, red);

	return true;
}

void Food::getRandCoor(int & x, int & y) {
	//指定範圍內的隨機函數生成器
	srand((unsigned)time(NULL));		//隨機種子,以系統時間作爲基數
	x = foodAllowPosX + (rand() % 350);
	y = foodAllowPosY + (rand() % 330);
	coor_x = x;
	coor_y = y;
}

bool Food::checkFoodPosition() {
	//檢查食物位置的合法性
	return true;
}

這裏我用FOOD作爲基類,然後用APPLE來繼承它,這主要是想在以後擴展這個遊戲的時候加入一些新的玩法,讓每種不同的食物都有自己各自屬性和反應事件。
5》食物的隨機位置產生

void Food::getRandCoor(int & x, int & y) {
	//指定範圍內的隨機函數生成器
	srand((unsigned)time(NULL));		//隨機種子,以系統時間作爲基數
	x = foodAllowPosX + (rand() % 350);
	y = foodAllowPosY + (rand() % 330);
	coor_x = x;
	coor_y = y;
}

這個成員函數用了隨機數生成器來產生指定區間內的座標,並把這個座標當做食物出現的座標。因爲屏幕是在無休止刷新的,所以食物的擦除就不勞我們費心了。
6》其他
我在這裏只是調了一些關鍵的地方作了闡述。其他還有瑣碎的地方都需要一塊塊完善,但是都相對簡單。至於將蛇繪製到屏幕上,這是件麻煩事!我不能展開講,我的水平也不敢講,但是這真的會令你沮喪,如果你只是看某些實現邏輯而其他的可以自己搞定,那麼上面的解釋還是挺有幫助的;如果你是個新手,那就會覺得知道邏輯和流程卻無法把他們繪製到屏幕上,似乎是本我狠狠的放了鴿子。
也許不必那麼沮喪!因爲我講了你也未必能懂(哈哈),你有其他途徑可以實現自己的貪喫蛇遊戲:
(1)實現純語言版本的,沒錯,就是在那個黑黑的Dos框裏的,因爲他的實現相對簡單,關鍵是他避免了圖形渲染和Windows的窗口創建!這真的是一個不錯的入門Demo。你還可以在網上找一些關於他的代碼來提高自己的開發效率,如果你仍然感覺邏輯上有困難,那麼我也會盡快整理出他的寫作思路~
(2)學習一些圖形渲染的庫和工具(如DirectX),抑或是一些簡單的遊戲引擎,如果那樣的話,你真的會瞧不起我做的這個貪喫蛇。再不就看看我提供的兩套源碼吧!

4,不足之處
上面的代碼儘管解決了一些核心的遊戲邏輯,但是依然存在不足。計分系統由於食物出現的位置不當而暴增、食物萬一出現在蛇身上怎麼辦?而我在食物位置的合法性檢查上只是返回了個字面量true!希望我們一塊交流和改進。

5,資源鏈接
說明:你休想直接複製粘貼上面的代碼塊來放在編譯器裏運行它,並且天真的等待遊戲畫面的出現,因爲我早說過這些需要相應工具的支持!你甚至不能運行起版本2的EXE程序,因爲可能需要DirectX的遊戲環境,而恰好你的機器上沒有!但是版本1的貪喫蛇是可以運行起來的,源碼也是可以編譯的(如果你的編譯器正常的話!),因爲他真的是用純語言做的,當然性能也會有不足。
貪喫蛇版本一資源鏈接:http://pan.baidu.com/s/1c2EUVc8 密碼:na6m
貪喫蛇版本二資源鏈接:http://pan.baidu.com/s/1hsb7k92 密碼:6sbg

想一起解決編程中遇到的麻煩嗎?想一起學習獨立遊戲開發嗎?想找一羣志同道合的朋友嗎?想找到自己關於計算機真正的興趣所在嗎?那就加入我們吧!(老學長公衆號剛剛開通不久,每隔3天會發表一篇有質量的文章,希望大家多支持!)
公衆號:奇妙的coco

6,版權聲明
遊戲中的音樂、圖片、圖標等資源均來源於網絡,僅供學習之用。
借鑑數目:《遊戲編程入門》、《Windows遊戲編程大師技巧》

最後謝謝大家可以看我分享的一些經驗,這些都是在項目過程中遇到的麻煩,希望大家可以收穫到一些知識,少走點彎路!

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