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

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

欢迎你开启了c++的游戏编程世界之旅

如果你还未学过c++基本语法,请先学习基本语法再来学习游戏编程噢~。

对这样的“黑框框”是不是很熟悉:

c/c++游戏编程之控制台贪吃蛇(一)_第1张图片

这就是控制台程序,死板且黑乎乎的控制台也能干一些有趣的事情,没错,我们将在控制台中完成第一个游戏作品——贪吃蛇的开发。

控制台是在光标(一闪一闪的那个)的位置默认从左到右,从上到下的顺序打印字符的,如果想要在控制台中任意位置打印字符,那么我们就需要定义一个名为GotoPos的函数,看不懂这个函数的内部实现?没关系,到后面学习win32编程的时候你会明白的。我们将使用这个函数控制字符在控制台中打印的位置:

//控制台贪吃蛇
#include 
#include 

using std::cout;

void GotoPos(int x, int y) {
	HANDLE hout; //定义句柄
	COORD cor; //定义坐标
	hout = GetStdHandle(STD_OUTPUT_HANDLE); //获取标准输出句柄
	cor.X = x; 
	cor.Y = y;
	SetConsoleCursorPosition(hout, cor); //设置光标位置
}

int main() {
	GotoPos(2, 2);
	cout << "hello world";
    return 0;
}

控制台坐标系的x轴方向是从左到右,y轴方向是从上到下,可见区域最左上角的点坐标就是(0, 0),所以GotoPos(2, 2),就代表向右边两个字符间距向下2个字符间距

代码运行效果如图:

c/c++游戏编程之控制台贪吃蛇(一)_第2张图片

我们先定义一个结构体SnakeNode,表示蛇的身体节点,每一个节点存储了坐标信息以及指向前后身体节点的指针:

struct SnakeNode {
	int posx;
	int posy;
	SnakeNode* pNextNode;
	SnakeNode* pLastNode;
} *pSnakeHead, *pSnakeTail;

再定义一个Init()函数,用来初始化游戏数据:

void Init() {
	speedx = 1; //游戏开始时蛇默认向右移动
	speedy = 0;
	pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间,初始坐标为(10, 10)
	pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
}

定义一个循环遍历链表的PrintSnake()打印函数,我们用大写字母’O’表示蛇的节点:

void PrintSnake() {
	SnakeNode* pTemp = pSnakeHead;
	while (pTemp != nullptr) {
		GotoPos(pTemp->posx, pTemp->posy);
		cout << 'O';
		pTemp = pTemp->pNextNode;
	}
}

所有代码如下:

//控制台贪吃蛇
#include 
#include 
#include 

using std::cout;

struct SnakeNode {
	int posx;
	int posy;
	SnakeNode* pNextNode;
	SnakeNode* pLastNode;
} *pSnakeHead, * pSnakeTail;

void GotoPos(int x, int y);
void Init();
void PrintSnake();

