导航
目录
一. 简易版的总结
首先,简易版贪吃蛇虽然有完整的游戏机制,但是却没有完整的游戏体验。我们小时候花10块钱买来的游戏机——任天堂BrickGame里面的贪吃蛇是有关卡的,既然是向经典致敬,那么一定要有原汁原味才行呢,下文中我将介绍墙体以及一些琐碎功能的添加。
其次,我们回到代码层面,在写了几个方块小游戏之后,我们不难发现有些函数和头文件几乎每个小游戏都会用到,或者说调用的频率十分高,那么我们为什么不把这几个函数和头文件放到一个Engine.h中去呢?这样虽然会增加函数调用带来的性能开销(即抽象惩罚),但是也可以大大减少工作量,尤其是小游戏合集做多了之后。
二. 引擎文件
先上完整代码,下文稍作解释。
#pragma once
#include <iostream>
#include <Windows.h>
#include <conio.h>
#include <iomanip>
#include <vector>
#include <list>
#include <functional>
// 点
struct Point
{
int x, y;
};
typedef std::vector<Point> Sites;
// 设置光标位置
inline void
SetPos(int i, short j) // 设置光标位置
{
COORD pos = {short(i << 1), j};
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
// 设置控制台属性
void SetConsole(std::string name, int width, int height, std::string color)
{
SetConsoleTitle(name.c_str());
system(("mode con cols=" + std::to_string(width) + " lines=" + std::to_string(height)).c_str());
system(("color " + color).c_str());
// 隐藏控制台光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
}
// 暂停游戏
void Pause()
{
while (true)
if (kbhit() && getch() == 32) // 按空格重新开始
break;
}
// 打印一串字符
void FillStr(int x, int y, const std::string &fillstr)
{
SetPos(x, y);
std::cout << fillstr;
}
// 填充矩形区域
void FillRec(int x, int y, int width, int height, const std::string &fillstr)
{
for (int i = 0; i < height; ++i)
{
SetPos(x, y + i);
for (int j = width; j--;)
std::cout << fillstr;
}
}
// 按像素点填充区域
void FillArea(int x, int y, const Sites &Sites, const std::string &fillstr)
{
for (const Point &Point : Sites)
{
SetPos(x + Point.x, y + Point.y);
std::cout << fillstr;
}
}
struct Player // 防止头文件被反复包含
{
};
首先#pragma once防止头文件被反复包含,但是在某些情况下还是会出问题,所以我们增加了一个空结构体struct Player{ };以便之后出现BUG后更快地定位。
<Windows.h>隐藏控制台窗口光标、设置控制台窗口光标位置的代码需要用到句柄,所以这里包含该头文件。
<conio.h>包含的getch()、kbhit()函数被使用到了。
<iomanip>控制输出格式,包含的setw()函数被使用到了。
SetPos():设置控制台窗口光标的位置。
SetConsole():设置窗口标题、宽高、颜色,隐藏控制台窗口光标。
FillStr():在该位置打印一行字符串。
FillRec():用两个字符填充一个矩形区域的每一个像素点。
FillArea():在像素点集的每个像素点位置处填充两个字符。
为什么是两个呢?因为小方块占两个字符吖,为了方便理解和操作,SetPos()函数被我改成了横坐标只能为偶数,所以相当于有一个网格,网格里面的每一个小方格(即像素点)都包含2个字符。所以这里FillRec()、FillArea()函数的参数最多2个字符,否则会出现混乱,而传递1个字符作为参数则相当于只填充了每个小方格的左半边。
我在VS上模仿别人做的一个控制台游戏合集项目(该系列的b站教程),用的是宽字符所以不用进行这种处理,但是我发现VScode好像不能很好的支持unicode字符集,所以这里还是用的GBK编码格式,并对SetPos函数进行了适当的处理。
另外,关于性能方面,除基本类型以外的所有类型一律采用引用传参,以提高参数传递的效率。
三. 主体文件
先上完整代码,下文稍作解释。
#include "../Engine/BrickEngine.h"
#include "Map.h"
void run();
std::list<Point> snack; // 贪吃蛇
// 食物位置、贪吃蛇方向、贪吃蛇速度、关卡数、玩家得分
int FoodX, FoodY, SnackD, Speed(1), MapNum(1), Score;
bool Dead; // 贪吃蛇是否死亡
std::string Map; // 关卡
void LogMap(std::string Map) // 设置蛇的属性并打印地图
{
if (MapNum == 5 || MapNum == 6)
SnackD = 0, snack = {{36, 24}, {36, 25}, {36, 26}};
else
SnackD = 1, snack = {{16, 17}, {15, 17}, {14, 17}};
SetPos(0, 4);
for (int i = 0; i < 40 * 26; ++i)
std::cout << (Map[i] == '0' ? "■" : " ");
}
void LogSelectMap() // 打印预览地图
{
Map = Maps[MapNum - 1];
LogMap(Map);
FillRec(14, 13, 12, 8, " ");
FillStr(18, 15, " Map: " + std::to_string(MapNum));
FillStr(18, 17, "Speed: " + std::to_string(Speed));
}
void AddFood() // 生成食物
{
bool food_CD = false;
while (true)
{
FoodX = rand() % 40, FoodY = rand() % 24 + 5;
if (Map[FoodX + (FoodY - 4) * 40] == '0')
food_CD = true;
else
for (const Point &s : snack)
if (s.x == FoodX && s.y == FoodY)
{
food_CD = true;
break;
}
if (!food_CD)
break;
food_CD = false;
}
FillStr(FoodX, FoodY, "●");
}
void SelectMap() // 选择地图
{
while (true)
{
if (kbhit())
{
int ch = getch();
if (ch == 224)
{
switch (getch())
{
case 75: // 小键盘左键
MapNum -= MapNum != 1;
break;
case 77: // 小键盘右键
MapNum += MapNum != 6;
break;
case 72: // 小键盘上键
Speed += Speed != 5;
break;
case 80: // 小键盘下键
Speed -= Speed != 1;
break;
}
LogSelectMap();
FillStr(9, 2, ' ' + std::to_string(MapNum));
FillStr(32, 2, std::to_string(Speed));
}
else if (ch == 32)
break;
}
}
}
void MoveSnack(std::list<Point> &snack) // 移动贪吃蛇
{
// 在链表头部插入坐标对
int dx = 0, dy = 0;
SnackD & 1 ? dx = 2 - SnackD : dy = SnackD - 1;
snack.insert(begin(snack), {snack.front().x + dx, snack.front().y + dy});
// 判断是否吃到食物
if (snack.front().x != FoodX || snack.front().y != FoodY) // 没吃到食物,丢弃贪吃蛇尾部
{
FillStr(snack.back().x, snack.back().y, " ");
snack.pop_back();
}
else // 吃到食物
{
SetPos(20, 2);
std::cout << std::setw(3) << ++Score;
AddFood();
}
// 判断死亡
if (Map[snack.front().x + (snack.front().y - 4) * 40] == '0')
Dead = true;
else
for (std::list<Point>::iterator it = ++snack.begin(); it != snack.end(); ++it)
if ((*it).x == snack.front().x && (*it).y == snack.front().y)
Dead = true;
// 显示贪吃蛇
if (Dead)
for (const Point &s : snack)
FillStr(s.x, s.y, "x");
else
{
FillStr(snack.front().x, snack.front().y, "□");
FillStr((*++snack.begin()).x, (*++snack.begin()).y, "■");
}
if (Dead) // 游戏结束
{
FillStr(17, 16, "Game Over!");
FillStr(13, 17, "press Spacebar to restart...");
Pause();
run();
}
}
void Initialize() // 初始化游戏
{
system("cls");
Dead = false, Score = 0;
FillStr(6, 1, "==========\t\t==============\t\t===========\n");
FillStr(6, 2, "| Map: " + std::to_string(MapNum) + " |\t\t| SCORE: " + std::to_string(Score) + " |\t\t| SPEED:" + std::to_string(Speed) + " |");
FillStr(6, 3, "==========\t\t==============\t\t===========\n");
LogSelectMap(); // 打印预选地图
SelectMap(); // 选择地图
LogMap(Map); // 打印已选地图
AddFood(); // 添加食物
}
void run() // 运行游戏
{
Initialize();
int t = 0; // 控制贪吃蛇的速度
bool move = false; // 使贪吃蛇:每移动一次至多改变一次方向
while (true)
{
Sleep(1);
if (t++ > 30 - Speed * 5)
{
MoveSnack(snack);
move = true, t = 0;
}
if (kbhit())
{
int ch = getch();
if (ch == 224 && move) // 改变贪吃蛇的方向
{
ch = getch();
SnackD = SnackD & 1 ? (ch == 72 ? 0 : (ch == 80 ? 2 : SnackD)) : (ch == 75 ? 3 : (ch == 77 ? 1 : SnackD));
move = false;
}
else if (ch == 122) // 加速移动
MoveSnack(snack);
else if (ch == 32) // 暂停游戏
Pause();
}
}
}
int main()
{
SetConsole("贪吃蛇", 80, 30, "80");
srand((int)time(0));
run();
}
顺着贪吃蛇简易版的思路添加地图很容易实现,但是对一些特殊的字符串的显示,如"Map: 2",其位置需要根据游戏内的效果慢慢调试。值得注意的是贪吃蛇的初始状态需视地图而定,否则会出现游戏刚开始就结束的情况。
下面让我们来分析一下相比于简易版贪吃蛇(C++)它做的一些改动吧。
1. 新添加的属性
新增Speed、MapNum、Score、Map四个属性,Score用来计分,Speed、MapNum分别用于速度的调整和关卡的选择,Map用来存储地图。Speed、Score在每一次重玩时都需要保留上次的设置,所以该全局属性只用在声明的时候"初始化"一次,而其他全局属性需要在每次重新开始游戏时重置其初始值,所以我们把初始化的工作交给了Initialize()函数。
int FoodX, FoodY, SnackDirection, Speed(1), MapNum(1), Score;
bool Dead;
std::string Map;;
2. 打印地图
既然地图是用char数组来存储的,那么我们直接遍历打印即可。但是为什么是从第4行开始打印呢,因为顶部3行要用来显示"速度、关卡数、分数"这三个信息。
SetPos(0, 4);
for (int i = 0; i < 40 * 26; ++i)
std::cout << (Map[i] == '0' ? "■" : " ");
3. 打印预览地图
我们根据关卡数来对Map进行赋值,即缓冲地图文件的内容,接下来调用FillRec()函数来擦除地图中间的那块,在该空白区域中间打印速度和地图的信息。(大致上的意思就是在打印速度和地图信息的区域周围留出一些空白区域,留白之后会界面会更加美观。)
Map = Maps[MapNum - 1];
LogMap(Map);
FillRec(14, 13, 12, 8, " ");
FillStr(18, 15, " Map: " + std::to_string(MapNum));
FillStr(18, 17, "Speed: " + std::to_string(Speed));
4. 选择地图
监听键盘事件,上下键选择速度,左右键选择关卡。LogSelectMap()函数在这里的作用是更新预览地图的信息;最后更新窗口顶部的速度等级和关卡编号。
switch (getch())
{
case 75: // 小键盘左键
MapNum -= MapNum != 1;
break;
case 77: // 小键盘右键
MapNum += MapNum != 6;
break;
case 72: // 小键盘上键
Speed += Speed != 5;
break;
case 80: // 小键盘下键
Speed -= Speed != 1;
break;
}
LogSelectMap();
FillStr(9, 2, ' ' + std::to_string(MapNum));
FillStr(32, 2, std::to_string(Speed));
5. 设置初始属性
根据地图的选择分别为贪吃蛇设置不同的初始属性。
if (MapNum == 5 || MapNum == 6)
SnackD = 0, snack = {{36, 24}, {36, 25}, {36, 26}};
else
SnackD = 1, snack = {{16, 17}, {15, 17}, {14, 17}};
6. 生成食物判断
判断生成的食物是否“在墙上”。
if (Map[FoodX + (FoodY - 4) * 40] == '0')
food_CD = true;
7. 更新游戏分数
贪吃蛇吃到食物,更新分数。
SetPos(20, 2);
std::cout << std::setw(3) << ++Score;
8. 判断贪吃蛇撞墙
判断贪吃蛇是否撞墙,若撞墙则贪吃蛇死亡。
if (Map[snack.front().x + (snack.front().y - 4) * 40] == '0')
Dead = true;
9. 游戏初始化
初始化游戏,主要是上面那个显示关卡编号、分数、速度的3个框。先调用LogSelectMap()打印预览地图(第一次玩打印的是第一个地图,重玩打印上次选择的地图),然后调用SelectMap()让玩家选择地图和速度;最后再执行LogMap()重新打印所选地图的完整地图,即加载玩家选择的地图。
FillStr(6, 1, "==========\t\t==============\t\t===========\n");
FillStr(6, 2, "| Map: " + std::to_string(MapNum) + " |\t\t| SCORE: " + std::to_string(Score) + " |\t\t| SPEED:" + std::to_string(Speed) + " |");
FillStr(6, 3, "==========\t\t==============\t\t===========\n");
LogSelectMap(); // 打印预选地图
SelectMap(); // 选择地图
LogMap(Map); // 打印已选地图
AddFood(); // 添加食物
10. 控制速度
这里稍作修改,使得Speed可以影响速度。
if (t++ > 30 - Speed * 5)
{
MoveSnack(snack);
move = true, t = 0;
}
11. 贪吃蛇显示效果
为了更好的体现死亡的效果,这里对显示贪吃蛇的语句稍作修改。
if (Dead)
for (const SnackSegment &s : snack)
FillStr(s.x, s.y, "x");
else
{
FillStr(snack.front().x, snack.front().y, "□");
FillStr((*++snack.begin()).x, (*++snack.begin()).y, "■");
}
四. 地图文件
先上完整代码,下文稍作解释。
std::string Maps[6] = {
{"0000000000000000000000000000000000000000"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0000000000000000000000000000000000000000"},
{"0000000000000000000000000000000000000000"
"0.................................0....0"
"0.................................0....0"
"0.................................0....0"
"0....0000000000000000000000000....0....0"
"0............................0....0....0"
"0............................0....0....0"
"0............................0....0....0"
"0............................0....0....0"
"0000000000000000000000000....0....0....0"
"0.........0..................0....0....0"
"0.........0..................0....0....0"
"0.........0..................0.........0"
"0.........0..................0.........0"
"0....0....0..................0.........0"
"0....0....0..................0.........0"
"0....0....0....0000000000000000000000000"
"0....0....0............................0"
"0....0....0............................0"
"0....0....0............................0"
"0....0....0............................0"
"0....0....0000000000000000000000000....0"
"0....0.................................0"
"0....0.................................0"
"0....0.................................0"
"0000000000000000000000000000000000000000"},
{"0000000000000000000000000000000000000000"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0......................................0"
"000000000000...0000000000...000000000000"
"0......................................0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0......................................0"
"000000000000...0000000000...000000000000"
"0......................................0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0............0............0............0"
"0000000000000000000000000000000000000000"},
{"0000000000000000000000000000000000000000"
"0......................................0"
"0......................................0"
"0......................................0"
"0...00000000000000....00000000000000...0"
"0...0......................0.......0...0"
"0...0......................0.......0...0"
"0...0......................0.......0...0"
"0...0...0..00000000000000000...0...0...0"
"0...0...0......................0...0...0"
"0...0...0......................0...00000"
"0.......000000000000000000000..0.......0"
"0.......0......................0.......0"
"0.......0......................0.......0"
"0.......0..000000000000000000000.......0"
"0...0...0......................0...0...0"
"0...0...0......................0...0...0"
"0...0...0...00000000000000000..0...0...0"
"0...0.......0......................0...0"
"0...0.......0......................0...0"
"0...0.......0......................0...0"
"000000000000000000....00000000000000...0"
"0......................................0"
"0......................................0"
"0......................................0"
"0000000000000000000000000000000000000000"},
{"0000000000000000000000000000000000000000"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0..........000000000000000000..........0"
"0.....0....0......0..0......0....0.....0"
"0.....0....0......0..0......0....0.....0"
"0.....0....0......0..0......0....0.....0"
"0.....0....0...0..0..0..0...0....0.....0"
"0.....0....0...0..0..0..0...0....0.....0"
"0.....0....0...0..0..0..0...0....0.....0"
"0.....0....0...0..0..0..0...0....0.....0"
"0.....0....0...0..0..0..0...0....0.....0"
"0.....0....0...0..0..0..0...0....0.....0"
"0.....0....0...0..0..0..0...0....0.....0"
"0.....0........0..0..0..0........0.....0"
"0.....0........0..0..0..0........0.....0"
"0.....0........0........0........0.....0"
"0.....0........0........0........0.....0"
"0.....0000000000000000000000000000.....0"
"0......................................0"
"0......................................0"
"0......................................0"
"0......................................0"
"0000000000000000000000000000000000000000"},
{"0000000000000000000000000000000000000000"
"0.......0..............................0"
"0..0..0...0.00000000000000000000000000.0"
"00..0..0..0.0...................0....0.0"
"000..0..0.0.0.00000000000000000.0.0..0.0"
"0..0..0...0.0.0...............0.0.0..0.0"
"0...0..00.0.0.0.0000000000000.0.0.0..0.0"
"0.0..0..0.0.0.0.0.........0.0.0.0.0..0.0"
"0..0..0.0.0.0.0.0.0000000.0.0.0.0.0..0.0"
"00..0...0.0.0.0.0.0.....0.0.0.0.0.0..0.0"
"000..0..0.0.0.0.0.0.000.0.0.0.0.0.0..0.0"
"0..0..000.0.0.0.0.....0.0.0.0.0.0.0..0.0"
"0...0..00.0.0.0.000.0.0.0.0.0.0.0.0..0.0"
"0.0.....0.0.0.0.....0.0.0.0.0.0.0.0..0.0"
"0..0..0.0.0.0.0000000.0.0.0.0.0.0.0..0.0"
"0..0000.0.0.0.........0.0.0.0.0.0.0..0.0"
"00....0.0.0.00000000000.0.0.0.0.0.0..0.0"
"000.0...0.0.............0.0.0.0.0.0..0.0"
"0...00000.000000000000000.0.0.0.0.0..0.0"
"0.000...0.................0.0.0.0.0..0.0"
"0...0.0.0000000000000000000.0.0.0.0..0.0"
"000...0.....................0.0.0.0..0.0"
"0...0000000000000000000000000.0...0..0.0"
"0.000...0...0...0...0...0...0.00000000.0"
"0.....0...0...0...0...0...0............0"
"0000000000000000000000000000000000000000"},
};
用'0'来表示墙体,用'.'来表示空。
五. 游戏效果图
哈哈,第6个地图大概是留给外星人玩的吧。