一、设计思路
贪吃蛇游戏的基本游戏规则如下:
- 蛇在固定游戏框架区域内移动;
- 蛇身体行径轨迹会沿着蛇头行径轨迹移动;
- 框架内随机出现食物,蛇吃掉食物后身体变长,食物被吃掉后系统随机生成下一个食物;
- 当蛇头碰到蛇身体或障壁时游戏结束。
不难看出,在贪吃蛇游戏代码设计中,难点在于如何实现蛇身体移动轨迹与蛇头保持一致。如图所示:
从上图不难看出,蛇的移动方式基本遵循“加头去尾”的规则,即蛇头每次朝指定方向前进一个单位,蛇尾朝蛇头行径轨迹跟进一个单位;同时,蛇头每次“吃下”一个食物时,蛇尾“保持不动”而蛇头“前进一个单位”。
在学习了C++的标准模板库(STL)之后,笔者发现:通过设计一个贪吃蛇的类,该类中包含记录蛇头坐标和蛇尾坐标的成员,以及记录蛇身移动方式数据的vector向量成员。当蛇头每次移动一个单位,vector尾部插入一个记录蛇头移动方向的数据,同时vector首部删除一个数据,蛇尾移动前通过读取vector首部数据从而可以按照蛇头移动方向进行移动。而蛇头“吃下”食物时,只需在vector尾部插入记录蛇头移动方向的数据,vector首部不删除数据。
如此一来,仅使用vector向量记录的方式,即可用较少的代码实现贪吃蛇移动。
二、代码实现
下面通过实例代码来描述贪吃蛇游戏设计的基本思路。
程序设计平台:Visual Studio 2022 (需添加easyX插件)
打开Visual Studio 2022,选择添加C++控制台空白项目即可。
下面展示程序代码(附注释说明):
1、类定义
项目中添加.h类文件,添加如下代码:
#include <graphics.h> #include <vector> #include "Var.cpp" using namespace std; using namespace SnakeVar; #pragma once class Snake //贪吃蛇基类 { public: int HeadX; //蛇头部坐标 int HeadY; int TailX; //蛇尾部坐标 int TailY; vector<int> snake; //设置向量snake表示贪吃蛇蛇身方向数据,每个单位数值范围为[1,4] int map[GameWidth][GameHeight] = { 0 }; //游戏区域 void SetHead(int cx, int cy, int length); //初始化蛇头位置 };
讯享网
在相应.cpp 文件中对Snake::SetHead()函数进行定义:
讯享网#include "Snake.h" void Snake::SetHead(int cx, int cy, int length) { HeadX = cx; //设置蛇头坐标 HeadY = cy; for (int i=0;i<length;i++) //蛇头朝下length-1个单位均赋值为1代表蛇身 map[cx][cy+i] = 1; TailX = cx; TailY = cy+length-1; //最后一个单位所在坐标为蛇尾坐标 snake.resize(length, 1); //初始化向量snake容量,数据均赋值为1 }
2、头文件引用及变量声明
#include <graphics.h> //使用easyX画图及窗口创建功能 #include <conio.h> //代码中使用getchar(),_getch()及_kbhit()函数需引用 #include <vector> //使用vector向量 #include <time.h> //时间函数为随机数提供种子 #include <stdlib.h> //使用随机函数rand()功能 using namespace std; using namespace SnakeVar; //变量声明区 unsigned int snakelength = InitSnakeLen; //蛇身长度 Snake newsnake; static Direction Movingflag = DIR_UP; //初始默认运动方向为向上
3、定义常量及宏
新建一个Var.cpp文件用于定义常量及宏:
讯享网namespace SnakeVar { const unsigned int GameWidth = 15; //游戏区域长宽值 const unsigned int GameHeight = 15; typedef enum { DIR_UP = 1, DIR_DOWN = 2, DIR_LEFT = 3, DIR_RIGHT = 4 } Direction; //枚举移动方向类型 #define BLOCK 30 //单个方块像素单位 #define InitSnakeLen 2 //初始蛇长度(包括头部) }
4、主程序函数声明及定义

在主程序所在Main.cpp文件中对所用函数声明如下:
//函数声明 int Crand(int MinValue, int MaxValue); //设置范围随机数 void DrawMap(); //绘制贪吃蛇运动范围 void InitGame(); //窗口初始化 void MoveSnake(Direction direction); //蛇身移动 void SetFood(); //在贪吃蛇运动范围内生成食物 void SetWindowCentered(HWND hwnd); //窗口显示中心化
下面对程序中使用的函数逐一说明:
(1)随机函数Crand()
讯享网int Crand(int MinValue, int MaxValue) { srand((unsigned)time(NULL) + (unsigned)rand()); int result = (rand() % (MaxValue - MinValue + 1)) + MinValue; return result; }
总所周知,C/C++语言产生随机数是需要“种子”(srand())来辅助的,然而系统默认种子数值为1,因此单纯使用rand()函数生成的随机数具有很强规律性,随机性很差。因此,在实践中通常使用时间函数time()作为种子以提高随机数的随机性。这里通过Crand()函数可以随机在最小值MinValue和最大值MaxValue之间生成随机数,为贪吃蛇的“食物”提供随机坐标位置。
(2)DrawMap()绘制界面
上文在Snake类中定义了二维数组map用于提供界面坐标,map数组中各单位不同数值代表不同含义:数值为0表示空白区域,用黑色方块表示;数值大于0表示该区域为贪吃蛇身体占据部分,用蓝色方块表示;数值小于0表示该点位为“食物”所在位置,用黄色方块表示。最后,根据Snake类中蛇头位置坐标HeadX及HeadY数值定位蛇头位置,用红色方块表示。
程序中每次map数组中发生数值变动,使用DrawMap()函数遍历map数组并重新绘制界面:
void DrawMap() { setlinestyle(PS_DOT); //单元格边框 for (int x = 0; x < GameHeight; x++) { for (int y=0;y< GameWidth;y++) if (newsnake.map[y][x] == 0) //数值为0区域为空白区 { setfillcolor(BLACK); fillrectangle(y*BLOCK, x*BLOCK, (y+1)*BLOCK, (x+1)*BLOCK); } else if (newsnake.map[y][x] > 0) //数值大于0区域为蛇身部分 { setfillcolor(BLUE); fillrectangle(y * BLOCK, x * BLOCK, (y + 1) * BLOCK, (x + 1) * BLOCK); } else if (newsnake.map[y][x] < 0) //负数部分为食物 { setfillcolor(YELLOW); fillrectangle(y * BLOCK, x * BLOCK, (y + 1) * BLOCK, (x + 1) * BLOCK); } } setfillcolor(RED); //蛇头绘制为红色 fillrectangle(newsnake.HeadX * BLOCK, newsnake.HeadY * BLOCK, (newsnake.HeadX + 1) * BLOCK, (newsnake.HeadY + 1) * BLOCK); }
(3)窗口初始化InitGame()
依据初始高度及初始宽度绘制窗口,并居中显示:
讯享网void InitGame() { initgraph(GameWidth * BLOCK, GameHeight * BLOCK, 0); HWND hwnd = GetHWnd(); //获取窗口句柄 SetWindowCentered(hwnd); //窗口居中显示 }
(4)移动蛇头MoveSnake()
void MoveSnake(Direction direction) { vector<int>::iterator sp = newsnake.snake.begin(); //迭代器指向向量首部 unsigned int number; //传递移动方式数据 switch (direction) { case DIR_UP: number = 1; //方向数据:向上移动为1,向下移动为2,向左移动为3,向右移动为4 if (newsnake.HeadY <= 0) exit(0); //蛇头碰到最顶端时 if (newsnake.map[newsnake.HeadX][newsnake.HeadY-1]<0) //当蛇头下一次移动位置上有食物 { snakelength++; //蛇身长度增加 newsnake.snake.push_back(number); //向量尾部插入记录蛇头移动方式的数据 newsnake.map[newsnake.HeadX][--newsnake.HeadY] = number; //蛇尾坐标不变,蛇头坐标向当前方向增加一个单位,新坐标位置赋值大于0表示位置被蛇身占据 SetFood(); //生成下一个食物 } else if (newsnake.map[newsnake.HeadX][newsnake.HeadY - 1]==0) //蛇头下一次移动位置为空白区域时 { newsnake.snake.erase(sp); //蛇头每次移动一个单位,向量snake删除向量首部数据并在向量尾部添加蛇头读取的新方向数据 newsnake.snake.push_back(number); newsnake.map[newsnake.HeadX][--newsnake.HeadY] = number; switch (newsnake.snake.front()) //蛇尾移动方式需从向量snake首部读取数据 { case 1: newsnake.map[newsnake.TailX][newsnake.TailY--] = 0; //读取数值为1,蛇尾向上移动,同时原坐标位置设置为0 break; case 2: newsnake.map[newsnake.TailX][newsnake.TailY++] = 0; //读取数值为2,蛇尾向下移动,同时原坐标位置设置为0 break; case 3: newsnake.map[newsnake.TailX--][newsnake.TailY] = 0; //读取数值为3,蛇尾向左移动,同时原坐标位置设置为0 break; case 4: newsnake.map[newsnake.TailX++][newsnake.TailY] = 0; //读取数值为4,蛇尾向右移动,同时原坐标位置设置为0 break; } } else //当蛇头下一次移动位置上数值大于0,表明蛇头碰到蛇身 { exit(0); //程序退出 } break; case DIR_DOWN: //其他方向控制代码参考上述代码 number = 2; if (newsnake.HeadY+1 >= GameHeight) exit(0); if (newsnake.map[newsnake.HeadX][newsnake.HeadY + 1] < 0) { snakelength++; newsnake.snake.push_back(number); newsnake.map[newsnake.HeadX][++newsnake.HeadY] = number; SetFood(); } else if (newsnake.map[newsnake.HeadX][newsnake.HeadY + 1] == 0) { newsnake.snake.erase(sp); newsnake.snake.push_back(number); newsnake.map[newsnake.HeadX][++newsnake.HeadY] = number; switch (newsnake.snake.front()) { case 1: newsnake.map[newsnake.TailX][newsnake.TailY--] = 0; break; case 2: newsnake.map[newsnake.TailX][newsnake.TailY++] = 0; break; case 3: newsnake.map[newsnake.TailX--][newsnake.TailY] = 0; break; case 4: newsnake.map[newsnake.TailX++][newsnake.TailY] = 0; break; } } else { exit(0); } break; case DIR_LEFT: number = 3; if (newsnake.HeadX <= 0) exit(0); if (newsnake.map[newsnake.HeadX-1][newsnake.HeadY] < 0) { snakelength++; newsnake.snake.push_back(number); newsnake.map[--newsnake.HeadX][newsnake.HeadY] = number; SetFood(); } else if (newsnake.map[newsnake.HeadX-1][newsnake.HeadY] == 0) { newsnake.snake.erase(sp); newsnake.snake.push_back(number); newsnake.map[--newsnake.HeadX][newsnake.HeadY] = number; switch (newsnake.snake.front()) { case 1: newsnake.map[newsnake.TailX][newsnake.TailY--] = 0; break; case 2: newsnake.map[newsnake.TailX][newsnake.TailY++] = 0; break; case 3: newsnake.map[newsnake.TailX--][newsnake.TailY] = 0; break; case 4: newsnake.map[newsnake.TailX++][newsnake.TailY] = 0; break; } } else { exit(0); } break; case DIR_RIGHT: number = 4; if (newsnake.HeadX + 1 >= GameWidth) exit(0); if (newsnake.map[newsnake.HeadX+1][newsnake.HeadY] < 0) { snakelength++; newsnake.snake.push_back(number); newsnake.map[++newsnake.HeadX][newsnake.HeadY] = number; SetFood(); } else if (newsnake.map[newsnake.HeadX+1][newsnake.HeadY] == 0) { newsnake.snake.erase(sp); newsnake.snake.push_back(number); newsnake.map[++newsnake.HeadX][newsnake.HeadY] = number; switch (newsnake.snake.front()) { case 1: newsnake.map[newsnake.TailX][newsnake.TailY--] = 0; break; case 2: newsnake.map[newsnake.TailX][newsnake.TailY++] = 0; break; case 3: newsnake.map[newsnake.TailX--][newsnake.TailY] = 0; break; case 4: newsnake.map[newsnake.TailX++][newsnake.TailY] = 0; break; } } else { exit(0); } break; } DrawMap(); //每完成一次移动过程,界面绘制一次 Sleep(1000-(snakelength-InitSnakeLen)*10); //移动后延迟时间,蛇身越长,延迟时间越短 }
上文提到,贪吃蛇移动方式简化为蛇头坐标及蛇尾坐标数值变动以及snake向量增、删数据的算法。本代码中,对蛇头下一次移动后动作作如下判定:
- 当蛇头下一次移动位置为空白区域时,向snake向量尾部添加数据,同时删除snake向量首部数据;
- 当蛇头下一次移动位置上有食物时,仅完成向量尾部添加数据的动作,不删除向量首部数据;
- 当蛇头移动到游戏边界时,游戏结束。
具体移动方式处理可参考上述代码注释。
(5)随机生成食物SetFood()
讯享网void SetFood() { re: int cx = 0, cy = 0; cx = Crand(0, GameWidth-1); Sleep(150); //人为制造时间差,降低X与Y数值相同的几率 cy = Crand(0, GameHeight-1); if (newsnake.map[cx][cy] > 0) //随机坐标位于蛇身时重新生成 goto re; else newsnake.map[cx][cy] = -1; }
该段代码中为方便,在判定随机坐标位是否位于蛇身的部分使用了goto语句,不喜欢goto语句的可用do-while循序方式改写。
(6)设置窗口居中SetWindowCentered()
void SetWindowCentered(HWND hwnd) { RECT rect; INT windowX, windowY; GetWindowRect(hwnd, &rect); windowX = (GetSystemMetrics(SM_CXFULLSCREEN) - (rect.right - rect.left)) / 2; //设置窗口居中显示位置 windowY = (GetSystemMetrics(SM_CYFULLSCREEN) - (rect.bottom - rect.top)) / 2; SetWindowPos(hwnd, HWND_TOPMOST, windowX, windowY, -1, -1, SWP_NOSIZE | SWP_NOZORDER);
5、main函数部分
主程序段代码如下:
讯享网int main(int argc, char* argv[]) { InitGame(); //初始化窗口 newsnake.SetHead(GameWidth/2, GameHeight/2, snakelength); //初始化蛇头位置 SetFood(); //设置食物位置 DrawMap(); //游戏界面绘制 while (true) //通过循环代码实现蛇身连续不停运动 { if (_kbhit()) { char ch = _getch(); //使用wsad按键实现上下左右方向控制 switch (ch) { case 'w': if (Movingflag !=DIR_DOWN) Movingflag = DIR_UP; //当蛇头向下运动时向上运动按键失效,避免触碰蛇身 break; case 's': if (Movingflag != DIR_UP) Movingflag = DIR_DOWN; //当蛇头向上运动时向下运动按键失效 break; case 'a': if (Movingflag != DIR_RIGHT) Movingflag = DIR_LEFT; //当蛇头向右运动时向左运动按键失效 break; case 'd': if (Movingflag != DIR_LEFT) Movingflag = DIR_RIGHT; //当蛇头向左运动时向右运动按键失效 break; } } MoveSnake(Movingflag); //完成一次蛇头运动,然后循环 } getchar(); return 0; }
代码编译后,调试效果如下:

内容如有错误,欢迎随时批评指正,谢谢各位。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/74936.html