在《 面向对象方法编一个简易的控制台版贪吃蛇(一)》中,我们已经让食物、地图蛇等可以在控制台上显示出来。下面我们应该考虑的问题是:如何通过键盘操作让蛇动起来。只有先把这个问题解决了,你在以后实现诸如吃到食物、碰到墙壁等功能的时候才可以直接拿过来测试。
首先,需要明确的一个问题是。这个操控蛇的方法应该写在哪里?由于我们是对蛇进行操作,因此,我们可以将这个方法写在蛇类(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);
}