# 前言
最近学习了 C++ STL, 想着自己使用 C++ 实现一个贪吃蛇的小游戏,锻炼一下 C++ 代码能力和软件工程能力。完整代码参见:贪吃蛇小游戏 ——C++ 实现
# 贪吃蛇的基本规则
首先,有一块场地,场地周围是墙,场地内的部分是墙,部分是食物,部分是蛇。贪吃蛇没吃到任何东西,就正常移动;吃到食物,尾巴会增长;如果吃到了自己的尾巴或者撞了墙,游戏结束。
# 游戏基本设定
- 场地内部墙的个数在 [1,min (height-2,width-2)] 之间
- 1 次只出现一个食物
- 用户输入场地规格必须大于 3
- 默认蛇的移动方向向右,初始位置随机生成
- 如果贪吃蛇吃到了食物,会在新的地方生成食物
- 因为本人使用
vscode
编程,无法弹出控制台,因此无法追踪光标位置,也就难以实现原地打印。因此,本程序中使用每 1s 打印一次棋盘的方式.
# 贪吃蛇对象划分
- 场地:周围用墙环绕,里面有部分墙,食物和蛇
- 游戏本身
- 游戏分数:贪吃蛇吃到的食物数量
- 游戏状态:用
true
/ false
表示是否结束
# 贪吃蛇对象设计
- 场地:
- 场地本身:使用一个二维数组表示,场地内的墙用
#
表示,蛇用 S
表示,食物用 $
表示,普通平地用 `` 表示。某个单位只能出现一个物种,不可能同时出现两种。
- 场地的长、宽:用
int
类型表示
- 蛇:使用一个结构体进行存储,其中几个字段分别为:
- 整条蛇的位置信息:使用一个
std::deque
存储,其中单个位置信息使用 std::pair<int,int>
来存储
- 蛇的运动方向:使用
int
存储 (使用一个枚举类型 INPUT_KEY
,来表示对应的 int
值)
- 游戏:使用一个结构体进行存储,其中几个字段分别为:
- 游戏分数:使用
int
类型存储
- 游戏状态:使用
true/false
表示是否结束
- 用户的名字
# 游戏主逻辑设计
- 提示用户输入游戏场地规格:长,宽 / 默认(不输入)
- 提示用户输入姓名
- 初始化游戏场地
- 初始化游戏分数和状态
- 初始化默认开始方向
while
游戏没有结束
- 获取用户方向输入
- 如果没有输入方向,则按照上次的方向继续
- 根据用户输入方向更新游戏状态
- 打印棋盘
- 通知用户游戏结束,打印其姓名和分数,清理程序中所有的值
# 游戏状态更新逻辑
1. 根据蛇头,计算下一个位置的坐标
2. 如果场地中下个位置的值是`S`,判断其是否为蛇尾,如果不是蛇尾或者是墙,游戏结束
3. 如果下个位置是`$`,则(不去掉蛇尾):
1. 游戏分数+1
4. 否则:
1. 将场地蛇尾处`S`修改为' '
5. 将下一个位置加载蛇头
6. 将蛇头处' '修改为`S`
# 游戏对象定义 ( game_init.h
)
| #include <deque> |
| |
| |
| enum INPUT_KEY |
| { |
| RIGHT = 77, |
| LEFT = 75, |
| UP = 72, |
| DOWN = 80, |
| }; |
| |
| |
| typedef struct |
| { |
| INPUT_KEY direction; |
| std::deque<std::pair<int, int>> position; |
| } snake; |
# 游戏
| |
| typedef struct |
| { |
| int score; |
| bool end; |
| char *name; |
| } game; |
# 场地
| |
| typedef struct |
| { |
| char **arr; |
| int width; |
| int height; |
| snake s; |
| } board; |
# 游戏对象初始化( game_init.c
)
# 游戏初始化
| |
| * @brief Initialize the game |
| * |
| * @param g the game struct |
| * @param name the name of the user |
| */ |
| void init_game(game &g, char *name) |
| { |
| g.end = false; |
| g.name = name; |
| g.score = 0; |
| } |
# 场地初始化
# 初始化内容
场地的初始化包括初始化如下几个部分:
- 场地:
- 场地本身:使用一个二维数组表示,场地内的墙用
#
表示,蛇用 S
表示,食物用 $
表示,普通平地用 `` 表示。某个单位只能出现一个物种,不可能同时出现两种。
- 场地的长、宽:用
int
类型表示
- 蛇:使用一个结构体进行存储,其中几个字段分别为:
- 整条蛇的位置信息:使用一个
std::deque
存储,其中单个位置信息使用 std::pair<int,int>
来存储
- 蛇的运动方向:使用
int
存储 (使用一个枚举类型 INPUT_KEY
,来表示对应的 int
值)
# 初始化步骤
初始化的过程分为如下步骤:
- 初始化场地的长和宽
- 初始化一个对应长和宽的二维数组,且其值为 ``
- 让二维数组的四周为
#
- 使用随机数生成器生成一个 [1,min (height-2,width-2)] 内随机的内部墙数量
- 随机生成内部墙的坐标
- 在对应位置处放置
#
- 随机生成食物的坐标
- 在对应位置处放置
$
- 随机生成蛇的坐标
- 初始化蛇,方向向右
- 在对应位置处放置
S
# 辅助函数 1:随机数生成器
C++ std::rand()
函数生成一个 [0, RAND_MAX
) 之间的数字。如果我们想生成一个 [0,num] 范围内的数字,需要 (1 + std::rand()) % num
。如果 num<=0
,我们不生成,直接返回 0。
| |
| * @brief generate a random number between [1,num] |
| * |
| * @param num the maximum of the random number |
| * @return int the random number generated |
| */ |
| int random(int num) |
| { |
| if (num <= 0) |
| { |
| return 0; |
| } |
| return (1 + std::rand()) % num + 1; |
| } |
# 辅助函数 2: place
函数
food
和 snake
一次只放置 1 个。 food
在初始化和每次被蛇吃掉后都需要重新放置(后期 update
)中还需要调用,而 snake
只有在初始化时候需要放置。因为其每次都是持续搜索,直到找到标记为 `` 的位置,因此我们写了一个 place
函数用于放置内容。该函数返回一个 pair
,用于 snake
初始化时将坐标压到 deque
中。
| |
| * @brief the generic method to place some char in the board |
| * |
| * @param b board |
| * @param p the character to be placed |
| */ |
| std::pair<int, int> place(board &b, char p) |
| { |
| int xPos_max = b.height - 2; |
| int yPos_max = b.width - 2; |
| int x = random(xPos_max); |
| int y = random(yPos_max); |
| if (b.arr[x][y] != ' ') |
| { |
| return place(b, p); |
| } |
| else |
| { |
| b.arr[x][y] = p; |
| return std::make_pair(x, y); |
| } |
| } |
# 辅助函数 3: place_food
& place_snake
在 place_food
中,我们直接调用 place
函数,将传入的 char
设定为 $
;而在 place_snake
中,除了需要放置食物外,还需要将 place
函数返回的坐标压到 deque
中,并且初始化方向。
| |
| * @brief initialize the snake in the board |
| * |
| * @param b the game board |
| */ |
| void place_snake(board &b) |
| { |
| std::pair<int, int> snake_pos = place(b, 'S'); |
| b.s.position.push_back(snake_pos); |
| b.s.direction = RIGHT; |
| } |
# 辅助函数 4: print_board
| |
| * @brief print the board of the game |
| * |
| * @param b the game board |
| */ |
| void print_board(board &b) |
| { |
| int head_x = b.s.position[0].first; |
| int head_y = b.s.position[0].second; |
| for (int i = 0; i < b.height; i++) |
| { |
| for (int j = 0; j < b.width; j++) |
| { |
| if(i == head_x && j == head_y) { |
| std::cout << "\033[0;31m" << b.arr[i][j] << "\033[0m" << " "; |
| } |
| else{ |
| std::cout << b.arr[i][j] << " "; |
| } |
| } |
| std::cout << std::endl; |
| } |
| } |
# 初始化 board
该步骤遵循前文初始化步骤。
| |
| * @brief Initialize the board |
| * |
| * @param b the board struct |
| * @param width the width of the board |
| * @param height the height of the board |
| */ |
| void init_board(board &b, int width, int height) |
| { |
| |
| b.width = width; |
| b.height = height; |
| |
| |
| b.arr = new char *[height]; |
| for (int i = 0; i < height; i++) |
| { |
| b.arr[i] = new char[width]; |
| std::fill(b.arr[i][0], b.arr[i][width - 1], ' '); |
| } |
| |
| |
| std::fill(b.arr[0][0], b.arr[0][width - 1], '#'); |
| for (int i = 1; i < height - 1; i++) |
| { |
| b.arr[i][0] = '#'; |
| b.arr[i][width - 1] = '#'; |
| } |
| std::fill(b.arr[height - 1][0], b.arr[height - 1][width - 1], '#'); |
| |
| |
| int xPos_max = height - 2; |
| int yPos_max = width - 2; |
| |
| |
| int wall_num = random(std::min(height - 2, width - 2)); |
| for (int i = 0; i < wall_num; i++) |
| { |
| int x = random(xPos_max); |
| int y = random(yPos_max); |
| b.arr[x][y] = '#'; |
| } |
| |
| |
| place_food(b); |
| |
| |
| place_snake(b); |
| } |
# 游戏逻辑实现
- 提示用户输入游戏场地规格:长,宽 / 默认(不输入), 如果长或宽小于 3, 则提示用户重新输入
- 提示用户输入姓名
- 初始化游戏场地
- 初始化游戏分数和状态
- 初始化默认开始方向
while
游戏没有结束:
- 获取用户方向输入
- 如果没有输入方向,则按照上次的方向继续
- 根据用户输入方向更新游戏状态
- 通知用户游戏结束,打印其姓名和分数,清理程序中所有的值
| |
| while (!g.end) |
| { |
| if (_kbhit()) |
| { |
| _getch(); |
| int key = _getch(); |
| b.s.direction = static_cast<INPUT_KEY>(key); |
| } |
| update(g, b); |
| print_board(b); |
| sleep(1); |
| } |
# 游戏结束
| |
| std::cout << "Game Over!" << std::endl; |
| std::cout << "Your Score is: " << g.score << std::endl; |
| |
| |
| delete[] name; |
| for (int i = 0; i < b.height; i++) |
| { |
| delete[] b.arr[i]; |
| } |
| delete[] b.arr; |
# update
函数更新游戏状态
# 步骤
1. 根据蛇头,计算下一个位置的坐标
2. 如果场地中下个位置的值是`S`,判断其是否为蛇尾,如果不是蛇尾或者是墙,游戏结束
3. 如果下个位置是`$`,则(不去掉蛇尾):
1. 游戏分数+1
4. 否则:
1. 将场地蛇尾处`S`修改为' '
5. 将下一个位置加载蛇头
6. 将蛇头处' '修改为`S`
# update
函数
| void update(game &g, board &b) |
| { |
| |
| std::pair<int, int> head = b.s.position[0]; |
| int x = head.first; |
| int y = head.second; |
| |
| |
| std::pair<int, int> tail = b.s.position.back(); |
| int tail_x = tail.first; |
| int tail_y = tail.second; |
| |
| |
| switch (b.s.direction) |
| { |
| case UP: |
| x -= 1; |
| break; |
| case DOWN: |
| x += 1; |
| break; |
| case RIGHT: |
| y += 1; |
| break; |
| case LEFT: |
| y -= 1; |
| break; |
| default: |
| break; |
| } |
| |
| |
| if (b.arr[x][y] == '#' || (b.arr[x][y] == 'S' && x != tail_x && y != tail_y)) |
| { |
| g.end = true; |
| return; |
| } |
| |
| |
| if (b.arr[x][y] == '$') |
| { |
| g.score += 1; |
| } |
| else |
| { |
| b.arr[tail_x][tail_y] = ' '; |
| b.s.position.pop_back(); |
| } |
| b.s.position.push_front(std::pair<int, int>(x, y)); |
| b.arr[x][y] = 'S'; |
| } |
# bug 修复
random()
随机数产生需要在后面 +1
- 为
board
新增 plain
字段,让蛇占据所有空间后,通知用户通关!
- 长度为 1 时,可以反方向运动