在《 面向對象方法編一個簡易的控制檯版貪喫蛇(一)》中,我們已經讓食物、地圖蛇等可以在控制檯上顯示出來。下面我們應該考慮的問題是:如何通過鍵盤操作讓蛇動起來。只有先把這個問題解決了,你在以後實現諸如喫到食物、碰到牆壁等功能的時候纔可以直接拿過來測試。
首先,需要明確的一個問題是。這個操控蛇的方法應該寫在哪裏?由於我們是對蛇進行操作,因此,我們可以將這個方法寫在蛇類(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);
}