c/c++游戏编程之控制台贪吃蛇(二)

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_WIDTHMAP_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

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值