int main() {
	Init();

	while (1) {
		PrintSnake();
		Sleep(100); //程序休眠100ms
		system("cls"); //清空控制台
	}
	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() {
	pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间
	pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
}

void PrintSnake() {
	SnakeNode* pTemp = pSnakeHead;
	while (pTemp != nullptr) {
		GotoPos(pTemp->posx, pTemp->posy);
		cout << 'O';
		pTemp = pTemp->pNextNode;
	}
}

main()函数的while里面我们用了Sleep()函数,这个函数尤其重要,传入的参数100表示程序运行至此休眠100毫秒。由此可以见得,我们可以用这个函数控制游戏的帧率,但直接单独使用Sleep只能粗略地进行控制,因为单次循环的真正耗时 = Sleep + 游戏代码运行时间,至于更好的方法我会在后续介绍。

代码运行截图如下:

c/c++游戏编程之控制台贪吃蛇(一)_第3张图片

控制台只在坐标(10, 10)原地打印’O’,这说明我们还没有让蛇动起来,我们先定义两个全局变量(speedx和speedy),用来表示蛇在各个方向上的速度。

int speedx; //x轴速度
int speedy; //y轴速度

再在Init()函数里为它们初始化:

void Init() {
	speedx = 1; //游戏开始时蛇默认向右移动
	speedy = 0;
	pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间
	pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
}

为了让蛇动起来,如果把蛇整体的坐标都重新改变,那么将会产生遍历每个节点的巨大开销,为了提高效率,我们采取为蛇头部添加新节点,然后删去尾巴节点的方法,而无需关心身体中间的部分,这也是为什么选择用链表而不用数组实现贪吃蛇的原因。

定义刷新蛇头的函数

void RefreshSnakeHead(int posx, int posy) {
	SnakeNode* pNewNode = new SnakeNode{ posx, posy, pSnakeHead, nullptr }; //在新位置形成新蛇头, 新蛇头next指针指向旧蛇头
	pSnakeHead->pLastNode = pNewNode; //旧蛇头last指针指向新蛇头
	pSnakeHead = pNewNode; //更新蛇头指针
}

定义刷新蛇尾的函数

void RefreshSnakeTail() {

	SnakeNode* pTemp = pSnakeTail; //定义临时指针
	pSnakeTail = pSnakeTail->pLastNode; //上一节身体变成新蛇尾
	pSnakeTail->pNextNode = nullptr;
	
	delete pTemp; //释放老蛇尾
	pTemp = nullptr; //为释放后的指针置空
}

定义一个Move()函数,用来控制蛇的移动,当获取到用户输入时如果输入为WSAD中的一个,就改变蛇的速度speedx和speedy,然后调用刷新蛇头蛇尾的函数:

void Move() {

	char cinput;

	if (_kbhit()) {

		cinput = _getch(); //使用_getch()需要 #include 

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

我们使用W(小写字母w的ASCII码为87,大写字母的ASCII码W为119)、S、A、D键控制蛇的上下左右移动。并使用_kbhit()函数和_getch()函数来非阻塞地获取玩家键盘输入。
(tips: 在Visual Studio中直接使用kbhit()和getch()可能无法通过安全检查,因此使用安全版本的_kbhit()和_getch(),或者去设置里关闭安全检查)

改写main函数:

int main() {
	Init();

	while (1) {
		Move();
		PrintSnake();
		Sleep(100);
		system("cls");
	}
    return 0;
}

全部代码:

//控制台贪吃蛇
#include 
#include 
#include 

using std::cout;

struct SnakeNode {
	int posx;
	int posy;
	SnakeNode* pNextNode;
	SnakeNode* pLastNode;
} *pSnakeHead, * pSnakeTail;

int speedx; //x轴速度
int speedy; //y轴速度

//设置光标位置
void GotoPos(int x, int y);
//初始化游戏
void Init();
//打印蛇体
void PrintSnake();
//刷新蛇头节点
void RefreshSnakeHead(int posx, int posy);
//刷新蛇尾节点
void RefreshSnakeTail();
//移动控制
void Move();

int main() {
	Init();

	while (1) {
		Move();
		PrintSnake();

		Sleep(100);
		system("cls");
	}

	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() {
	speedx = 1; //游戏开始时蛇默认向右移动
	speedy = 0;
	pSnakeHead = new SnakeNode{ 10, 10, nullptr, nullptr }; //为蛇头申请一个空间
	pSnakeTail = pSnakeHead; //刚开始只有一节身体,蛇头即蛇尾
}

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; //更新蛇头指针
}

void RefreshSnakeTail() {

	SnakeNode* pTemp = pSnakeTail;
	pSnakeTail = pSnakeTail->pLastNode;
	pSnakeTail->pNextNode = nullptr;

	delete pTemp; //释放节点空间
	pTemp = nullptr; //为释放后的指针置空
}

void Move() {

	char cinput;

	if (_kbhit()) {

		cinput = _getch(); //使用getch()需要 #include 

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

运行截图:

c/c++游戏编程之控制台贪吃蛇(一)_第4张图片
c/c++游戏编程之控制台贪吃蛇(一)_第5张图片

蛇动了起来,并且可以操控方向。

做到这里,感官敏锐的你是不是发现我们的控制台会出现“闪屏”现象?问题就在于main函数中的system(“cls”)。我将在后续讲解避免“闪屏”的方法以及优化效率的方法。

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

求点赞、收藏!欢迎在评论区留言,有问必答!
作者水平有限,如果有误,欢迎指正!
编译环境:Visual Studio 2019

你可能感兴趣的:(c++游戏编程,c++,游戏,学习,c语言)