面向對象方法編一個簡易的控制檯版貪吃蛇(三)

之前關於貪吃蛇的內容參見:面向對象方法編一個簡易的控制檯版貪吃蛇(一)點擊打開鏈接    和     面向對象方法編一個簡易的控制檯版貪吃蛇(二)點擊打開鏈接

現在,我們開始寫碰撞。

一說起增加什麼功能,我首先想到的還是寫一個新的方法。但是這個方法寫在哪個類裏面似乎永遠都是一門學問,你可以寫在一個現有的類裏面,也可以再創建一個類。即使對於這樣一款小遊戲也需要考慮,因爲如果寫的不科學的話,可能後續實現起來就會顯得很困難。

一開始,我採取的辦法是是額外創建一個類,取名叫GameFunction,專門用來實現遊戲當中的功能。但是,這樣做的話。我想要調用GameFunction裏的函數的話,就不能用現有主函數裏的各個來調用GameFunction裏面的方法了(也調用不到)。那怎麼辦,我就想把主函數改一下,直接new 一個GameFunction類的對象,同時在GameFunction的構造函數內部將地圖、蛇和食物初始化。但是這樣的話,剛剛創建的GameFunction對象又無法訪Map、Food和Snake裏各自的show函數,這樣就無法將東西顯示在控制檯上。又該怎麼辦?個人能力有限,這個問題我沒能解決,得想其他的辦法,如果有哪位朋友能夠給我提個意見,我將不勝感激。

那就只能在現有的類裏面添加這些方法了。

我又選取了一個方法,那就是將用來判斷是否碰撞的方法(isCollide)分別寫在Map類和Food類裏面。之所以分開寫的原因,是因爲當蛇碰到Map和Food時的操作不盡相同,分開寫便於寫主函數。

Map.cpp中的isCollide方法,其實也不難,就是把所有map結點都遍歷一遍,看有沒有結點和它重合。

bool Map::isCollide(BaseNode* node)
{
	for (auto Map : m_map)
	{
		if (Map->x == node->x&&Map->y == node->y)
		{
			return true;
		}
	}
	return false;
}
同理,Food.cpp當中的isCollide方法:

bool Food::isCollide(BaseNode* node)
{
	for (auto Food : m_food)
	{
		if (Food->x == node->x&&Food->y == node->y)
		{
			return true;
		}
	}
	return false;
}

現在需要處理的是當蛇碰撞到不同的東西時所產生的不同效果。當蛇碰到地圖Map的時候,蛇就死了,這個比較好處理,只需當碰上之後,直接break掉跳出主函數中的while循環就行了;當蛇碰到食物的時候,蛇身子增加,同時當前食物消失並隨機生成另外一個食物。

先考慮蛇身子增加的問題。我們可以在Snake類裏面增加一個函數getLonger專門用來實現蛇身子增加的功能。

void Snake::getLonger()  //在蛇的尾部增加一個結點
{
	m_snake.push_back(new BaseNode(m_snake.back()->x, m_snake.back()->y, TYPE_SNAKE));
}

現在我們對主函數進行一下修改,然後測試一下,看看碰撞是否管用。寫主函數的時候我們又發現一個問題:isCollide函數的參數是BaseNode*類型,主函數中的類型沒有BaseNode*類型,因此傳參的時候存在類型不匹配的問題。我們這樣解決這個問題:在Snake類裏面定義一個getSnake函數,專門用來獲取蛇的結點。

vector<BaseNode*>*  getSnake() { return &m_snake;}

等我們在主函數調用isCollide函數的時候,我們就可以這麼寫:

1.當判斷地圖和蛇碰撞時:

