c/c++游戏编程之控制台贪吃蛇(一)
c/c++游戏编程之控制台贪吃蛇(二)
如果你还未学过c++基本语法,请先学习基本语法再来学习游戏编程噢~。
对这样的“黑框框”是不是很熟悉:
这就是控制台程序,死板且黑乎乎的控制台也能干一些有趣的事情,没错,我们将在控制台中完成第一个游戏作品——贪吃蛇的开发。
控制台是在光标(一闪一闪的那个)的位置默认从左到右,从上到下的顺序打印字符的,如果想要在控制台中任意位置打印字符,那么我们就需要定义一个名为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个字符间距。
代码运行效果如图:
我们先定义一个结构体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 + 游戏代码运行时间,至于更好的方法我会在后续介绍。
代码运行截图如下:
控制台只在坐标(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();
}
运行截图:
蛇动了起来,并且可以操控方向。
做到这里,感官敏锐的你是不是发现我们的控制台会出现“闪屏”现象?问题就在于main函数中的system(“cls”)。我将在后续讲解避免“闪屏”的方法以及优化效率的方法。
c/c++游戏编程之控制台贪吃蛇(二)
求点赞、收藏!欢迎在评论区留言,有问必答!
作者水平有限,如果有误,欢迎指正!
编译环境:Visual Studio 2019