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

在《 面向對象方法編一個簡易的控制檯版貪喫蛇(一)》中,我們已經讓食物、地圖蛇等可以在控制檯上顯示出來。下面我們應該考慮的問題是:如何通過鍵盤操作讓蛇動起來。只有先把這個問題解決了,你在以後實現諸如喫到食物、碰到牆壁等功能的時候纔可以直接拿過來測試。

首先,需要明確的一個問題是。這個操控蛇的方法應該寫在哪裏?由於我們是對蛇進行操作,因此,我們可以將這個方法寫在蛇類(Snake)裏面。那麼,如何捕獲按鍵?我們首先需要一個char類型的變量(char c)。專門用來捕獲你所按下的按鍵。

那麼,按什麼按鍵能夠實現蛇的上下左右移動呢?我們可以專門定義一個函數(setKey)用來設置你想要的按鍵,如果你想用WASD來控制的話,你就可以直接調用這個函數來設置。相應的,在蛇類裏面也要增加四個變量(keyUp, keyDown, keyLeft, keyRight),代表四個按鍵,便於在setKey函數裏面進行傳值操作。

setKey函數聲明:

void setKey(int up,int down,int left,int right);

setKey函數定義:

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

設置按鍵完成之後,如何捕獲按鍵並讓蛇在屏幕上動起來?我們需要在蛇類裏面專門定義一個函數來實現控制蛇的功能,這個函數我命名爲controlSnake;其對應的函數聲明和函數定義如下所示:

controlSnake函數聲明:

void 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;
		}
		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;
		}
	}
}

這些函數都寫完之後,我們開始對主函數進行修改,以檢驗我們剛纔所寫的東西是否奏效。這是就需要我們之前所說的loop循環了,因爲你沒有循環的話,程序只能運行一次就直接退出了,這個loop循環我們暫時這麼寫:

主函數:

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

#include<Windows.h>

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

	snake->showSnake();
	food->showFood();
	map->showMap();
	while (true)
	{
		Sleep(100);
		snake->setKey(VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT);//用鍵盤的上下左右鍵來控制蛇
		snake->controlSnake();

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

點擊運行之後,我點擊一下向右按鍵,卻是這樣的運行效果:



顯示起來真的好混亂。爲什麼是這樣?因爲我一旦按了一個按鍵之後就不停地執行while循環,於是就會從左到右的把蛇畫滿整個控制檯。而且,我們在對蛇進行操作的時候除了將前一個結點的值覆蓋以外,還應該擦除蛇屁股的那個結點,如何將這些問題解決呢?

對於顯示過於混亂的問題,我們可以在while循環裏面讓程序“歇一會兒”,只需增加一個語句:Sleep(num)即可,num自己指定。我將num設置爲100試一下,運行程序,同樣,我點擊右鍵。運行結果:


這下,沒有一開始那麼混亂了。但是還有一個問題,那就是:蛇越來越長(我用這個特點,實力地玩了好長時間的畫地圖,哈哈~)。只有在蛇喫到東西的時候才能變長,一邊走一邊變長顯然不是我們想要的效果。那麼,如何解決?

首先,我們得知道爲什麼會造成這一情況,因爲我們最開始在對蛇身進行控制的時候,也就是這段語句(位於Snake.cpp文件中的controlSnake函數定義裏):

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;

}

對於這個問題我一開始認爲:在這個語句中我們只是將點的座標一次向前覆蓋,但是前一次的蛇的尾部結點卻還在vector裏面並沒有消失,所以再一次遍歷vector的時候,前一次的蛇的尾部依然會顯示出來。那麼,既然這樣,那我們只需把蛇尾部的那個結點給刪除掉就可以了,即:一個m_snake.pop_back()就可以了。但是,我沒意識到的一個問題是,儘管在屏幕上顯示了這麼多的結點,但並不代表vector裏面多出了結點。在實際操作的時候發現,由於在目前爲止,蛇的結點只有一個,因此,無論我在哪兒寫這個函數,只要pop掉,蛇的結點就會一個不剩。於是,程序就會出現異常。因此,造成這個情況的原因並不是我一開始想的那樣。

真正的原因是:由於我在while循環裏面並沒有對控制檯進行刷新操作,因此,上一個while循環留下的痕跡,在本次while循環中依然會顯示。所以,這只是單純的顯示問題。那麼,我每一次循環就用語句system(“cls”)語句刷新一下如何?我用了,蛇倒是不會越來越長了,但是整個控制檯卻閃的非常厲害,看着就頭暈。

那麼,有沒有其他辦法?打一個空格鍵過去不就可以不顯示了了嘛,所以,我們的思路是:在繪製蛇之前,先把蛇的身子全都用空格擦除掉,然後再繪製新的結點。具體做法如下:

由於controller類是專門用來調控整個控制檯的顯示。因此我們可以在controller.h文件裏面聲明一個clearNode函數,其聲明和定義如下(分別位於controller.h和controller.cpp內):

clearNode函數聲明:

static void clearNode(BaseNode* node);

clearNode函數定義:

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

寫好了之後,我們在snake類裏的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;
		}
	}
}

在運行一次,發現,蛇可以自由的移動了,而且沒有自己越走越長。

下面,將目前爲止整個貪喫蛇的完整代碼張貼出來。至於碰撞、喫食物等功能以後再說。

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.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();
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>

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

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 (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;
		}
	}
}

4.Food.h

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

	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();
	}
}

void Food::showFood()
{
	Controller::showBaseNode(m_food);
}


5.Map.h

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

	void showMap();

private:  
	vector<BaseNode*> m_map;

};

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();
	}
}

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


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