if( map->isCollide(snake->getSnake()->front())

{

……

}

碰撞的時候,只需判斷蛇頭是否碰上就可以了(因爲按照正常的玩法,蛇的其它部位也不可能碰到)

2.同理當判斷食物和蛇是否碰撞時:

if(food->isCollide(snake->getSnake()->front()))

{

……

}

這樣,主函數就被修改成了這樣:

#include<iostream>
#include"Controller.h"
#include"baseNode.h"
#include"Map.h"
#include"Snake.h"
#include"Food.h"
#include<Windows.h>

void main()
{
	
	Snake* snake = new Snake(5, 6);
	Food* food = new Food(10, 9);
	Map* map = new Map();

	snake->showSnake();
	snake->setKey(VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT);

	food->showFood();
	map->showMap();
	while (true)
	{
		Sleep(150);
		snake->controlSnake();

		if (map->isCollide(snake->getSnake()->front()))
		{
			break;
		}
		if(food->isCollide(snake->getSnake()->front()))
		{
			snake->getLonger();
		}
		snake->showSnake();
	}
	cout << "Game Over!";
	system("pause");
}

測試結果如下:

1.當撞地圖的時候:


OK,運行正常。

2.當蛇裝上食物的時候:


老天,蛇怎麼又越走越長了?我用m_snake.size()方法判斷了一下蛇的長度是否變長了,發現吃到食物之後,蛇的長度一直都是2。也就是說,跟之前的一樣,渲染出問題了,即:顯示的問題。我們之前已經在Controller.h裏面已經有了一個用於擦除結點的方法clearNode,現在又該派上用場了。在主函數中,每一次while循環我們就會調用controlSnake方法,因此,我們檢查controlSnake方法裏面有什麼異常。最後發現:用來擦除蛇身結點的程序的位置寫的並不準確。原來我們的controlSnake是這麼寫的:

void Snake::controlSnake() 
{
	int stateU = (GetAsyncKeyState(keyUp) & 0x8000);
	int stateD = (GetAsyncKeyState(keyDown) & 0x8000);
	int stateL = (GetAsyncKeyState(keyLeft) & 0x8000);
	int stateR = (GetAsyncKeyState(keyRight) & 0x8000);
	//當按下指定按鍵的時候,我們就把char c設置成如下變量。
	if (stateU != 0)
	{
		c = 'u';
	}
	if (stateD != 0)
	{
		c = 'd';
	}
	if (stateL != 0)
	{
		c = 'l';
	}
	if (stateR != 0)
	{
		c = 'r';
	}
	if (c == 'u' || c == 'd' || c == 'l' || c == 'r') //如果捕獲了按鍵
	{
		for (int i = m_snake.size() - 1; i > 0; i--)
		{
			m_snake.at(i)->x = m_snake.at(i - 1)->x;
			m_snake.at(i)->y = m_snake.at(i - 1)->y;
		}
		for (auto node : m_snake)  
		{
			Controller::clearNode(node);
		}
		switch (c) //對蛇的頭部進行操作
		{
		case 'u'://捕獲到向上鍵的話,蛇頭y值減去1,以達到向上走的目的
			m_snake.front()->y--;
			break;
		case 'd':
			m_snake.front()->y++;
			break;
		case 'l':
			m_snake.front()->x--;
			break;
		case 'r':
			m_snake.front()->x++;
			break;
		}
	}
}

我們發現,在原來的語句中,我們只是在接收到鍵盤消息的時候,纔會進行擦除操作。最開始的時候蛇只有一個結點,使得以下這段代碼一直都沒有執行:

for (int i = m_snake.size() - 1; i > 0; i--)

{

m_snake.at(i)->x = m_snake.at(i - 1)->x;

m_snake.at(i)->y = m_snake.at(i - 1)->y;

}

因此每次移動的時候,以上這段代碼之後的擦除操作就會直接執行進而把上一次所顯示蛇尾直接擦除掉,所以運行的時候沒有問題。但是當吃到食物,蛇身子變爲兩個結點的時候,上面的代碼開始執行了,每個結點都往移動的方向進了一段,此時,我再進行擦除操作,擦除的已經是移動完了的蛇。而此時的蛇尾巴已經往前挪動了一步,使得你此時擦除的時候正好把上一次循環時的蛇尾巴給錯過了。於是蛇就又越走越長了。只需把擦除操作挪到移動操作之前就可以了。即:先擦除、後移動。這樣一來,controlSnake方法就被修改成了這樣:

void Snake::controlSnake() 
{
	int stateU = (GetAsyncKeyState(keyUp) & 0x8000);
	int stateD = (GetAsyncKeyState(keyDown) & 0x8000);
	int stateL = (GetAsyncKeyState(keyLeft) & 0x8000);
	int stateR = (GetAsyncKeyState(keyRight) & 0x8000);
	//當按下指定按鍵的時候,我們就把char c設置成如下變量。
	if (stateU != 0)
	{
		c = 'u';
	}
	if (stateD != 0)
	{
		c = 'd';
	}
	if (stateL != 0)
	{
		c = 'l';
	}
	if (stateR != 0)
	{
		c = 'r';
	}


	if (c == 'u' || c == 'd' || c == 'l' || c == 'r') //如果捕獲了按鍵
	{
		for (auto node : m_snake)  
		{
			Controller::clearNode(node);
		}
		for (int i = m_snake.size() - 1; i > 0; i--)
		{
			m_snake.at(i)->x = m_snake.at(i - 1)->x;
			m_snake.at(i)->y = m_snake.at(i - 1)->y;
		}
		
		switch (c) //對蛇的頭部進行操作
		{
		case 'u'://捕獲到向上鍵的話,蛇頭y值減去1,以達到向上走的目的
			m_snake.front()->y--;
			break;
		case 'd':
			m_snake.front()->y++;
			break;
		case 'l':
			m_snake.front()->x--;
			break;
		case 'r':
			m_snake.front()->x++;
			break;
		}
	}
}

測試結果:



OK,運行正常!

此時請注意一點:雖然此時蛇已經吃掉了食物,但是食物的結點並沒有從vector當中消失,只是由於顯示的原因,蛇的圖案覆蓋掉了食物的圖案。爲解決這個問題,我在Food.h文件裏面定義一個clearFood方法。用來實現這個功能。

void Food::clearFood()
{
	delete m_food.back();
}

在主函數中我們修改一下while循環:

while (true)
{
	Sleep(100);
	snake->controlSnake();
	if (map->isCollide(snake->getSnake()->front()))
	{
		break;
	}

	if(food->isCollide(snake->getSnake()->front()))
	{
		food->clearFood();
		snake->getLonger();
	}

	//food->showFood();
	//map->showMap();
	snake->showSnake();
}

到現在爲止,碰撞還剩一個問題:當蛇與自己碰撞的時候也是會死的。仿照在Food和Map類裏面的isCollide方法,我們再Snake類裏面也定義一個碰撞方法,依然取名isCollide。這個函數我們這樣寫:

bool Snake::isCollide()
{
	for (int i = 1; i < m_snake.size(); i++)
	{
		if (m_snake.at(i)->x == m_snake.front()->x&&m_snake.at(i)->y == m_snake.front()->y)
		{
			return true;
		}
	}
	return false;
}

主函數中,我們在while循環中添加這樣幾行代碼:

if (snake->isCollide())
{
	break;
}

現在我們測試一下,但是蛇的身長只有一個,我們可以先在蛇的初始化程序中(即:Snake的構造函數當中)新加幾個結點,然後進行測試。我們可以對Snake的構造函數進行修改,其實就是讓初始化時的多幾個結點:

Snake::Snake(int x, int y)
{
	m_snake.push_back(new BaseNode(x, y, TYPE_SNAKE));//創建一個蛇,就是將相應的結點壓入vector,地圖和食物同理。
	m_snake.push_back(new BaseNode(x - 1, y, TYPE_SNAKE));
	m_snake.push_back(new BaseNode(x - 2, y, TYPE_SNAKE));
	m_snake.push_back(new BaseNode(x - 3, y, TYPE_SNAKE));
}

點擊運行一下,發現運行成功。

 

現在,我們我們還剩下這樣幾個問題沒有解決:

1.在蛇吃掉食物的時候,在蛇身加長的同時,還要隨機生成另外一個食物,隨機生成食物的功能沒有完成。

2.在貪吃蛇的傳統玩法中,吃的食物越多,蛇身越來越長,同時蛇的移動速度也在不斷加快。如何實現蛇移動速度不斷加快的功能?

3.在目前的程序當中,我們可以通過主函數當中while循環中的sleep(num)函數來改變蛇的運行速度,但是num的數值太小,速度實在是太快,而num數值太大的時候,按鍵將會很不靈敏。如何解決這個問題?

4.地圖的問題:在第一關的時候,我們可以像現在這樣把地圖只設置成四面牆,但是地圖不總是這樣簡單,當你過關的時候,肯定就得額外換一個地圖。如何實現地圖的改變?

5.由問題4我們又可以想到另外一個問題:如何實現過關的功能。

 

以上這些問題,是我目前能夠想到的。具體實現,以後再與大家分享。現在,實現目前這些所有功能的代碼全部張貼出來:

1.baseNode.h

/*
	baseNode.h
*/

#pragma once

enum nodeType  //定義了結點的類型
{
	TYPE_SNAKE,   //蛇類
	TYPE_MAP,	  //地圖類
	TYPE_FOOD     //食物類
};

class BaseNode
{
public:
	BaseNode();
	BaseNode(int x, int y, nodeType type);
	~BaseNode();


	int x;
	int y;
	nodeType type;
};

baseNode.cpp

//baseNode.cpp
#include "baseNode.h"

BaseNode::BaseNode()
{
}

BaseNode::BaseNode(int x, int y, nodeType type)
{
	this->x = x;
	this->y = y;
	this->type = type;
}

BaseNode::~BaseNode()
{
}

2.Controller.h

/*
	Controller.h
	“掌管”控制檯上蛇、地圖等物體的顯示
*/

#pragma once
#include<vector>
#include"baseNode.h"
using namespace std;
class Controller
{
public:
	static void moveXY(int x, int y);  //移動光標的位置
	static void showBaseNode(vector<BaseNode*> v); //把地圖,蛇,食物什麼的在控制檯上顯示出來
	static void clearNode(BaseNode* node);   //擦除結點
};

Controller.cpp

//Controller.cpp

#include "Controller.h"
#include"baseNode.h"
#include<iostream>
#include<Windows.h>
#include<conio.h>
using namespace std;

/*
	moveXY這個函數通過SetConsoleCursorPosition這個函數可以定位到(x,y)在控制檯上的位置
	這樣,我在showBaseNode函數裏面調用這個函數,就可以在控制檯上相應的位置上輸出我想要的樣式。
*/

void Controller::moveXY(int x, int y)
{
	COORD coord;
	coord.X = x;
	coord.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}

void Controller::showBaseNode(vector<BaseNode*> v)
{
	for (int i = 0; i < v.size(); i++)
	{
		BaseNode* node = v.at(i);
		Controller::moveXY(node->x, node->y);  //moveXY方法的作用在這裏體現了出來,若去掉這個語句的話
		                                       //在控制檯上顯示的圖像是不管是啥都是按控制上的行依次輸出的。
		if (node->type == TYPE_SNAKE)
		{
			cout << "o";
		}
		else if (node->type == TYPE_MAP)
		{
			cout << "*";
		}
		else if (node->type == TYPE_FOOD)
		{
			cout << "#";
		}
	}
}

void Controller::clearNode(BaseNode* node)
{
	moveXY(node->x, node->y);
	cout << " ";
}

3.Food.h

//Food.h
#pragma once
#include"Controller.h"
#include"baseNode.h"
class Food
{
public:
	Food(int x,int y);
	~Food();

	bool isCollide(BaseNode* node);

	void clearFood();//蛇碰到食物之後,當前食物消失,當前方法用來讓當前食物結點消失。
	void showFood();
private:
	vector<BaseNode*> m_food;
};

Food.cpp

//Food.cpp
#include "Food.h"

Food::Food(int x,int y)
{
	m_food.push_back(new BaseNode(x, y, TYPE_FOOD));
}

Food::~Food()
{
	while (m_food.size())
	{
		delete m_food.back();
		m_food.pop_back();
	}
}

bool Food::isCollide(BaseNode* node)
{
	for (auto Food : m_food)
	{
		if (Food->x == node->x&&Food->y == node->y)
		{
			return true;
		}
	}
	return false;
}

void Food::clearFood()
{
	delete m_food.back();
}

void Food::showFood()
{
	Controller::showBaseNode(m_food);
}
4.Map.h

//Map.h
#pragma once
#include"baseNode.h"
#include"Controller.h"
using namespace std;
class Map
{
public:
	Map();
	~Map();

	bool isCollide(BaseNode* node);

	void showMap();

private:  //想一想:地圖上都顯示什麼?地圖本身、食物還有蛇。
	vector<BaseNode*> m_map;
	/*vector<BaseNode*> m_food;
	vector<BaseNode*> m_snake;*/
};

Map.cpp

//Map.cpp
#include "Map.h"

Map::Map()  //最早初始化地圖的時候,顯然是周圍的四面牆
{
	for (int i = 0; i < 60; i++)
	{
		m_map.push_back(new BaseNode(i, 0, TYPE_MAP));
		m_map.push_back(new BaseNode(i, 19, TYPE_MAP));
	}

	for (int i = 0; i < 20; i++)
	{
		m_map.push_back(new BaseNode(0, i, TYPE_MAP));
		m_map.push_back(new BaseNode(59, i, TYPE_MAP));
	}
}

Map::~Map()
{
	while (m_map.size())
	{
		delete m_map.back();
		m_map.pop_back();
	}
}

bool Map::isCollide(BaseNode* node)
{
	for (auto Map : m_map)
	{
		if (Map->x == node->x&&Map->y == node->y)
		{
			return true;
		}
	}
	return false;
}

void Map::showMap()
{
	Controller::showBaseNode(m_map);
}

5.Snake.h

//Snake.h

#pragma once
#include<vector>
#include"Controller.h"
#include"baseNode.h"
using namespace std;

class Snake
{
public:
	Snake(int x, int y);  //構造函數是用來初始化蛇的位置的
	~Snake();

	
	void showSnake();
	void setKey(int up,int down,int left,int right);
	void controlSnake();
	void getLonger();
	bool isCollide();

	vector<BaseNode*>*  getSnake() { return &m_snake;}
private:
	vector<BaseNode*> m_snake;

	char c;
	int keyUp;
	int keyDown;
	int keyLeft;
	int keyRight;
};

Snake.cpp

//Snake.cpp
#include "Snake.h"
#include"Controller.h"
#include<Windows.h>
#include<iostream>
using namespace std;

Snake::Snake(int x, int y)
{
	m_snake.push_back(new BaseNode(x, y, TYPE_SNAKE));//創建一個蛇,就是將相應的結點壓入vector,地圖和食物同理。
	m_snake.push_back(new BaseNode(x - 1, y, TYPE_SNAKE));
	m_snake.push_back(new BaseNode(x - 2, y, TYPE_SNAKE));
	m_snake.push_back(new BaseNode(x - 3, y, TYPE_SNAKE));
}

Snake::~Snake()
{
	while (m_snake.size())
	{
		delete m_snake.back();
		m_snake.pop_back();
	}
}

void Snake::showSnake()
{
	Controller::showBaseNode(m_snake);
}

void Snake::setKey(int up, int down, int left, int right)
{
	keyUp = up;
	keyDown = down;
	keyLeft = left;
	keyRight = right;
}

void Snake::controlSnake() 
{
	int stateU = (GetAsyncKeyState(keyUp) & 0x8000);
	int stateD = (GetAsyncKeyState(keyDown) & 0x8000);
	int stateL = (GetAsyncKeyState(keyLeft) & 0x8000);
	int stateR = (GetAsyncKeyState(keyRight) & 0x8000);
	//當按下指定按鍵的時候,我們就把char c設置成如下變量。
	if (stateU != 0)
	{
		c = 'u';
	}
	if (stateD != 0)
	{
		c = 'd';
	}
	if (stateL != 0)
	{
		c = 'l';
	}
	if (stateR != 0)
	{
		c = 'r';
	}


	if (c == 'u' || c == 'd' || c == 'l' || c == 'r') //如果捕獲了按鍵
	{
		for (auto node : m_snake)  //蛇的尾巴用空格覆蓋掉
		{
			Controller::clearNode(node);
		}
		for (int i = m_snake.size() - 1; i > 0; i--)//蛇除去頭部的結點依次向前覆蓋,以達到走動的效果
		{
			m_snake.at(i)->x = m_snake.at(i - 1)->x;
			m_snake.at(i)->y = m_snake.at(i - 1)->y;
		}
		
		switch (c) //對蛇的頭部進行操作
		{
		case 'u'://捕獲到向上鍵的話,蛇頭y值減去1,以達到向上走的目的
			m_snake.front()->y--;
			break;
		case 'd':
			m_snake.front()->y++;
			break;
		case 'l':
			m_snake.front()->x--;
			break;
		case 'r':
			m_snake.front()->x++;
			break;
		}
	}
}

bool Snake::isCollide()
{
	for (int i = 1; i < m_snake.size(); i++)
	{
		if (m_snake.at(i)->x == m_snake.front()->x&&m_snake.at(i)->y == m_snake.front()->y)
		{
			return true;
		}
	}
	return false;
}

void Snake::getLonger()  //在蛇的尾部增加一個結點
{
	m_snake.push_back(new BaseNode(m_snake.back()->x, m_snake.back()->y, TYPE_SNAKE));
}

6.Main.cpp

#include<iostream>
#include"Controller.h"
#include"baseNode.h"
#include"Map.h"
#include"Snake.h"
#include"Food.h"
#include<Windows.h>

void main()
{
	
	Snake* snake = new Snake(5, 6);
	Food* food = new Food(10, 9);
	Map* map = new Map();

	snake->showSnake();
	snake->setKey(VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT);

	food->showFood();
	map->showMap();
	while (true)
	{
		Sleep(150);
		snake->controlSnake();

		if (map->isCollide(snake->getSnake()->front()))
		{
			break;
		}
		if (snake->isCollide())
		{
			break;
		}
		if(food->isCollide(snake->getSnake()->front()))
		{
			food->clearFood();
			snake->getLonger();
		}

		//food->showFood();
		//map->showMap();
		snake->showSnake();
	}
	cout << "Game Over!";
	system("pause");
}






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