Hello,大家好,这里是“大千小熊”,一个又会MMD,又会C++的正派角色。Bilibili同步更新,期待你的关注。
程序效果:
开始游戏的欢迎界面:
字符画(1)
开发商的Logo:
字符画(2)
游戏的主界面:
主界面
注意事项:您需要使用“旧版本的控制台”才能真确的进行游戏。
玩法说明:贪吃蛇会随着长度越来越难,蛇的速度会不断的加速。
程序耗时:从0到完整的代码总共耗时为7个小时。
思路分析:
贪吃蛇是一个很经典的游戏,其中算法不难,新手也能轻松做出。
其中这个程序涉及两个问题:(1)Windows控制台编程。(2)C++程序设计。
对于(1)您可以参阅Windows Doc文档。(2)相信您已经有了一定的基础,有基础就够了。
什么是程序的句柄?
在Windows编程中,是Windows用来标识被应用程序所建立或使用的对象的唯一整数,Windows使用各种各样的句柄标识诸如应用程序实例,窗口,控制,位图,GDI对象等等。
进程和线程的概念是什么?
您可以理解为线程是进程的子概念,进程是资源调度的最小单位,而线程是CPU调度的最小单位。
贪吃蛇怎么实现扭来扭曲的效果?
实现的方法有许多,在这里我使用了路劲缓存,将贪吃蛇每一次走过的路线都保存一次,然后在根据长度,还原路劲,把路径输出来。
怎么检测键盘上的按钮被按下来了?
在这里,使用了kbhit()和getch(),来检测按钮是不是被按下。同时,在检测方面您可以设置成为多线程,这样的好处是,可以精确捕捉玩家的按键时刻,手感很好。甚至您还可以改写程序,在另一个线程判断用户按下一个按钮是不是一直按下,以此来判断要不要暂时加速贪吃蛇。
程序开始的字符画是怎么实现的?
在这里,您可以使用freopen来重定向输入输出,将字符画保存在二进制文件中,然后使用getline函数按行读取和输出。当您想要再次重定向到不同的文件,您可以使用cin.clear()来再一次地freopen。
控制台的字符宽度和长度怎么处理?
在控制台中,字符的宽度是长度的二分之一,也就是说,字符比较高和窄,然而比如您现在想填充一个宽字符,比如,您在(0,0)位置插入了一个“■”,很可能将(0,1)的位置也占用了。(注意:(x,y)坐标轴中x向下,y向右。这是我自己在程序中规定的,您可以看gotoPos()为什么x和y是反过来的,因为默认的坐标轴是x向右,y向下。)所以在这里我们规定,我们只使用形如( ,2)( ,4)这样y是偶数的坐标一次来对应。
还有更简单的写法吗?
当然有,如果您感兴趣,您可以百度搜索“最简单的贪吃蛇”,有人用短短的几行代码就实现了全部的功能,非常厉害。
程序的改进空间是什么?
您可以,再次重定向一个新的文本,在里面保存一个最大的分数(蛇的长度),然后每次挑战如果刷新了记录,就把结果保存在这个txt文本中。
您还可以如上面所说的,记录按键按下的时间,如果玩家一直在按同一个方向的按钮,可以让蛇暂时加速,当玩家松开按键,或改变按键,蛇的加速取消。
您还可以设置一个暂停游戏的按钮,或者重新再开始游戏的机制。
怎么控制画布中的光标?
您可以参阅WIndows控制台的文档,里面有详细的函数可以控制光标的位置。
程序的源代码(含详细的注释):
字符画,游戏开始界面(Fre_Welcome.txt):
Loading...
Snack
Program by Bear Qian (MoonPolishLove)
.o ** ...
.=@@@ =@@ .@@ @@@^.
.@@O=@@@ ..*****. =@^ .@@ =o@^.
=@@^O^ O@@@* =@@@@@@^...@@@@@@@@@@^ .@@ .*@@@@@@@@@@@O^
=@@@@ .@@^ =@@@@^ =@^ @@^.=@@ooooooooo. =@@@@@@@@o@O .@O^
\@@[\@@@@@@@@@@@/.[@@^ =@^ @@^=@@` =@^.@@=@^*@O]` .@O^
.[ .[[[[[[[O@@@` =@^ @@/\OO@@@@@@@@^ =@^.@@=@^...@^
..******=@@^***. =@^ @@^. .=@@@. =@^.@@=@^. .@^ =@^
O@@@@@@@@@@@@@@^ =@^ @@^. .@@@ =@@@@@@@^* .@^ =@@@@
O@^. ]` =@^ =@\]]@@^. ,/@/` =@^*@@=@^* .@@@@/[`.
O@^. .@@^ =@^ =@^**@@^. =@@ =O^.@@=@@O .@^
O@^.,/@/`]/]],[` =@^ \@^.,]@` ,@] .@@]/@@`.@^ .]..
,]@@[..[[@@@]. ,[` =@@ =@/ ]/@@@@@[[@@^.@^ ,@O^
.@@@@@* =@@^ .*@@@@@@@@@O *= @@@@@@@@@
.
字符画,程序Logo(Fre_Logo.txt):
Loading...
Snack
Program by Bear Qian (MoonPolishLove)
@@@ o@@@@^ =@@^ @@@* *@@@ ^
@@@ =@@@@@@@@@@@@O^ =@@^ =@@^ @@@ *@@^ @@@@@
@@@ *=@OO* O@@ =@@^ O@@oO@@@@@@^@@@@@^
@@@ O@@ =@@^ O@@@@Oo^ O@@@@^ @@O
=@@@@@@@@@@@@@@@@@@@@ O@@ *=@@^ =@@^ @@O @@@@@@@@@^*@@@@@@@@@
=OOOOOOOO@@@OOOOOOOOO O@@ =@@^ =@@^ =@@^ @@* =@^*@@^
*@@@* =@@@@@@@@@@@@@@@@@@@@@^ @@@ =@@^ =@@^ @@@@@@@@@^*@@^ *=@@@
*=@@@@* *OOoooooooO@@oooooooO^* *@@o =@@^ @@@ @@* =@^*@@@@@@O
=@@^=@@^ O@@ @@@ =@@^ @@@ @@@@@@@@@^*@@@ =@o
*@@* @@@ O@@ =@@^ =@@^ =@@ @@* =@^*@@@oooO@@@
=@@^ @@@^ O@@ =@^ =@@^ ** @@* o@@@^ *@@@@@@@^
=@@@* =@@@ O@@ =@@^ @@* *O^ *@@^ =@^*
=@@@@^ @@@@^ O@@ =@@^ O@@ @@@ *@@^ *@@@
=@@^* =@@@ @@@ =@@@@* =@@^ o@@ @@@ *@@o
*o^
代码的主要实现部分:
虽然看起来很长,但实际内容没有什么复杂的东西。
//文档参阅:https://docs.microsoft.com/zh-cn/windows/console/window-and-screen-buffer-size
//地图的边框(2,0)(2,46)(25,0)(25,46)//第一个是行,我改写的,默认第一个是列
//注意,由于控制台的特性,纵列的宽度是横向的二分之一,所以绘制图像的时候需要从(,0)(,2)这样绘制
//不要从(,1)来绘制图像。
//黑色 = 0 蓝色 = 1 绿色 = 2 湖蓝色 = 3
//红色 = 4 紫色 = 5 黄色 = 6 白色 = 7
//灰色 = 8 淡蓝色=9 淡绿色=A 白色=C
//淡紫色=D 淡黄色=E 亮白色=F system("color 背景颜色文字颜色")
#include <cmath>#include<conio.h>#include<cstdio>#include<ctime>#include<iomanip>//格式化输出程序
#include<iostream>#include<thread>//多线程的程序
#include<vector>#include<windows.h>using namespace std;
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); //全局句柄
CONSOLE_SCREEN_BUFFER_INFO bInfo; // 窗口缓冲区信息
void InitCMD(); //控制台初始化
void gotoPos(SHORT x, SHORT y); //设置光标的位置
void InitMap(); //画边框和信息
void PaintSnack(); //画出Sncak的样子
void MoveSncak(); //移动Snack的头部,并且在队列里面将🐍的位置存储下来
void ShowScore(); //为了设置双重缓冲区,将要改变的东西存放在这个地方
void SetDirection();
void Sweet(); //检查糖果设置
void SnackCheck(int x, int y); //测试(x,y)这个位置是什么情况
typedef pair<int, int>P;
int Speed = 700;
int Map[53][53] = {
0}; // 0可以行走 1不可以走的地方 2是糖果的位置 依旧按照纵坐标为偶数来
class Snack {
public:
Snack(int Len, int _x, int _y) : length(Len), x(_x), y(_y) {
q.push_back(P(x, y));
Map[x][y] = 1;
}
int GetLength() { return length; }
int x;
int y;
vector<pair<int, int>> q;
int length = 0;
int Direction = 0; // 0 上 1 下 2 左 3 右
private:
};
Snack Actor(1, 12, 22);
bool SweetIn = false,
SweetShow = false; //判断糖果是不是在地图之中,是不是闪烁出现过了
int sx = 2, sy = 0; //全局糖果的坐标。
int main() {
srand(time(0));
InitCMD();
InitMap();
thread first(SetDirection); //使用多线程
first.detach(); //变为守护线程
while (1) {
Sleep(10);
Sweet();
ShowScore();
PaintSnack();
gotoPos(0, 0);
// for (int i = 2; i <= 25; i++) {
// for (int j = 0; j <= 46; j++) {
// if (j % 2 == 0)
// cout << setw(2) << Map[i][j];
// }
// cout << endl;
// }
MoveSncak();
}
return 0;
}
void SnackCheck(int x, int y) {
if (Map[x][y] == 2) {
Actor.length += 1;
Map[x][y] = 0;
SweetShow = false;
SweetIn = false;
sx = 2;
sy = 0;
}
if (Map[x][y] == 1) {
gotoPos(12, 10);
SetConsoleTextAttribute(hOut, 0xBC);
cout << "GAME OVER 蛇蛇的长度是" << Actor.length;
Sleep(5000);
exit(0);
}
}
void Sweet() {
if (SweetIn) {
Map[sx][sy] = 2;//虽然存在糖果,但是我为了保险依旧重新设置一遍。
if (SweetShow == false) {
gotoPos(sx, sy);
SetConsoleTextAttribute(hOut, 0xFC);
cout << "■";
SweetShow = true;
} else {
gotoPos(sx, sy);
SetConsoleTextAttribute(hOut, 0xFC);
cout << " ";
SweetShow = false;
}
}
if (!SweetIn) {
while (Map[sx][sy] == 1) {
sx = rand() % 21 + 3;
sy = rand() % 45 + 1;
if (sy % 2 != 0)
sy++;
}
Map[sx][sy] = 2;
gotoPos(sx, sy);
SetConsoleTextAttribute(hOut, 0xFC);
cout << "■";
SweetIn = true;
SweetShow = true;
}
}
void PaintSnack() {
vector<P>::iterator ite;
int pl = 1;
for (ite = Actor.q.end() - 1; pl <= Actor.length; ite--) {
gotoPos((*ite).first, (*ite).second);
pl++;
SetConsoleTextAttribute(hOut, 0xF2);
cout << "■";
Map[(*ite).first][(*ite).second] = 1;
}
if (Actor.q.size() > Actor.length) {
ite = Actor.q.end() - 1 - Actor.length;
gotoPos((*ite).first, (*ite).second);
SetConsoleTextAttribute(hOut, 0xFF);
cout << " ";
Map[(*ite).first][(*ite).second] = 0;
}
}
void MoveSncak() {
if (Speed > 100)
Speed = 700 - pow(1.7, Actor.length);
if (Speed < 70)
Speed = 70;
Sleep(Speed);
switch (Actor.Direction) {
case 0:
SnackCheck(Actor.x - 1, Actor.y); //行走之前先判断能不能走
Actor.q.push_back(P(Actor.x - 1, Actor.y));
Actor.x -= 1; //控制头部的坐标
break;
case 1: //下
SnackCheck(Actor.x + 1, Actor.y);
Actor.q.push_back(P(Actor.x + 1, Actor.y));
Actor.x += 1; //控制头部的坐标
break;
case 2: //左
SnackCheck(Actor.x, Actor.y - 2);
Actor.q.push_back(P(Actor.x, Actor.y - 2));
Actor.y -= 2; //控制头部的坐标
break;
case 3: //右
SnackCheck(Actor.x, Actor.y + 2);
Actor.q.push_back(P(Actor.x, Actor.y + 2));
Actor.y += 2; //控制头部的坐标
break;
case 4:
break;
}
if (Actor.q.size() > 400) {
//为了将过长的路径缓存去除,根据地图不可能长度超过400。
Actor.q.erase(Actor.q.begin());
}
}
void SetDirection() {
while (1) {
Sleep(10);
if (kbhit()) {
switch (getch()) {
case 'w':
case 'W':
case 72:
Actor.Direction = 0;
break;
case 'a':
case 'A':
case 75:
Actor.Direction = 2;
break;
case 'd':
case 'D':
case 77:
Actor.Direction = 3;
break;
case 's':
case 'S':
case 80:
Actor.Direction = 1;
break;
}
}
}
}
void InitCMD() {
system("color 0E");
SetConsoleTitleA("贪吃蛇_By 大千小熊");
COORD dSiz = {202, 100};
SetConsoleScreenBufferSize(hOut, dSiz); //设置窗口缓冲区大小
CONSOLE_CURSOR_INFO _guan_biao = {1, FALSE}; //设置光标大小,隐藏光标
SetConsoleCursorInfo(hOut, &_guan_biao);
SMALL_RECT rc = {0, 0, 90, 90}; //设置窗口位置
SetConsoleWindowInfo(hOut, true, &rc);
system("mode con cols=170 lines=40"); //调整窗口大小
cin.clear();
freopen("Fre_Welcome.txt", "r", stdin);
string Tchar;
while (getline(cin, Tchar)) {
cout << Tchar << endl;
}
cin.clear();
Sleep(2000);
system("cls");
freopen("Fre_Logo.txt", "r", stdin);
while (getline(cin, Tchar)) {
cout << Tchar << endl;
}
Sleep(2000);
// system("color F0");
SetConsoleTextAttribute(hOut, 0xF0);
system("cls");
}
void gotoPos(SHORT y, SHORT x) { //现在gotoPos为(行,列)
COORD pos = {x, y};
SetConsoleCursorPosition(hOut, pos);
}
void InitMap() {
system("mode con cols=130 lines=28"); //调整窗口大小
gotoPos(0, 0);
cout << "贪吃蛇小程序\nPrograme By 大千小熊\n";
//上边框
for (int i = 0; i < 23; i++) {
cout << "●";
Map[2][2 * i] = 1;
}
//左边框
gotoPos(2, 0);
for (int i = 0; i < 23; i++) {
cout << "●" << endl;
Map[2 + i][0] = 1;
}
//右边框
for (int i = 0; i < 23; i++) {
gotoPos(2 + i, 46);
cout << "●";
Map[2 + i][46] = 1;
}
//下边框
gotoPos(25, 0);
for (int i = 0; i < 24; i++) {
cout << "●";
Map[25][2 * i] = 1;
}
//游戏的提示信息
gotoPos(4, 53);
cout << "■游戏说明:";
gotoPos(5, 53);
cout << "通过上下左右按键或者WSAD键盘以来控制移动";
gotoPos(6, 53);
cout << "需要您注意的是:本程序需要使用旧版本的控制台才能正常运行";
gotoPos(7, 53);
cout << "具体的方法是:在程序标题栏右键,勾选旧版本控制台选项";
}
void ShowScore() {
gotoPos(9, 53);
SetConsoleTextAttribute(hOut, 0xF0);
cout << "■当前得分(贪吃蛇的长度):";
gotoPos(10, 53);
SetConsoleTextAttribute(hOut, 0xF0);
cout << setw(4) << Actor.GetLength();
gotoPos(12, 53);
SetConsoleTextAttribute(hOut, 0xF0);
cout << "■当前速度(游戏难度):";
gotoPos(13, 53);
SetConsoleTextAttribute(hOut, 0xF0);
cout << setw(4) << 1000 - Speed;
gotoPos(15, 53);
SetConsoleTextAttribute(hOut, 0xF0);
cout << "■游戏的难度跟蛇的长度为2次函数的关系";