最近学校的程序设计课要求自己做项目, 我第一个准备做的项目就是贪吃蛇, 也顺便在这里记录下开发过程.

注意: 课程要求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();
    }
}

此作者没有提供个人介绍。
最后更新于 2025-10-11