贪吃蛇
贪吃蛇游戏开发
在学习完C++零基础编程后检测学习成果,学习制作一个简单的贪吃蛇小游戏。由于还没有学习数据结构,蛇体使用二维数组来存储。
使用到的头文件有iostream、windows.h、conio.h
运用基础里的define关键字、运算符、for/while循环、if/switch条件、一维和二维数组、指针、引用、函数、结构体
学习了枚举类型enum、控制台隐藏光标(获取句柄、定义光标信息结构体、设置光标信息)、检测键盘输入_kbhit()、 获取键盘输入_getch()
游戏截图:
游戏运行 游戏结束

游戏循环
使用while(1)无限循环,程序启动后一直在while里运行
地图绘制
1. 定义地图宽和高
#define H 28
#define W 60
2. 实现drawMap函数
使用for循环打印边框
void drawMap(Map* map) {
system("cls");
//cout << "⌈⌉⌊⌋";
cout << "⌈";
for (int x = 0; x < W; x++) {
cout << "-";
}
cout << "⌉" << endl;
for (int y = 0; y < H; y++) {
cout << "|";
for (int x = 0; x < W; x++) {
if (map->data[y][x] == BlockType::EMPTY) {
cout << " ";
}
}
cout << "|" << endl;
}
cout << "⌊";
for (int x = 0; x < W; x++) {
cout << "-";
}
cout << "⌋";
}
光标隐藏*
引入头文件windows.h
1. 获取标准输出句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetStdHandle(STD_OUTPUT_HANDLE)是一个 Windows API 函数,用于获取当前控制台的标准输出设备的句柄。STD_OUTPUT_HANDLE表示标准输出流(通常是控制台窗口)。- 返回值是一个
HANDLE类型的对象,表示对控制台输出设备的引用。
2. 定义光标信息结构体
CONSOLE_CURSOR_INFO curInfo = { 1, FALSE };
CONSOLE_CURSOR_INFO是一个结构体,用于描述控制台光标的属性。- 它包含两个成员:
dwSize:光标的大小,以百分比表示(范围为 1 到 100)。这里设置为1,表示光标的大小为最小值。bVisible:布尔值,表示光标是否可见。FALSE表示隐藏光标,TRUE表示显示光标。
3. 设置光标信息
SetConsoleCursorInfo(hOutput, &curInfo);
SetConsoleCursorInfo是另一个 Windows API 函数,用于设置控制台光标的属性。- 参数:
- 第一个参数是控制台输出设备的句柄(
hOutput)。 - 第二个参数是指向
CONSOLE_CURSOR_INFO结构体的指针,指定新的光标属性。
- 第一个参数是控制台输出设备的句柄(
void hideCursor() {
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO curInfo = { 1,FALSE };
SetConsoleCursorInfo(hOutput, &curInfo);
}
地图定义
1. 定义枚举类型格子种类(BlockType)
enum BlockType {
EMPTY = 0,
FOOD = 1,
};
2. 定义地图结构体(Map)
包含BlockType类型的二维数组data[H][W]存储地图每个格子的类型和bool类型的hasFood来判断地图内是否有食物
struct Map {
BlockType data[H][W];
bool hasFood;
};
3. 实现initMap函数(初始化地图)
使用for循环给map的data数组赋值
void initMap(Map* map) {
for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
map->data[y][x] = BlockType::EMPTY;
}
}
map->hasFood = false;
}
蛇体定义
1. 定义位置结构体(Pos)
包含 x轴和y轴坐标
struct Pos {
int x;
int y;
};
2.定义蛇结构体(Snake)
包含Pos类型的数组snake[H*W]存储蛇的位置、snakeDir(蛇的方向)、snakeLength(蛇的长度)、lastMoveTime(上一次移动的时间)、moveFrequency(移动频率)
struct Snake {
Pos snake[H * W];
int snakeDir;
int snakeLength;
int lastMoveTime;
int moveFrequency;
};
3.实现initSnake函数(初始化蛇)
给蛇的每一个变量赋值
void initSnake(Snake* snk) {
snk->snakeLength = 3;
snk->snakeDir = 1;
snk->snake[0] = { W / 2,H / 2 };
snk->snake[1] = { W / 2 - 1,H / 2 };
snk->snake[2] = { W / 2 - 2,H / 2 };
snk->lastMoveTime = 0;
snk->moveFrequency = 200;
}
蛇体绘制
1、实现drawUnit函数(用于在格子上绘制)
1. 定义坐标变量
COORD coord;
COORD是 Windows API 中的一个结构体,用于表示控制台屏幕缓冲区中的坐标。- 它包含两个成员:
X和Y,分别表示横坐标和纵坐标。
2. 获取标准输出句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetStdHandle(STD_OUTPUT_HANDLE)是一个 Windows API 函数,用于获取当前控制台的标准输出设备的句柄。- 返回值是一个
HANDLE类型的对象,表示对控制台输出设备的引用。
3. 设置目标坐标
coord.X = p.x + 1;
coord.Y = p.y + 1;
- 这里将传入的坐标
p.x和p.y加上偏移量1,以调整实际绘制的位置。 - 偏移量
1的原因是绘制地图时占用了位置。
4. 设置光标位置
SetConsoleCursorPosition(hOutput, coord);
SetConsoleCursorPosition是一个 Windows API 函数,用于设置控制台光标的位置。- 参数:
- 第一个参数是控制台输出设备的句柄(
hOutput)。 - 第二个参数是目标坐标(
coord)。
- 第一个参数是控制台输出设备的句柄(
5. 输出内容
cout << unit;
- 使用
cout将字符串unit输出到控制台。 - 因为在此之前已经设置了光标位置,所以
unit会被绘制到指定的坐标处。
void drawUnit(Pos p, const char unit[]) {
COORD coord;
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
coord.X = p.x + 1;
coord.Y = p.y + 1;
SetConsoleCursorPosition(hOutput, coord);
cout << unit;
}
2.实现drawSnake函数(绘制蛇身)
for循环调用drwaUnit函数绘制蛇身
void drawSnake(Snake* snk) {
for (int i = 0; i < snk->snakeLength; i++) {
drawUnit(snk->snake[i], "■");
}
}
蛇体移动
蛇体运动时,蛇体已经绘制在移动前的对应位置,移动只需将蛇尾隐去,再绘制新的蛇头即可
1.实现moveSnake函数(计算蛇移动后的位置)
// snk [0] [1] [2]
// 原先蛇坐标 (0,0) (0,1) (0,2)
// 头 身 身
// 向下走 dir = (1,0)
// 蛇身 = 原先前一个蛇身(头)的坐标(snk.snake[i] = snk.snake[i-1],i从大到小)
// 蛇头 = 原先蛇头加上dir(snk[0])
// snk [0] [1] [2]
// 现在蛇坐标 (0+1,0+0) (0,0) (0,1)
// 头 身 身
void moveSnake(Snake* snk) {
for (int i = snk->snakeLength - 1; i >= 1; i--) {
snk->snake[i] = snk->snake[i - 1];
}
snk->snake[0].y += dir[snk->snakeDir][0];
snk->snake[0].x += dir[snk->snakeDir][1];
}
2.实现doMove函数
提取蛇尾位置,将蛇尾绘制为空,调用moveSnake计算蛇的位置,判断是否越界和是否吃到食物,再绘制蛇头
bool doMove(Snake* snk, Map* map) {
Pos tail = snk->snake[snk->snakeLength - 1];
drawUnit(tail, " ");
moveSnake(snk);
if (checkOutOfBound(snk->snake[0])) {
return false;
}
checkEatFood(snk, tail, map);
drawUnit(snk->snake[0], "■");
return true;
}
运动频率控制
使用GetTickCount获取当前时间,与lastMoveTime比较,间隔大于moveFrequency就执行doMove进行移动
实现checkSnakeMove函数
bool checkSnakeMove(Snake* snk, Map* map) {
int curTime = GetTickCount();
if (curTime - snk->lastMoveTime > snk->moveFrequency) {
if (doMove(snk, map) == false) {
return false;
}
snk->lastMoveTime = curTime;
}
return true;
}
边界检测
使用蛇头的x和y坐标与地图边缘进行比较
实现checkOutOfBound函数
bool checkOutOfBound(Pos p) {
return (p.x <= 0 || p.x >= W + 1||p.y <= 0 || p.y >= H + 1);
}
游戏结束
跳出程序主体的while循环,在地图中间绘制Game Over字样,再进入一个死循环阻止程序退出
drawUnit({ W / 2 - 4, H / 2 }, "Game over");
蛇体转向*
引入头文件conio.h
1. 定义方向
方向用二维数组存储,并设为常量
const int dir[4][2] = {
{-1,0},//上
{0,1}, //右
{1,0}, //下
{0,-1}//左
};
2. 实现checkChangeDIr函数
使用_kbhit()检测键盘输入
使用switch判断_getch()键盘接收的方向就,执行对应操作,设置限制条件防止原地掉头
void checkChangeDir(Snake* snk) {
if (_kbhit()) {
switch (_getch()) {
case'w':
if (snk->snakeDir != 2) {
snk->snakeDir = 0;
}
break;
case'd':
if (snk->snakeDir != 3) {
snk->snakeDir = 1;
}
break;
case's':
if (snk->snakeDir != 0) {
snk->snakeDir = 2;
}
break;
case'a':
if (snk->snakeDir != 1) {
snk->snakeDir = 3;
}
break;
default:
break;
}
}
}
食物生成
使用map的hasFood判断地图内是否有食物
使用while(1)循环尝试生成食物,生成成功后跳出
使用rand() % 高或宽随机生成食物的x和y坐标
使用循环遍历蛇身,检测食物的坐标是否与蛇身重合,在蛇身上就重新生成
确定生成食物位置可行,修改地图该单元的类型,置hasFood为1,在对应位置绘制食物
实现checkFoodGenerate函数
void checkFoodGenerate(Snake* snk, Map* map) {
if (map->hasFood == false) {
while (1) {
int x = rand() % W;
int y = rand() % H;
int i = 0;
while (i < snk->snakeLength) {
if (x == snk->snake[i].x && y == snk->snake[i].y) {
break;
}
i++;
}
if (snk->snakeLength == i) {
map->data[y][x] = BlockType::FOOD;
map->hasFood = true;
drawUnit({ x,y }, "●");
return;
}
}
}
}
食物碰撞
检测蛇头的位置是否和食物的位置重合,碰到食物,蛇长度加1,添加在尾部,置食物位置的类型改为EMPTY,置hasFood为0,绘制蛇尾
实现checkEatFood函数
void checkEatFood(Snake* snk, Pos tail, Map* map) {
Pos head = snk->snake[0];
if (map->data[head.y][head.x] == BlockType::FOOD) {
snk->snake[snk->snakeLength++] = tail;
map->data[head.y][head.x] = BlockType::EMPTY;
map->hasFood = false;
drawUnit(tail, "■");
}
}
完整代码
#include<iostream>
#include<Windows.h>
#include<conio.h>
using namespace std;
#define H 28
#define W 60
const int dir[4][2] = {
{-1,0},//上
{0,1}, //右
{1,0}, //下
{0,-1}//左
};
enum BlockType {
EMPTY = 0,
FOOD = 1,
};
struct Map {
BlockType data[H][W];
bool hasFood;
};
struct Pos {
int x;
int y;
};
struct Snake {
Pos snake[H * W];
int snakeDir;
int snakeLength;
int lastMoveTime;
int moveFrequency;
};
void initSnake(Snake* snk) {
snk->snakeLength = 3;
snk->snakeDir = 1;
snk->snake[0] = { W / 2,H / 2 };
snk->snake[1] = { W / 2 - 1,H / 2 };
snk->snake[2] = { W / 2 - 2,H / 2 };
snk->lastMoveTime = 0;
snk->moveFrequency = 200;
}
void initMap(Map* map) {
for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
map->data[y][x] = BlockType::EMPTY;
}
}
map->hasFood = false;
}
void hideCursor() {
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO curInfo = { 1,FALSE };
SetConsoleCursorInfo(hOutput, &curInfo);
}
void drawUnit(Pos p, const char unit[]) {
COORD coord;
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
coord.X = p.x + 1;
coord.Y = p.y + 1;
SetConsoleCursorPosition(hOutput, coord);
cout << unit;
}
void drawSnake(Snake* snk) {
for (int i = 0; i < snk->snakeLength; i++) {
drawUnit(snk->snake[i], "■");
}
}
void drawMap(Map* map) {
system("cls");
//cout << "⌈⌉⌊⌋";
cout << "⌈";
for (int x = 0; x < W; x++) {
cout << "-";
}
cout << "⌉" << endl;
for (int y = 0; y < H; y++) {
cout << "|";
for (int x = 0; x < W; x++) {
if (map->data[y][x] == BlockType::EMPTY) {
cout << " ";
}
}
cout << "|" << endl;
}
cout << "⌊";
for (int x = 0; x < W; x++) {
cout << "-";
}
cout << "⌋";
}
bool checkOutOfBound(Pos p) {
return (p.x <= 0 || p.x >= W + 1||p.y <= 0 || p.y >= H + 1);
}
// 原先蛇坐标 (0,0) (0,1) (0,2)
// 向下走 dir = (1,0)
// 蛇头 = 原先蛇头加上dir插入到蛇头位置(snk[0])
// 蛇身 = 原先前一个蛇身的坐标(snk.snake[i] = snk.snake[i-1],i从大到小)
// 现在蛇坐标 (0+1,0+0) (0,0) (0,1)
void moveSnake(Snake* snk) {
for (int i = snk->snakeLength - 1; i >= 1; i--) {
snk->snake[i] = snk->snake[i - 1];
}
snk->snake[0].y += dir[snk->snakeDir][0];
snk->snake[0].x += dir[snk->snakeDir][1];
}
void checkEatFood(Snake* snk, Pos tail, Map* map) {
Pos head = snk->snake[0];
if (map->data[head.y][head.x] == BlockType::FOOD) {
snk->snake[snk->snakeLength++] = tail;
map->data[head.y][head.x] = BlockType::EMPTY;
map->hasFood = false;
drawUnit(tail, "■");
}
}
bool doMove(Snake* snk, Map* map) {
Pos tail = snk->snake[snk->snakeLength - 1];
drawUnit(tail, " ");
moveSnake(snk);
if (checkOutOfBound(snk->snake[0])) {
return false;
}
checkEatFood(snk, tail, map);
drawUnit(snk->snake[0], "■");
return true;
}
bool checkSnakeMove(Snake* snk, Map* map) {
int curTime = GetTickCount();
if (curTime - snk->lastMoveTime > snk->moveFrequency) {
if (doMove(snk, map) == false) {
return false;
}
snk->lastMoveTime = curTime;
}
return true;
}
void checkChangeDir(Snake* snk) {
if (_kbhit()) {
switch (_getch()) {
case'w':
if (snk->snakeDir != 2) {
snk->snakeDir = 0;
}
break;
case'd':
if (snk->snakeDir != 3) {
snk->snakeDir = 1;
}
break;
case's':
if (snk->snakeDir != 0) {
snk->snakeDir = 2;
}
break;
case'a':
if (snk->snakeDir != 1) {
snk->snakeDir = 3;
}
break;
default:
break;
}
}
}
void checkFoodGenerate(Snake* snk, Map* map) {
if (map->hasFood == false) {
while (1) {
int x = rand() % W;
int y = rand() % H;
int i = 0;
while (i < snk->snakeLength) {
if (x == snk->snake[i].x && y == snk->snake[i].y) {
break;
}
i++;
}
if (snk->snakeLength == i) {
map->data[y][x] = BlockType::FOOD;
map->hasFood = true;
drawUnit({ x,y }, "●");
return;
}
}
}
}
void initGame(Snake* snk, Map* map) {
initMap(map);
initSnake(snk);
drawMap(map);
drawSnake(snk);
}
int main() {
Map map;
Snake snk;
hideCursor();
initGame(&snk, &map);
while (1) {
checkChangeDir(&snk);
if (checkSnakeMove(&snk, &map) == false) {
break;
}
checkFoodGenerate(&snk, &map);
}
drawUnit({ W / 2 - 4, H / 2 }, "Game over");
while(1){}
return 0;
}
浙公网安备 33010602011771号