最近学校的程序设计课要求自己做项目, 我第一个准备做的项目就是贪吃蛇, 也顺便在这里记录下开发过程.
注意: 课程要求C89, 我之前cpp写惯了导致还是有一些只满足了c99没满足c89的地方, 在下一个大作业中会改正.
让蛇出现
我们先进行初始化, 也就是先将地图输入进map数组中, 然后对蛇进行随机出生. 然后, 还要写一个刷新屏幕并输出的程序, 以实现监看以及debug. 初始化时还要考虑到蛇移动时如何对蛇胃进行自动删除, 于是我准备对于每个点都赋予一个life, life为0时刷新函数将会将其设为背景.
但是当我写到刷新界面时, 我发现了个问题, 如何异步执行等待用户输入和刷新?我的作业要求是使用C89, 但是线程在很后的c标准才出现(好像是C11?), 于是我咨询了gpt-5, 才了解了还有非阻塞输入这种概念.
(From GPT-5)
Windows 自带的 MSVC / MinGW 编译器通常会附带一个头文件 <conio.h>,里面有两个最重要的函数:
(1) _kbhit()
作用: 检查是否有按键按下。
返回值:
如果有按下的键(在输入缓冲区等待读取)→ 返回一个非零值
如果没有键被按 → 返回 0
特点:
立即返回,不会阻塞程序
检测到的键会留在输入缓冲,等你用 _getch() 取走
(2) _getch()
作用: 从键盘读取一个字符(不需要按回车)
特点:
读取会阻塞,直到有键按下
一旦有按键,就会立刻返回该键(ASCII 码)
不会显示在屏幕上(无回显)
如果输入的是方向键,会返回两个字符——先返回0或224,下一次 _getch() 才返回方向键的实际码
然后, 根据这些介绍, 我们很容易能写出这样一份验证代码:
#include <conio.h>
#include <stdio.h>
#include <windows.h>
int main(void) {
int ch;
while (1) {
if (_kbhit()) { // 1. 先检测有无键按下
ch = _getch(); // 2. 读取按键(不回显)
printf("Key code: %d\n", ch);
// 如果是方向键,要再调用一次 _getch() 获取真正值
if (ch == 0 || ch == 224) {
ch = _getch();
printf("Arrow key code: %d\n", ch);
// 72: 上 80: 下 75: 左 77: 右
}
}
// 3. 模拟游戏刷新逻辑
printf("Snake moves...\n");
Sleep(200); // 延时200毫秒(Game Tick)
}
}
好了, 这就是初始化的代码:
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include<stdlib.h>/*用于清空控制台*/
/*init variable*/
/*mapBoundaryLength是用于数组防止0开头所以加了一 */
#define mapBoundaryLength 31
#define realMapLength 30
int sleepTime;
char map[mapBoundaryLength][mapBoundaryLength];
int index1, index2, snakeLength = 3, snakeL, snakeR, snakeHeading, life[mapBoundaryLength][mapBoundaryLength];/*snakeL -> snakeLine; snakeR -> snakeRow*/
/*init function*/
void printf_yellow(const char *s)
{
printf("\033[0m\033[1;33m%s\033[0m", s);
}
void printf_red(const char *s)
{
printf("\033[0m\033[1;31m%s\033[0m", s);
}
int randomInit() {
srand((unsigned)time(NULL));/*用时间进行伪随机数的播种*/
return rand();
}/*用于返回随机数*/
void randomSet(int LPadding, int RPadding, int todo) {
/*设置padding以防止蛇身和墙初始化时重叠, 也可以在后续程序中调整生成物的范围*/
if(todo == 1) {
/*如果是生成蛇的坐标*/
int randomNum = randomInit();
snakeL = (randomNum % (realMapLength - (LPadding * 2)) + LPadding + 1);
randomNum = randomInit();
snakeR = (randomNum % (realMapLength - (RPadding * 2)) + RPadding + 1);
randomNum = randomInit();
snakeHeading = randomNum % 4 + 1;
}
/*如果要加功能就传入其他的todo参数*/
}
void mapInit() {
/*mapBoundaryInit*/
/*first and last line*/
for(index1 = 1; index1 <= realMapLength; index1++) {
map[1][index1] = '#';
map[realMapLength][index1] = '#';
}
/*other lines*/
for(index1 = 2; index1 < realMapLength; index1++) {
map[index1][1] = '#';
map[index1][realMapLength] = '#';
for(index2 = 2; index2 < realMapLength; index2++) {
map[index1][index2] = ' ';
}
}
/*lifeInit*/
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
life[index1][index2] = 0;
}
}
/*sankeInit*/
randomSet(10, 10, 1);
life[snakeL][snakeR] = 3;
if(snakeHeading == 1) {
/*heading north*/
life[snakeL + 1][snakeR] = 2;
life[snakeL + 2][snakeR] = 1;
} else if(snakeHeading == 2) {
/*heading east*/
life[snakeL][snakeR - 1] = 2;
life[snakeL][snakeR - 2] = 1;
} else if(snakeHeading == 3) {
/*heading south*/
life[snakeL - 1][snakeR] = 2;
life[snakeL - 2][snakeR] = 1;
} else {
/*heading west*/
life[snakeL][snakeR + 1] = 2;
life[snakeL][snakeR + 2] = 1;
}
}
void refresh() {
system("cls");
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
if(life[index1][index2] == 0) {
printf("%c", map[index1][index2]);
} else {
if(index1 == snakeL && index2 == snakeR) {
printf_yellow("@");
} else {
printf_red("+");
}
}
}
printf("\n");
}
}
int main()
{
sleepTime = 1000;/*这里以后可以做难度*/
mapInit();
while(1) {
refresh();
system("pause");
return 0;
}
}
如果成功, 应该是这样的:

