贪吃蛇

贪吃蛇游戏开发

在学习完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 中的一个结构体,用于表示控制台屏幕缓冲区中的坐标。
  • 它包含两个成员:XY,分别表示横坐标和纵坐标。

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.xp.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;
		}
	}
}

食物生成

使用maphasFood判断地图内是否有食物

使用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;
}

posted @ 2025-02-18 02:26  十四2001  阅读(39)  评论(0)    收藏  举报