c/c++游戏编程之控制台贪吃蛇(一)
c/c++游戏编程之控制台贪吃蛇(二)
为了解决“闪屏”问题,我们不再使用system(“cls”)进行清屏,而是直接用空格符’
'清掉蛇尾即可,这就像裁剪(只对需要改变的区域进行更新,不对固定的地方进行多余的操作)。
总的来说:游戏画面的每一帧变化只有蛇头和蛇尾的位置变了,我们只需要在新蛇头的位置填上’O’,用空格符’ '覆盖旧蛇尾,而不需要每次都将整条蛇重新打印一遍。
所以,我们只要在游戏刚开始时将整条蛇打印一次就行了:
void Init() {
speedx = 1; //游戏开始时蛇默认向右移动
speedy = 0;
pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间
pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
PrintSnake();
}
在RefreshSnakeHead()函数打印新蛇头:
void RefreshSnakeHead(int posx, int posy) {
SnakeNode* pNewNode = new SnakeNode{ posx, posy, pSnakeHead, nullptr }; //在新位置形成新蛇头, 新蛇头next指针指向旧蛇头
pSnakeHead->pLastNode = pNewNode; //旧蛇头last指针指向新蛇头
pSnakeHead = pNewNode; //更新蛇头指针
//打印新蛇头
GotoPos(pSnakeHead->posx, pSnakeHead->posy);
cout << 'O';
}
在RefreshSnakeTail()函数用空格符’ '去掉蛇尾:
void RefreshSnakeTail() {
//去掉蛇尾
GotoPos(pSnakeTail->posx, pSnakeTail->posy);
cout << ' ';
SnakeNode* pTemp = pSnakeTail;
pSnakeTail = pSnakeTail->pLastNode;
pSnakeTail->pNextNode = nullptr;
delete pTemp; //释放节点空间
pTemp = nullptr; //为释放后的指针置空
}
main()函数不再调用PrintSnake()和system(“cls”):
int main() {
Init();
while (1) {
Move();
Sleep(100);
}
return 0;
}
到现在为止,游戏画面只有蛇哥自己,怎么能让它这么孤单?
我们先添加两个全局常量:MAP_WIDTH和MAP_HEIGHT(地图宽度和地图高度):
const int MAP_WIDTH = 100; //地图宽度
const int MAP_HEIGHT = 50; //地图高度
再定义一个名为PrintMap的函数为游戏画面添加地图框,我们用‘+’代表地图边界:
void PrintMap() {
string tempStr = "+" + string(' ', MAP_WIDTH - 2) + "+\n";
GotoPos(0, 1);
for (int index = 2; index < MAP_HEIGHT; ++index) {
cout << tempStr;
}
tempStr = string('+', MAP_WIDTH);
cout << tempStr;
GotoPos(0, 0);
cout << tempStr;
}
在Init()函数里调用PrintMap:
void Init() {
speedx = 1; //游戏开始时蛇默认向右移动
speedy = 0;
pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间
pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
PrintSnake();
PrintMap();
}
运行效果截图:
现在定义一个名为Crash函数用来判定蛇头有没有跟墙壁相撞,如果撞上,则向主调函数返回true,并打印“Game Over”,休眠2秒钟后退出程序:
bool Crash() {
//如果蛇头撞上地图边界,则返回true
if (pSnakeHead->posx >= MAP_WIDTH || pSnakeHead->posx <= 0 ||
pSnakeHead->posy <= 0 || pSnakeHead->posy >= MAP_HEIGHT) {
return true;
}
return false;
}
我们再定义一个释放链表空间的函数FreeList(),以在游戏结束时调用:
void FreeList() {
SnakeNode* pTemp;
while (pSnakeHead != NULL) {
pTemp = pSnakeHead;
pSnakeHead = pSnakeHead->pNextNode;
delete pTemp;
pTemp = nullptr;
}
}
改写main()函数:
int main() {
Init();
while (1) {
Move();
if (Crash()) {
GotoPos(MAP_WIDTH / 2, MAP_HEIGHT / 2); //在地图中央打印"Game Over"
cout << "Game Over";
FreeList();
Sleep(2000);
break;
}
Sleep(100);
}
return 0;
}
好了,现在该到我们的重磅嘉宾——食物出场了,先定义一个Food结构体和全局变量food:
struct Food {
int posx; //食物x坐标
int posy; //食物y坐标
char symbol; //食物符号
} food;
在Init()函数里初始化食物状态,用字符’F’代表食物:
void Init() {
srand(unsigned(time(NULL))); //初始化随机数种子
speedx = 1; //游戏开始时蛇默认向右移动
speedy = 0;
//在地图边界内随机生成食物
food.posx = 1 + rand() % (MAP_WIDTH - 1);
food.posy = 1 + rand() % (MAP_HEIGHT - 1);
food.symbol = 'F';
pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间
pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
PrintSnake();
PrintMap();
//打印食物
GotoPos(food.posx, food.posy);
cout << food.symbol;
}
定义一个判定食物是否被吃的函数EatFood(),如果被吃就刷新食物的位置:
(tips: 这里以及Init()函数里面都没有处理食物刷到蛇身上的情况,这里就交给你自己去解决啦,很简单的问题要相信自己(其实是作者自己懒罢了- _ -))
bool EatFood() {
if (pSnakeHead->posx == food.posx && pSnakeHead->posy == food.posy) {
food.posx = 1 + rand() % (MAP_WIDTH - 1);
food.posy = 1 + rand() % (MAP_HEIGHT - 1);
GotoPos(food.posx, food.posy);
cout << food.symbol;
return true;
}
return false;
}
改写Move()函数,如果吃到了食物,蛇就变长了,也就是说不删除蛇尾:
void Move() {
char cinput;
if (_kbhit()) {
cinput = _getch(); //使用getch()需要 #include <conio.h>
switch (cinput) {
case 87: case 119: {
speedx = 0;
speedy = -1;
break;
}
case 53: case 115: {
speedx = 0;
speedy = 1;
break;
}
case 65: case 97: {
speedx = -1;
speedy = 0;
break;
}
case 68: case 100: {
speedx = 1;
speedy = 0;
break;
}
default: {
break;
}
}
}
RefreshSnakeHead(pSnakeHead->posx + speedx, pSnakeHead->posy + speedy);
if (!EatFood()) {
RefreshSnakeTail();
}
}
所有代码:
//控制台贪吃蛇
#include <iostream>
#include <Windows.h>
#include <conio.h>
#include <ctime>
#include <cstdlib>
#include <string>
using std::cout;
using std::string;
struct SnakeNode {
int posx;
int posy;
SnakeNode* pNextNode;
SnakeNode* pLastNode;
} *pSnakeHead, *pSnakeTail;
struct Food {
int posx;
int posy;
char symbol;
} food;
int speedx; //x轴速度
int speedy; //y轴速度
const int MAP_WIDTH = 100; //地图宽度
const int MAP_HEIGHT = 50; //地图高度
//设置光标位置
void GotoPos(int x, int y);
//打印蛇体
void PrintSnake();
//打印地图
void PrintMap();
//碰撞检测
bool Crash();
//判定食物是否被吃
bool EatFood();
//初始化游戏
void Init();
//释放链表
void FreeList();
//刷新蛇头节点
void RefreshSnakeHead(int posx, int posy);
//刷新蛇尾节点
void RefreshSnakeTail();
//移动控制
void Move();
int main() {
Init();
while (1) {
Move();
if (Crash()) {
GotoPos(MAP_WIDTH / 2, MAP_HEIGHT / 2); //在地图中央打印"Game Over"
cout << "Game Over";
FreeList();
Sleep(2000);
break;
}
Sleep(100);
}
return 0;
}
void GotoPos(int x, int y) {
HANDLE hout; //定义句柄
COORD cor; //定义坐标
hout = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出句柄
cor.X = x;
cor.Y = y;
SetConsoleCursorPosition(hout, cor); //设置光标位置
}
void Init() {
srand(unsigned(time(NULL))); //初始化随机数种子
speedx = 1; //游戏开始时蛇默认向右移动
speedy = 0;
//在地图边界内随机生成食物
food.posx = 1 + rand() % (MAP_WIDTH - 1);
food.posy = 1 + rand() % (MAP_HEIGHT - 1);
food.symbol = 'F';
pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间
pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
PrintSnake();
PrintMap();
//打印食物
GotoPos(food.posx, food.posy);
cout << food.symbol;
}
void PrintSnake() {
SnakeNode* pTemp = pSnakeHead;
while (pTemp != nullptr) {
GotoPos(pTemp->posx, pTemp->posy);
cout << 'O';
pTemp = pTemp->pNextNode;
}
}
void RefreshSnakeHead(int posx, int posy) {
SnakeNode* pNewNode = new SnakeNode{ posx, posy, pSnakeHead, nullptr }; //在新位置形成新蛇头, 新蛇头next指针指向旧蛇头
pSnakeHead->pLastNode = pNewNode; //旧蛇头last指针指向新蛇头
pSnakeHead = pNewNode; //更新蛇头指针
//打印新蛇头
GotoPos(pSnakeHead->posx, pSnakeHead->posy);
cout << 'O';
}
void RefreshSnakeTail() {
//去掉蛇尾
GotoPos(pSnakeTail->posx, pSnakeTail->posy);
cout << ' ';
SnakeNode* pTemp = pSnakeTail;
pSnakeTail = pSnakeTail->pLastNode;
pSnakeTail->pNextNode = nullptr;
delete pTemp; //释放节点空间
pTemp = nullptr; //为释放后的指针置空
}
void Move() {
char cinput;
if (_kbhit()) {
cinput = _getch(); //使用getch()需要 #include <conio.h>
switch (cinput) {
case 87: case 119: {
speedx = 0;
speedy = -1;
break;
}
case 53: case 115: {
speedx = 0;
speedy = 1;
break;
}
case 65: case 97: {
speedx = -1;
speedy = 0;
break;
}
case 68: case 100: {
speedx = 1;
speedy = 0;
break;
}
default: {
break;
}
}
}
RefreshSnakeHead(pSnakeHead->posx + speedx, pSnakeHead->posy + speedy);
if (!EatFood()) {
RefreshSnakeTail();
}
}
void PrintMap() {
string tempStr = "+" + string(MAP_WIDTH - 2, ' ') + "+\n";
GotoPos(0, 1);
for (int index = 2; index < MAP_HEIGHT; ++index) {
cout << tempStr;
}
tempStr = string(MAP_WIDTH, '+');
cout << tempStr;
GotoPos(0, 0);
cout << tempStr;
}
bool Crash() {
//如果蛇头撞上地图边界,则返回true
if (pSnakeHead->posx >= MAP_WIDTH || pSnakeHead->posx <= 0 ||
pSnakeHead->posy <= 0 || pSnakeHead->posy >= MAP_HEIGHT) {
return true;
}
return false;
}
void FreeList() {
SnakeNode* pTemp;
while (pSnakeHead != nullptr) {
pTemp = pSnakeHead;
pSnakeHead = pSnakeHead->pNextNode;
delete pTemp;
pTemp = nullptr;
}
}
bool EatFood() {
if (pSnakeHead->posx == food.posx && pSnakeHead->posy == food.posy) {
food.posx = 1 + rand() % (MAP_WIDTH - 1);
food.posy = 1 + rand() % (MAP_HEIGHT - 1);
GotoPos(food.posx, food.posy);
cout << food.symbol;
return true;
}
return false;
}
运行效果截图:
好了,截至目前,我们基本上已经完成了所有主要功能的开发,还有一些功能如显示分数、改变速度、障碍物、镜像穿墙等需要你自己动用聪明的大脑去扩展。
文章持续更新中!
求点赞、收藏!
作者水平有限,如果有误,欢迎指正!
编译环境:Visual Studio 2019