让蛇动起来
由于前面我们已经准备使用每个点的“生命”来进行移动, 所以代码就很好写了。只需要每次把长度所对应的生命加在头的最新位置, 然后每个地方的生命减一即可。
我们通过一个move函数来实现这个功能。先对所有生命非零的生命--, 再创建新的蛇头。最后再main函数加一个sleep函数就可以了。比较简单, 代码就不贴了。
让蛇听话
接下来,使用前文提到的非阻塞输入来进行让蛇根据方向键行动。要注意的是, 如果用户输出了不合法的字符, 需要有错误处理(提醒用户)。只需要在refresh末尾加上一个信息检测并打印就可以. 还要注意, 我们不能让蛇原地掉头, 所以还要加上一个判断。接下来, 我还发现如果说在一个sleeptime里面去输入多个输入, 因为getch的输入一次是一个字符, 所以说会堵塞。因此需要用while不断的覆盖到最后一个操作。
以下是main部分的代码:
int main()
{
sleepTime = 1000;/*这里以后可以做难度*/
mapInit();
int inputCheck = 0;
/*debug
messageType = 1;
messageLoop = 3;
infoMes = "DEBUG";*/
while(1) {
inputCheck = 0;
while (_kbhit()) {
chInput = _getch();
inputCheck = 1;
if(chInput == 0 || chInput == 224) chInput = _getch();
}
if(inputCheck){
if(chInput == 72 || chInput == 'w' || chInput == 'W') {
if(snakeHeading != 3) snakeHeading = 1;
else snakeCannotTrunAround();
}
else if(chInput == 77 || chInput == 'd' || chInput == 'D') {
if(snakeHeading != 4) snakeHeading = 2;
else snakeCannotTrunAround();
}
else if(chInput == 80 || chInput == 's' || chInput == 'S') {
if(snakeHeading != 1) snakeHeading = 3;
else snakeCannotTrunAround();
}
else if(chInput == 75 || chInput == 'a' || chInput == 'A') {
if(snakeHeading != 2) snakeHeading = 4;
else snakeCannotTrunAround();
}
else {
messageType = 1;
messageLoop = 5;
infoMes = "Please enter Arrow Key or WASD!";
}
}
snakeMove();
refresh();
Sleep(sleepTime);
if(ifDead() == 1) {
continue;
}
system("pause");
return 0;
}
}
让蛇死
下一步,就是实现蛇的死亡。这时候我发现原来的ifDead()函数有问题, 因为我是用life[][]来存储蛇, 那么我无法判断蛇是否和蛇身相撞, 于是把死亡判断提前到move函数, 全局变量isDead传入参数。这里比较简单, 就不贴代码了。
让蛇吃
接下来就是新建食物, 使用我们最开始就留下的random函数,padding设置为1即可。但是眼尖的可能发现了,这么下去就是如果随机到了蛇身, 我们就要重新随机, 随着蛇越来越长, 时间复杂度越来越高。 于是乎, 我们可以采取类似于C++的vector来实现一个动态数组。当然, 在C里面,我们使用结构体来实现, 单独维护一个freeCount变量存储有效长度。 当我们加入元素时, 加到尾部;当我们pop时, 将末尾元素移动到这里并且count--即可。那问题来了, 蛇头移动后删除free数组时, 怎么知道它的索引呢?遍历吗?不, 只需额外维护一个index数组即可.
这里我们使用foodGenerate函数生成食物.
void foodGenerate() {
int randNumCache = randomInit();
int foodNum = randNumCache % freeCellCount + 1;
foodL = freeCell[foodNum].L;
foodR = freeCell[foodNum].R;
freeCell[foodNum].L = freeCell[freeCellCount].L;
freeCell[foodNum].R = freeCell[freeCellCount].R;
freeCellCount--;
}
食物采取O来表示, 背景色为绿色.
然后就可以处理吃食物的过程了.吃食物后下次刷新不再减少life即可.
最后, 就是分数的显示, 我们只需要在eatFood函数中socre++即可.
最终代码:
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>/*用于清空控制台*/
#include <windows.h>/*用于sleep*/
#include <conio.h>/*用于非堵塞输入*/
#include <stddef.h>/*用于NULL*/
/*init variable*/
/*mapBoundaryLength是用于数组防止0开头所以加了一 */
#define mapBoundaryLength 31
#define realMapLength 30
#define mapLengthSq 900
int sleepTime;
char map[mapBoundaryLength][mapBoundaryLength];
int index1, index2, snakeLength = 3, snakeL, snakeR, snakeHeading, life[mapBoundaryLength][mapBoundaryLength], messageLoop = 0, messageType, chInput, isDead = 0, freeCellCount = 0, foodL, foodR, freeIndex[mapBoundaryLength][mapBoundaryLength], isEatingFood = 0, gameScore = 0;/*snakeL -> snakeLine; snakeR -> snakeRow*/
char *infoMes = NULL;
struct freeCellStruct {
int L;
int R;
} freeCell[mapLengthSq];
/*init function*/
void printf_yellow(const char *s) {
printf("\033[0m\033[1;33m%s\033[0m", s);
}
void printf_red(const char *s) {
printf("\033[0m\033[1;31m%s\033[0m", s);
}
void printf_food(const char *s) {
printf("\033[0m\033[1;35;42m%s\033[0m", s);
}
int randomInit() {
srand((unsigned)time(NULL));/*用时间进行伪随机数的播种*/
return rand();
}/*用于返回随机数*/
void randomSet(int LPadding, int RPadding, int todo) {
/*设置padding以防止蛇身和墙初始化时重叠, 也可以在后续程序中调整生成物的范围*/
if(todo == 1) {
/*如果是生成蛇的坐标*/
int randomNum = randomInit();
snakeL = (randomNum % (realMapLength - (LPadding * 2)) + LPadding + 1);
randomNum = randomInit();
snakeR = (randomNum % (realMapLength - (RPadding * 2)) + RPadding + 1);
randomNum = randomInit();
snakeHeading = randomNum % 4 + 1;
}
/*如果要加功能就传入其他的todo参数*/
}
void foodGenerate();
void mapInit() {
/*mapBoundaryInit*/
/*first and last line*/
for(index1 = 1; index1 <= realMapLength; index1++) {
map[1][index1] = '#';
map[realMapLength][index1] = '#';
}
/*other lines*/
for(index1 = 2; index1 < realMapLength; index1++) {
map[index1][1] = '#';
map[index1][realMapLength] = '#';
for(index2 = 2; index2 < realMapLength; index2++) {
map[index1][index2] = ' ';
}
}
/*lifeInit*/
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
life[index1][index2] = 0;
}
}
/*sankeInit*/
randomSet(10, 10, 1);
life[snakeL][snakeR] = 3;
if(snakeHeading == 1) {
/*heading north*/
life[snakeL + 1][snakeR] = 2;
life[snakeL + 2][snakeR] = 1;
} else if(snakeHeading == 2) {
/*heading east*/
life[snakeL][snakeR - 1] = 2;
life[snakeL][snakeR - 2] = 1;
} else if(snakeHeading == 3) {
/*heading south*/
life[snakeL - 1][snakeR] = 2;
life[snakeL - 2][snakeR] = 1;
} else {
/*heading west*/
life[snakeL][snakeR + 1] = 2;
life[snakeL][snakeR + 2] = 1;
}
/*freeCell init*/
for(index1 = 2; index1 < realMapLength; index1++) {
for(index2 = 2; index2 < realMapLength; index2++) {
if(life[index1][index2] != 0) continue;
freeCellCount++;
freeCell[freeCellCount].L = index1;
freeCell[freeCellCount].R = index2;
freeIndex[index1][index2] = freeCellCount;
}
}
foodGenerate();
}
void foodGenerate() {
int randNumCache = randomInit();
int foodNum = (randNumCache % freeCellCount) + 1;
foodL = freeCell[foodNum].L;
foodR = freeCell[foodNum].R;
freeCell[foodNum].L = freeCell[freeCellCount].L;
freeCell[foodNum].R = freeCell[freeCellCount].R;
freeCellCount--;
}
void eatFood() {
snakeLength++;
isEatingFood = 1;
gameScore++;
foodGenerate();
}
void refresh() {
system("cls");
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
if(life[index1][index2] == 0) {
if(index1 == foodL && index2 == foodR) {
printf_food("O");
continue;
}
printf("%c", map[index1][index2]);
} else {
if(index1 == snakeL && index2 == snakeR) {
printf_yellow("@");
} else {
printf_red("+");
}
}
}
printf("\n");
}
printf("\n");
printf_yellow("Game Score:");
printf("\033[0m\033[1;33m%d\033[0m", gameScore);
if(messageLoop != 0) {
/*如果有消息要展示*/
printf("\n");
if(messageType == 1) {
printf_red(infoMes);
}
messageLoop--;
}
}
void snakeMove() {
/*先进行生命减一*/
/*循环中顺带确定蛇尾位置*/
int tailL, tailR;
if(!isEatingFood) {
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
if(life[index1][index2] == 1) {tailL = index1; tailR = index2;}
if(life[index1][index2] != 0) life[index1][index2]--;
}
}
}
/*再更改蛇头*/
if(snakeHeading == 1) {
snakeL--;
} else if(snakeHeading == 2) {
snakeR++;
} else if(snakeHeading == 3) {
snakeL++;
} else {
snakeR--;
}
if(!isEatingFood) {
freeIndex[tailL][tailR] = freeIndex[snakeL][snakeR];
freeCell[freeIndex[tailL][tailR]].L = tailL;
freeCell[freeIndex[tailL][tailR]].R = tailR;/*实现动态数组 */
}
isEatingFood = 0;
if(life[snakeL][snakeR] != 0 || map[snakeL][snakeR] == '#') isDead = 1;
life[snakeL][snakeR] = snakeLength;
if(snakeL == foodL && snakeR == foodR) {
eatFood();
}
}
void snakeCannotTrunAround() {
messageType = 1;
messageLoop = 5;
infoMes = "The snake cannot turn back!";
}
int main()
{
sleepTime = 300;/*这里以后可以做难度*/
mapInit();
int inputCheck = 0;
/*debug
messageType = 1;
messageLoop = 3;
infoMes = "DEBUG";*/
while(1) {
inputCheck = 0;
while (_kbhit()) {
chInput = _getch();
inputCheck = 1;
if(chInput == 0 || chInput == 224) chInput = _getch();
}
if(inputCheck){
if(chInput == 72 || chInput == 'w' || chInput == 'W') {
if(snakeHeading != 3) snakeHeading = 1;
else snakeCannotTrunAround();
}
else if(chInput == 77 || chInput == 'd' || chInput == 'D') {
if(snakeHeading != 4) snakeHeading = 2;
else snakeCannotTrunAround();
}
else if(chInput == 80 || chInput == 's' || chInput == 'S') {
if(snakeHeading != 1) snakeHeading = 3;
else snakeCannotTrunAround();
}
else if(chInput == 75 || chInput == 'a' || chInput == 'A') {
if(snakeHeading != 2) snakeHeading = 4;
else snakeCannotTrunAround();
}
else {
messageType = 1;
messageLoop = 5;
infoMes = "Please enter Arrow Key or WASD!";
}
}
snakeMove();
refresh();
Sleep(sleepTime);
if(!isDead) {
continue;
}
printf("\n");
printf_yellow("You Are DEAD!");
system("pause");
return 0;
}
}

优化
当然, 现在这游戏只是能玩了, 但是还有很多优化可以做.
随机数优化
如果说你玩了上面代码, 你会发现食物生成永远都是从上到下. 这是因为我使用时间作为随机种子, 时间是递增的.
优化方法: 只做一次种.
屏幕频闪优化
我使用system("cls")来刷新, 导致一闪一闪的. 可以使用gotoxy(0, 0);来直接重新绘制, 就不会一闪一闪的.
gotoxy代码如下:
void gotoxy(int x, int y) {
COORD pos;
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
其中GetStdHandle(STD_OUTPUT_HANDLE)是获取输出窗口的句柄, 来自于windows.h(所以这个函数强相关于Windows系统), SetConsoleCursorPosition就是更改光标位置.
这样就能减少很多频闪了. 我们可以隐藏掉光标实现不会有小的闪动。
void HideCursor()
{
CONSOLE_CURSOR_INFO cursor_info = {1, 0}; /* 第二个值为0表示隐藏光标*/
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}
增加难度选择
只需要在开头添加难度选择即可.
int sleepTimeInput;
printf("Please input difficulty: 1~10\n");
scanf("%d", &sleepTimeInput);
sleepTime = (10 - sleepTimeInput) * 100 + 50;
system("cls");
增加空格暂停
很基础的需求. 我们只需要在检测输入里多一行就行.
检测输入改成以下即可:
while(_kbhit()) {
chInput = _getch();
if(chInput == ' ') {
printf_yellow("\nPAUSED, type SPACE to continue.");
while(1) {
chInput = _getch();
if(chInput == ' ') break;
}
system("cls");
continue;
}
inputCheck = 1;
if(chInput == 0 || chInput == 224) chInput = _getch();
}
菜单SETTINGS
本来想做个最好分数,发现要写文件进行文件管理,那就干脆写个配置文件,然后再给个菜单吧。
那么首先,我们要先重新绘制开始菜单。这个根据自己喜好绘制就可以,我的想法是要读取键盘方向键输入来实现选取,用绿色来实现高亮。我们while(1)来获取方向键得输入, 并在每次输入后重新绘制控制台以实现更换选择。
接下来,就是使用fopen来进行读取设置. 因为我们这不是什么大的项目,所以不需要搞什么json文件或者格式,直接一个txt里面按顺序写入数字就行。
部分代码如下
struct SettingsStruct {
int difficulty;
int bestScores[11];
}settings;
void settingFileWrite() {
FILE *file;
file = fopen("settings.txt", "w");
if(file == NULL) {
fclose(file);
printf_red("\nFail to write setting file! The game will exit at next step!\n");
system("pause");
system("exit()");
}
fputs("", file); /*clear file*/
fprintf(file, "%d", settings.difficulty);
for(index1 = 1; index1 <= 10; index1++) {
if(settings.bestScores[index1] >= 100) fprintf(file, "%d", settings.bestScores[index1]);
else if(settings.bestScores[index1] >= 10) fprintf(file, "0%d", settings.bestScores[index1]);
else fprintf(file, "00%d", settings.bestScores[index1]);
}
fclose(file);
}
void settingFileOpen() {
FILE *file;
file = fopen("settings.txt", "r");
if(file == NULL) {
/*fail to open file*/
fclose(file);
settings.difficulty = 5;
for(index1 = 1; index1 <= 10; index1++) {
settings.bestScores[index1] = 0;
}
settingFileWrite();
return;
}
settings.difficulty = fgetc(file) - '0';
int numCache1, numCache2, numCache3;
for(index1 = 1; index1 <= 10; index1++) {
numCache1 = fgetc(file) - '0';
numCache2 = fgetc(file) - '0';
numCache3 = fgetc(file) - '0';
settings.bestScores[index1] = numCache1 * 100 + numCache2 * 10 + numCache3;
}
fclose(file);
}
void rankUpdate(int rankIndex, int rank) {
int rankNext = settings.bestScores[rankIndex];
settings.bestScores[rankIndex] = rank;
if(rankIndex != 10) {
rankUpdate(rankIndex + 1, rankNext);
}
}
int main()
{
/*sleepTime = 300;这里以后可以做难度*/
int sleepTimeInput;
menuInit();
/*read seetings*/
settingFileOpen();
while(1) {
if(menuInput == 1) {
/*game start*/
break;
} else if(menuInput == 2) {
system("cls");
printf("Please input difficulty: 1 to 9\n\033[0m\033[1;32mcurrent difficulty: %d\033[0m\n", settings.difficulty);
scanf("%d", &settings.difficulty);
settingFileWrite();
menuInput = 0;
menuInit();
} else if(menuInput == 3) {
system("cls");
for(index1 = 1; index1 <= 10; index1++) {
printf("#%d: %d;\n", index1, settings.bestScores[index1]);
}
menuInput = 0;
system("pause");
menuInit();
} else if(menuInput == 4) {
exit(0);
}
if(_kbhit) {
chInput = _getch();
if(chInput == 0 || chInput == 224) {
chInput = _getch();
if(chInput == 72) {
if(menuHover == 1) continue;
menuHover--;
menuInit();
} else if(chInput == 80) {
if(menuHover == menuNum) continue;
menuHover++;
menuInit();
} else {
printf_red("\nUSE UP Arrow Key or DOWN Arrow Key to select");
}
} else if(chInput == 13) {
menuInput = menuHover;
continue;
} else {
printf_red("\nUSE Arrow Key to select, use ENTER to continue");
}
}
}
sleepTime = (9 - settings.difficulty) * 100 + 50;
system("cls");
printf("Guides:\nUse SPACE to pause(also can be used to refresh terminal)\nUse Arrow Key or WASD to change heading\n");
srand((unsigned)time(NULL));/*用时间进行伪随机数的播种*/
system("pause");
system("cls");
mapInit();
int inputCheck = 0;
while(1) {
inputCheck = 0;
while(_kbhit()) {
chInput = _getch();
if(chInput == ' ') {
printf_yellow("\nPAUSED, type SPACE to continue.");
while(1) {
chInput = _getch();
if(chInput == ' ') break;
}
system("cls");
continue;
}
inputCheck = 1;
if(chInput == 0 || chInput == 224) chInput = _getch();
}
if(inputCheck){
if(chInput == 72 || chInput == 'w' || chInput == 'W') {
if(snakeHeading != 3) snakeHeading = 1;
else snakeCannotTrunAround();
}
else if(chInput == 77 || chInput == 'd' || chInput == 'D') {
if(snakeHeading != 4) snakeHeading = 2;
else snakeCannotTrunAround();
}
else if(chInput == 80 || chInput == 's' || chInput == 'S') {
if(snakeHeading != 1) snakeHeading = 3;
else snakeCannotTrunAround();
}
else if(chInput == 75 || chInput == 'a' || chInput == 'A') {
if(snakeHeading != 2) snakeHeading = 4;
else snakeCannotTrunAround();
}
else {
messageType = 1;
messageLoop = 5;
infoMes = "Please enter Arrow Key or WASD!";
}
}
snakeMove();
refresh();
Sleep(sleepTime);
if(!isDead) {
continue;
}
/*bestScores*/
int rankNow = 0;
for(index1 = 1; index1 <= 10; index1++) {
if(gameScore > settings.bestScores[index1]){
rankNow = index1;
break;
}
}
if(rankNow != 0) rankUpdate(rankNow, gameScore);
settingFileWrite();
printf("\n");
printf_yellow("You Are DEAD!");
system("pause");
return 0;
}
}
结束后不直接退出
每次游戏结束后都会退出,感觉没有连续感,如果说按照游戏来说,会导致留存率不高。我们只需要把main函数整合成一个函数,然后对于全局变量重新自定义就可以。
奖励方块
玩过诺基亚里面的贪吃蛇的都知道除了基础的方块,还有一个奖励方块,每当分数达到一定量就会生成,按照你吃到的时间距离生成时间的差来增加分数,并且不增加长度。这个奖励方块将贪吃蛇分数大大增加了随机性,使得贪吃蛇更好玩。
具体实现,我们需要在刷新时增加一个生成奖励方块的判定函数,并将奖励方块写入刷新函数和判定函数中。我们采取现实时间作为额外分的计算依据,并若5秒钟内没吃到就消失,这样能够增加玩家选择高难度的概率,能降低选择低难度刷分的概率,并且提高游戏上限。
p.s. 我写到这才发现我把row的含义记错了,把row改成了column,即R->C,此改动不再阐述。
void rewardGenerate() {
isRewardExisting = 1;
int randNumCache = randomInit();
int rewardNum = (randNumCache % freeCellCount) + 1;
rewardL = freeCell[rewardNum].L;
rewardC = freeCell[rewardNum].C;
freeCell[rewardNum].L = freeCell[freeCellCount].L;
freeCell[rewardNum].C = freeCell[freeCellCount].C;
freeCellCount--;
}
五秒钟计时中使用真实时间作为中转,获得时间使用time函数.
void eatReward() {
isRewardExisting = 0;
gameScore += (rewardLife * 3) + 1;
rewardLife = -1;
freeCellCount++;
}
void printf_reward() {
if(rewardLife == -1) {
rewardTime = time(NULL);
}
time_t timeCache = time(NULL);
rewardLife = (int)(8 - (timeCache - rewardTime));
printf("\033[0m\033[1;33;41m%d\033[0m", rewardLife);
if(rewardLife == 0) {
isRewardExisting = 0;
rewardLife = -1;
}
}
然后, 如果当我们在有奖励方块时, 暂停了, 就会发现一个bug:暂停时奖励方块的时间仍在流逝. 解决办法: 在暂停后输入空格继续时(break), 重新更新时间:
if(rewardLife != -1) {
time_t timeCache = time(NULL);
rewardTime = timeCache - (8 - rewardLife);
}
ascii图画
在菜单加上, 让其更美观:
____ _ _ _ _ __ _____
/ ___| | \ | | / \ | |/ / | ____|
\___ \ | \| | / _ \ | ' / | _|
___) | | |\ | / ___ \ | . \ | |___
|____/ |_| \_| /_/ \_\ |_|\_\ |_____|
最终代码(优化版)
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>/*用于清空控制台*/
#include <windows.h>/*用于sleep*/
#include <conio.h>/*用于非堵塞输入*/
#include <stddef.h>/*用于NULL*/
/*init variable*/
/*mapBoundaryLength是用于数组防止0开头所以加了一 */
#define mapBoundaryLength 31
#define realMapLength 30
#define menuWidth 42
#define mapLengthSq 900
int sleepTime;
char map[mapBoundaryLength][mapBoundaryLength];
int index1, index2, snakeLength, snakeL, snakeC, snakeHeading, life[mapBoundaryLength][mapBoundaryLength], messageLoop, messageType, chInput, isDead, freeCellCount, foodL, foodC, freeIndex[mapBoundaryLength][mapBoundaryLength], isEatingFood, gameScore, rewardL, rewardC, isRewardExisting, rewardLife;/*snakeL -> snakeLine; snakeC -> snakeColumn;etc.;*/
time_t rewardTime;
char *infoMes = NULL;
struct freeCellStruct {
int L;
int C;
} freeCell[mapLengthSq];
/*init function*/
void printf_yellow(const char *s) {
printf("\033[0m\033[1;33m%s\033[0m", s);
}
void printf_red(const char *s) {
printf("\033[0m\033[1;31m%s\033[0m", s);
}
void printf_food(const char *s) {
printf("\033[0m\033[1;35;42m%s\033[0m", s);
}
void printf_green(const char *s) {
printf("\033[0m\033[1;32m%s\033[0m", s);
}
void printf_reward() {
if(rewardLife == -1) {
rewardTime = time(NULL);
}
time_t timeCache = time(NULL);
rewardLife = (int)(8 - (timeCache - rewardTime));
printf("\033[0m\033[1;33;41m%d\033[0m", rewardLife);
if(rewardLife == 0) {
isRewardExisting = 0;
rewardLife = -1;
}
}
void gotoxy(int x, int y) {
COORD pos;
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
void HideCursor()
{
CONSOLE_CURSOR_INFO cursor_info = {1, 0}; /* 第二个值为0表示隐藏光标*/
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}
int randomInit() {
return rand();
}/*用于返回随机数*/
void randomSet(int LPadding, int CPadding, int todo) {
/*设置padding以防止蛇身和墙初始化时重叠, 也可以在后续程序中调整生成物的范围*/
if(todo == 1) {
/*如果是生成蛇的坐标*/
int randomNum = randomInit();
snakeL = (randomNum % (realMapLength - (LPadding * 2)) + LPadding + 1);
randomNum = randomInit();
snakeC = (randomNum % (realMapLength - (CPadding * 2)) + CPadding + 1);
randomNum = randomInit();
snakeHeading = randomNum % 4 + 1;
}
/*如果要加功能就传入其他的todo参数*/
}
void foodGenerate();
void mapInit() {
/*mapBoundaryInit*/
/*first and last line*/
for(index1 = 1; index1 <= realMapLength; index1++) {
map[1][index1] = '#';
map[realMapLength][index1] = '#';
}
/*other lines*/
for(index1 = 2; index1 < realMapLength; index1++) {
map[index1][1] = '#';
map[index1][realMapLength] = '#';
for(index2 = 2; index2 < realMapLength; index2++) {
map[index1][index2] = ' ';
}
}
/*lifeInit*/
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
life[index1][index2] = 0;
}
}
/*sankeInit*/
randomSet(10, 10, 1);
life[snakeL][snakeC] = 3;
if(snakeHeading == 1) {
/*heading north*/
life[snakeL + 1][snakeC] = 2;
life[snakeL + 2][snakeC] = 1;
} else if(snakeHeading == 2) {
/*heading east*/
life[snakeL][snakeC - 1] = 2;
life[snakeL][snakeC - 2] = 1;
} else if(snakeHeading == 3) {
/*heading south*/
life[snakeL - 1][snakeC] = 2;
life[snakeL - 2][snakeC] = 1;
} else {
/*heading west*/
life[snakeL][snakeC + 1] = 2;
life[snakeL][snakeC + 2] = 1;
}
/*freeCell init*/
for(index1 = 2; index1 < realMapLength; index1++) {
for(index2 = 2; index2 < realMapLength; index2++) {
if(life[index1][index2] != 0) continue;
freeCellCount++;
freeCell[freeCellCount].L = index1;
freeCell[freeCellCount].C = index2;
freeIndex[index1][index2] = freeCellCount;
}
}
foodGenerate();
}
void foodGenerate() {
int randNumCache = randomInit();
int foodNum = (randNumCache % freeCellCount) + 1;
foodL = freeCell[foodNum].L;
foodC = freeCell[foodNum].C;
freeCell[foodNum].L = freeCell[freeCellCount].L;
freeCell[foodNum].C = freeCell[freeCellCount].C;
freeCellCount--;
}
void rewardGenerate() {
isRewardExisting = 1;
int randNumCache = randomInit();
int rewardNum = (randNumCache % freeCellCount) + 1;
rewardL = freeCell[rewardNum].L;
rewardC = freeCell[rewardNum].C;
freeCell[rewardNum].L = freeCell[freeCellCount].L;
freeCell[rewardNum].C = freeCell[freeCellCount].C;
freeCellCount--;
}
void eatFood() {
snakeLength++;
isEatingFood = 1;
gameScore++;
if(gameScore % 8 == 0 && isRewardExisting == 0) {
rewardGenerate();
}
foodGenerate();
}
void eatReward() {
isRewardExisting = 0;
gameScore += (rewardLife * 3) + 1;
rewardLife = -1;
freeCellCount++;
}
void refresh() {
gotoxy(0, 0);
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
if(life[index1][index2] == 0) {
if(index1 == foodL && index2 == foodC) {
printf_food("O");
continue;
} else if(index1 == rewardL && index2 == rewardC && isRewardExisting == 1) {
printf_reward();
continue;
}
printf("%c", map[index1][index2]);
} else {
if(index1 == snakeL && index2 == snakeC) {
printf_yellow("@");
} else {
printf_red("+");
}
}
}
printf("\n");
}
printf("\n");
printf_yellow("Game Score:");
printf("\033[0m\033[1;33m%d\033[0m", gameScore);
if(messageLoop != 0) {
/*如果有消息要展示*/
printf("\n");
if(messageType == 1) {
printf_red(infoMes);
}
messageLoop--;
}
}
void snakeMove() {
/*先进行生命减一*/
/*循环中顺带确定蛇尾位置*/
int tailL, tailC;
if(!isEatingFood) {
for(index1 = 1; index1 <= realMapLength; index1++) {
for(index2 = 1; index2 <= realMapLength; index2++) {
if(life[index1][index2] == 1) {tailL = index1; tailC = index2;}
if(life[index1][index2] != 0) life[index1][index2]--;
}
}
}
/*再更改蛇头*/
if(snakeHeading == 1) {
snakeL--;
} else if(snakeHeading == 2) {
snakeC++;
} else if(snakeHeading == 3) {
snakeL++;
} else {
snakeC--;
}
if(!isEatingFood) {
freeIndex[tailL][tailC] = freeIndex[snakeL][snakeC];
freeCell[freeIndex[tailL][tailC]].L = tailL;
freeCell[freeIndex[tailL][tailC]].C = tailC;/*实现动态数组 */
}
isEatingFood = 0;
if(life[snakeL][snakeC] != 0 || map[snakeL][snakeC] == '#') isDead = 1;
life[snakeL][snakeC] = snakeLength;
if(snakeL == foodL && snakeC == foodC) {
eatFood();
} else if(snakeL == rewardL && snakeC == rewardC) {
eatReward();
}
}
void snakeCannotTrunAround() {
messageType = 1;
messageLoop = 5;
infoMes = "The snake cannot turn back!";
}
int menuHover;/*当前选取的*/
int menuNum;/*menu数量,开始游戏,菜单(目前就难度),排行榜, 退出*/
int menuInput;
void menuInit() {
gotoxy(0,0);
for(index1 = 1; index1 <= menuWidth; index1++) printf("#");
printf("\n");
for(index1 = 1; index1 <= 6; index1++) {
printf("#");
for(index2 = 2; index2 < menuWidth; index2++) printf(" ");
printf("#\n");
}
printf("#\033[0m\033[1;33m ____ _ _ _ _ __ _____ \033[0m#\n");
printf("#\033[0m\033[1;33m/ ___| | \\ | | / \\ | |/ / | ____|\033[0m#\n");
printf("#\033[0m\033[1;33m\\___ \\ | \\| | / _ \\ | ' / | _| \033[0m#\n");
printf("#\033[0m\033[1;33m ___) | | |\\ | / ___ \\ | . \\ | |___ \033[0m#\n");
printf("#\033[0m\033[1;33m|____/ |_| \\_| /_/ \\_\\ |_|\\_\\ |_____|\033[0m#\n");
for(index1 = 1; index1 <= 3; index1++) {
printf("#");
for(index2 = 2; index2 < menuWidth; index2++) printf(" ");
printf("#\n");
}
printf("# ");
if(menuHover == 1) printf_green("Start");
else printf("Start");
printf(" #\n");
printf("# ");
if(menuHover == 2) printf_green("Settings");
else printf("Settings");
printf(" #\n");
printf("# ");
if(menuHover == 3) printf_green("Highest Scores");
else printf("Highest Scores");
printf(" #\n");
printf("# ");
if(menuHover == 4) printf_green("Exit");
else printf("Exit");
printf(" #\n");
for(index1 = 1; index1 <= 6; index1++) {
printf("#");
for(index2 = 2; index2 < menuWidth; index2++) {
printf(" ");
}
printf("#\n");
}
for(index1 = 1; index1 <= menuWidth; index1++) {
printf("#");
}
printf_yellow("\nUse Arrow Key to select.");
}
struct SettingsStruct {
int difficulty;
int bestScores[11];
}settings;
void settingFileWrite() {
FILE *file;
file = fopen("settings.txt", "w");
if(file == NULL) {
fclose(file);
printf_red("\nFail to write setting file! The game will exit at next step!\n");
system("pause");
system("exit()");
}
fputs("", file); /*clear file*/
fprintf(file, "%d", settings.difficulty);
for(index1 = 1; index1 <= 10; index1++) {
if(settings.bestScores[index1] >= 100) fprintf(file, "%d", settings.bestScores[index1]);
else if(settings.bestScores[index1] >= 10) fprintf(file, "0%d", settings.bestScores[index1]);
else fprintf(file, "00%d", settings.bestScores[index1]);
}
fclose(file);
}
void settingFileOpen() {
FILE *file;
file = fopen("settings.txt", "r");
if(file == NULL) {
/*fail to open file*/
fclose(file);
settings.difficulty = 5;
for(index1 = 1; index1 <= 10; index1++) {
settings.bestScores[index1] = 0;
}
settingFileWrite();
return;
}
settings.difficulty = fgetc(file) - '0';
int numCache1, numCache2, numCache3;
for(index1 = 1; index1 <= 10; index1++) {
numCache1 = fgetc(file) - '0';
numCache2 = fgetc(file) - '0';
numCache3 = fgetc(file) - '0';
settings.bestScores[index1] = numCache1 * 100 + numCache2 * 10 + numCache3;
}
fclose(file);
}
void rankUpdate(int rankIndex, int rank) {
int rankNext = settings.bestScores[rankIndex];
settings.bestScores[rankIndex] = rank;
if(rankIndex != 10) {
rankUpdate(rankIndex + 1, rankNext);
}
}
void varietyInit(){
snakeLength = 3;
messageLoop = 0;
isDead = 0;
freeCellCount = 0;
isEatingFood = 0;
gameScore = 0;
menuHover = 1;/*当前选取的*/
menuNum = 4;/*menu数量,开始游戏,菜单(目前就难度),排行榜, 退出*/
menuInput = 0;
isRewardExisting = 0;
rewardLife = -1;
}
void mainBody() {
varietyInit();
/*sleepTime = 300;这里以后可以做难度*/
int sleepTimeInput;
menuInit();
/*read seetings*/
settingFileOpen();
while(1) {
if(menuInput == 1) {
/*game start*/
break;
} else if(menuInput == 2) {
system("cls");
printf("Please input difficulty: 1 to 9\n\033[0m\033[1;32mcurrent difficulty: %d\033[0m\n", settings.difficulty);
scanf("%d", &settings.difficulty);
settingFileWrite();
menuInput = 0;
system("cls");
menuInit();
} else if(menuInput == 3) {
system("cls");
for(index1 = 1; index1 <= 10; index1++) {
printf("#%d: %d;\n", index1, settings.bestScores[index1]);
}
menuInput = 0;
system("pause");
system("cls");
menuInit();
} else if(menuInput == 4) {
exit(0);
}
if(_kbhit) {
chInput = _getch();
if(chInput == 0 || chInput == 224) {
chInput = _getch();
if(chInput == 72) {
if(menuHover == 1) continue;
menuHover--;
menuInit();
} else if(chInput == 80) {
if(menuHover == menuNum) continue;
menuHover++;
menuInit();
} else {
printf_red("\nUSE UP Arrow Key or DOWN Arrow Key to select");
}
} else if(chInput == 13) {
menuInput = menuHover;
continue;
} else {
printf_red("\nUSE Arrow Key to select, use ENTER to continue");
}
}
}
sleepTime = (9 - settings.difficulty) * 100 + 50;
system("cls");
printf("Guides:\nUse SPACE to pause(also can be used to refresh terminal)\nUse Arrow Key or WASD to change heading\n\033[0m\033[1;33mNote:every time the score reaches the multiplies of 8, there will be a reward!\033[0m\n");
srand((unsigned)time(NULL));/*用时间进行伪随机数的播种*/
system("pause");
system("cls");
mapInit();
int inputCheck = 0;
while(1) {
inputCheck = 0;
while(_kbhit()) {
chInput = _getch();
if(chInput == ' ') {
printf_yellow("\nPAUSED, type SPACE to continue.");
while(1) {
chInput = _getch();
if(chInput == ' ') break;
}
if(rewardLife != -1) {
time_t timeCache = time(NULL);
rewardTime = timeCache - (8 - rewardLife);
}
system("cls");
continue;
}
inputCheck = 1;
if(chInput == 0 || chInput == 224) chInput = _getch();
}
if(inputCheck){
if(chInput == 72 || chInput == 'w' || chInput == 'W') {
if(snakeHeading != 3) snakeHeading = 1;
else snakeCannotTrunAround();
}
else if(chInput == 77 || chInput == 'd' || chInput == 'D') {
if(snakeHeading != 4) snakeHeading = 2;
else snakeCannotTrunAround();
}
else if(chInput == 80 || chInput == 's' || chInput == 'S') {
if(snakeHeading != 1) snakeHeading = 3;
else snakeCannotTrunAround();
}
else if(chInput == 75 || chInput == 'a' || chInput == 'A') {
if(snakeHeading != 2) snakeHeading = 4;
else snakeCannotTrunAround();
}
else {
messageType = 1;
messageLoop = 5;
infoMes = "Please enter Arrow Key or WASD!";
}
}
snakeMove();
refresh();
Sleep(sleepTime);
if(!isDead) {
continue;
}
/*bestScores*/
int rankNow = 0;
for(index1 = 1; index1 <= 10; index1++) {
if(gameScore > settings.bestScores[index1]){
rankNow = index1;
break;
}
}
if(rankNow != 0) rankUpdate(rankNow, gameScore);
settingFileWrite();
printf("\n");
printf_yellow("You Are DEAD!");
system("pause");
return;
}
}
void main()
{
HideCursor();
while(1) {
system("cls");
mainBody();
}
}

Comments NOTHING