国庆期间看了raylib绘图库,打算用这个库写几个复古小游戏,这是第一个贪吃蛇。

代码如下:

#include <raylib.h>
#include <raymath.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define GAME_SITE "https://raveh.net"
#define GAME_NAME "Snake"
#define GAME_VERSION "ver:0.1.γ"
#define GAME_RELEASE "Release:2022/10/12"

// 方向
typedef enum DIRECTION {
    DIR_UP = 0,
    DIR_LEFT,
    DIR_DOWN,
    DIR_RIGHT,
} DIRECTION;

// 状态
typedef enum STATE {
    STATE_RUNNING = 1,
    STATE_PAUSE,
    STATE_SUCCESS,
    STATE_FAIL,
} STATE;

/**
 * 贪吃蛇
 */
class Snake {
private:
    const static int WIN_WIDTH = 1280, WIN_HEIGHT = 800;   // 窗口大小
    const static int COLS_NUM = 70, ROWS_NUM = 48;         // 行列
    const static int BORDER = 10;                          // 边框
    const static int CELL_SIZE = 16;                       // 单元格大小
    const static int MAX_LENGTH = 256;                     // 蛇身最大长度
    
    const char *DIR_DESC[4] = {"上", "左", "下", "右"};    // 方向描述
    
    int speed = 1;                                         // 速度 (1-10)
    int length = 3;                                        // 长度
    int level = 1;                                         // 等级
    Vector2 snake[MAX_LENGTH];                             // 贪吃蛇 位置数组
    Vector2 apple;                                         // 苹果 位置
    DIRECTION dir = DIR_UP;                                // 默认方向
    STATE running = STATE_RUNNING;                         // 运行状态;
    
    // 读取字体
    unsigned int fileSize;
    unsigned char *fontFileData = LoadFileData("assets/fonts/unifont-15.0.01.ttf", &fileSize);
    // 将字符串中的字符逐一转换成Unicode码点,得到码点表
    int codepointsCount;
    int *codepoints = LoadCodepoints("贪吃蛇长速度等级方向信息任务成功失败暂停重开退出上下左右帮助αβγ-/:.,0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", &codepointsCount);
    
    /**
     * @brief 监听键盘事件
     */
    void ListenEvent() {
        if (running == STATE_SUCCESS || running == STATE_FAIL) {
            if (IsKeyPressed(KEY_R)) {
                InitData();
                return;
            }
        } else {
            if (IsKeyPressed(KEY_P) || IsKeyPressed(KEY_SPACE) || IsKeyPressed(KEY_ENTER)) {
                running = running == STATE_PAUSE ? STATE_RUNNING : STATE_PAUSE;
                return;
            }
        }
        if (running == STATE_RUNNING) {
            if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) {
                if (dir == DIR_RIGHT) {
                    return;
                }
                dir = DIR_LEFT;
                return;
            }
            if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) {
                if (dir == DIR_UP) {
                    return;
                }
                dir = DIR_DOWN;
                return;
            }
            if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) {
                if (dir == DIR_LEFT) {
                    return;
                }
                dir = DIR_RIGHT;
                return;
            }
            if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) {
                if (dir == DIR_DOWN) {
                    return;
                }
                dir = DIR_UP;
                return;
            }
        }
        if (IsKeyPressed(KEY_O)) {
            OpenURL(GAME_SITE);
            return;
        }
    }
    
    /**
     * @brief 更新数据
     */
    void UpdateData() {
        // 运行状态才更新数据
        if (running != STATE_RUNNING) {
            return;
        }
        // 空出首位 从最末一位向前交换
        for(int i = length; i > 0; i--){
            snake[i] = snake[i - 1];
        }
        // 获取下个位置
        Vector2 nextHeadPos;
        switch(dir) {
        case DIR_UP:
            nextHeadPos.x = snake[1].x;
            nextHeadPos.y = snake[1].y - 1;
            break;
        case DIR_DOWN:
            nextHeadPos.x = snake[1].x;
            nextHeadPos.y = snake[1].y + 1;
            break;
        case DIR_LEFT:
            nextHeadPos.x = snake[1].x - 1;
            nextHeadPos.y = snake[1].y;
            break;
        case DIR_RIGHT:
            nextHeadPos.x = snake[1].x + 1;
            nextHeadPos.y = snake[1].y;
            break;
        }
        // 如果下一个位置是"墙"或"蛇身"
        if (nextHeadPos.x < 0 || 
            nextHeadPos.y < 0 || 
            nextHeadPos.x > COLS_NUM - 1 || 
            nextHeadPos.y > ROWS_NUM - 1 ||
            isSnakeBody(nextHeadPos)) {
            running = STATE_FAIL;
            return;
        }
        // 如果下一个位置是"苹果"则蛇长度+1
        if (Vector2Equals(nextHeadPos, apple)) {
            if (++length >= MAX_LENGTH) {
                running = STATE_SUCCESS;
                return;
            }
            // 长度每增长10升1级
            level = length / 10 + 1;
            // 根据长度调整游戏速度,注意速度的范围是[1-10]
            if (length < 20) {
                speed = 1;
            } else if(length < 40){
                speed = 2;
            } else if(length < 60){
                speed = 3;
            } else if(length < 80){
                speed = 4;
            } else if(length < 100){
                speed = 5;
            } else if(length < 120){
                speed = 6;
            } else if(length < 140){
                speed = 7;
            } else if(length < 160){
                speed = 8;
            } else if(length < 180){
                speed = 9;
            }else {
                speed = 10;
            }
            apple = findRandomPosition();
        }
        snake[0] = nextHeadPos;
    }
    
    /**
     * @brief 检查某个点是否蛇的身体
     */
    bool isSnakeBody(Vector2 pos) {
        for(int i = 0; i <= length; i++) {
            if (Vector2Equals(pos, snake[i])) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * @brief 查找一个随机空闲的位置
     */
    Vector2 findRandomPosition() {
        Vector2 pos;
        pos.x = GetRandomValue(0, COLS_NUM - 1);
        pos.y = GetRandomValue(0, ROWS_NUM - 1);
        for(int i = 0; i <= length; i++){
            if (Vector2Equals(pos, snake[i])) {
                return findRandomPosition();
            }
        }
        return pos;
    }
    
    /**
     * @brief 初始化数据
     */
    void InitData() {
        dir = DIR_UP;
        running = STATE_RUNNING;
        length = 5;
        speed = 1;
        level = 1;
        
        // 数据
        snake[0] = (Vector2){COLS_NUM / 2, ROWS_NUM / 2};
        for(int i = 1; i <= length; i++){
            snake[i] = snake[0];
        }
        apple = findRandomPosition();
    }
    
    /**
     * @brief 画贪吃蛇
     */
    void DrawSnake(Image *img) {
        ImageClearBackground(img, BLANK);
        // 画苹果
        {
            float wpos = BORDER + apple.x * CELL_SIZE;
            float hpos = BORDER + apple.y * CELL_SIZE;
            ImageDrawRectangle(img, wpos + 2, hpos + 2, CELL_SIZE - 3, CELL_SIZE - 3, ORANGE);
        }
        // 画蛇
        for (int i = 0; i < length; i++) {
            float wpos = BORDER + snake[i].x * CELL_SIZE;
            float hpos = BORDER + snake[i].y * CELL_SIZE;
            if (i == 0) {
                ImageDrawRectangleLines(img, (Rectangle){wpos + 2, hpos + 2, CELL_SIZE - 3, CELL_SIZE - 3}, 1, DARKBLUE);
                ImageDrawRectangle(img, wpos + 3, hpos + 3, CELL_SIZE - 5, CELL_SIZE - 5, BLUE);
            } else {
                ImageDrawRectangle(img, wpos + 2, hpos + 2, CELL_SIZE - 3, CELL_SIZE - 3, DARKBLUE);
            }
        }
        // 画信息
        int fontSize = 16;
        Font font = LoadFontFromMemory(".ttf", fontFileData, fileSize, fontSize, codepoints, codepointsCount);
        float x = BORDER * 2 + CELL_SIZE * COLS_NUM * 1.0f, y = 100.0f, size = 18;
        // 画信息 整理数据
        char sLength[4];        // 长度
        char sLevel[2];         // 等级
        char sSpeed[2];         // 速度
        sprintf(sLength, "%d", length);
        sprintf(sLevel, "%d", level);
        sprintf(sSpeed, "%d", speed);
        ImageDrawTextEx(img, font, sLength, (Vector2){x + BORDER + 46, y + size * 3}, fontSize, 1, PINK);        // 长度
        ImageDrawTextEx(img, font, sLevel, (Vector2){x + BORDER + 46, y + size * 4}, fontSize, 1, PINK);         // 等级
        ImageDrawTextEx(img, font, sSpeed, (Vector2){x + BORDER + 46, y + size * 5}, fontSize, 1, PINK);         // 速度
        ImageDrawTextEx(img, font, DIR_DESC[dir], (Vector2){x + BORDER + 46, y + size * 6}, fontSize/2, 1, PINK);// 方向
        UnloadFont(font);
    }
    
    /**
     * @brief 画棋盘
     */
    Image GenImageBoard() {
        Image imgBoard = GenImageColor(WIN_WIDTH, WIN_HEIGHT, BLANK);
        // 画棋盘
        ImageDrawRectangleLines(&imgBoard, (Rectangle){BORDER * 1.0f, BORDER * 1.0f,    CELL_SIZE * COLS_NUM * 1.0f,    CELL_SIZE * ROWS_NUM * 1.0f}, 1, LIGHTGRAY);
        // 画格子
        for (int w = 1; w < COLS_NUM; w++) {
            for (int h = 1; h < ROWS_NUM; h++) {
                int wpos = BORDER + CELL_SIZE * w;
                int hpos = BORDER + CELL_SIZE * h;
                // 原点
                ImageDrawPixel(&imgBoard, wpos, hpos, DARKGRAY);
                // 上下左右
                ImageDrawPixel(&imgBoard, wpos, hpos - 6, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos, hpos - 4, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos, hpos - 2, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos, hpos + 2, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos, hpos + 4, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos, hpos + 6, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos - 6, hpos, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos - 4, hpos, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos - 2, hpos, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos + 2, hpos, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos + 4, hpos, DARKGRAY);
                ImageDrawPixel(&imgBoard, wpos + 6, hpos, DARKGRAY);
            }
        }
        // 信息
        int fontSize = 16;
        Font font = LoadFontFromMemory(".ttf", fontFileData, fileSize, fontSize, codepoints, codepointsCount);
        float x = BORDER * 2 + CELL_SIZE * COLS_NUM * 1.0f, y = 100.0f, size = 18;
        ImageDrawTextEx(&imgBoard, font, "贪吃蛇", (Vector2){x + BORDER, size}, fontSize * 2, 1, SKYBLUE);
        
        ImageDrawTextEx(&imgBoard, font, "信息", (Vector2){x + BORDER , y + size * 1}, fontSize, 1, LIME);
        ImageDrawTextEx(&imgBoard, font, "------------", (Vector2){x + BORDER , y + size * 2}, fontSize, 1, LIME);
        ImageDrawTextEx(&imgBoard, font, "长度:", (Vector2){x + BORDER , y + size * 3}, fontSize, 1, LIME);
        ImageDrawTextEx(&imgBoard, font, "等级:", (Vector2){x + BORDER, y + size * 4}, fontSize, 1, LIME);
        ImageDrawTextEx(&imgBoard, font, "速度:", (Vector2){x + BORDER, y + size * 5}, fontSize, 1, LIME);
        ImageDrawTextEx(&imgBoard, font, "方向:", (Vector2){x + BORDER, y + size * 6}, fontSize, 1, LIME);
        // 帮助
        ImageDrawTextEx(&imgBoard, font, "帮助", (Vector2){x + BORDER, y + size * 8}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "------------", (Vector2){x + BORDER, y + size * 9}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "向上:W,UP", (Vector2){x + BORDER, y + size * 10}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "向下:S,DOWN", (Vector2){x + BORDER, y + size * 11}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "向左:A,LEFT", (Vector2){x + BORDER, y + size * 12}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "向右:D,RIGHT", (Vector2){x + BORDER, y + size * 13}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "暂停:P,SPACE", (Vector2){x + BORDER, y + size * 14}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "重开:R", (Vector2){x + BORDER, y + size * 15}, fontSize, 1, BEIGE);
        ImageDrawTextEx(&imgBoard, font, "退出:ESC", (Vector2){x + BORDER, y + size * 16}, fontSize, 1, BEIGE);
        // 版本
        char copyright[64];
        sprintf(copyright, "%s/%s %s", GAME_NAME, GAME_VERSION, GAME_RELEASE);
        ImageDrawTextEx(&imgBoard, font, copyright, (Vector2){BORDER, WIN_HEIGHT - size}, fontSize, 1, DARKGRAY);
        ImageDrawTextEx(&imgBoard, font, GAME_SITE, (Vector2){WIN_WIDTH - 300.0f, WIN_HEIGHT - size}, fontSize, 1, DARKGRAY);
        // FPS
        // char fps[16];
        // sprintf(fps, "FPS:%.3f", GetFrameTime());
        // ImageDrawTextEx(&imgBoard, font, fps, (Vector2){WIN_WIDTH - 100.0f, 0.0f}, fontSize / 2, 1, DARKGRAY);
        UnloadFont(font);
        return imgBoard;
    }
    
    /**
     * @brief 创建一个带文字的图层
     */
    Image GenImageLayer(const char* str, Color color, int fontSize) {
        Image imgMasks = GenImageColor(WIN_WIDTH, WIN_HEIGHT, BLANK);
        Font font = LoadFontFromMemory(".ttf", fontFileData, fileSize, 16, codepoints, codepointsCount);
        ImageDrawTextEx(&imgMasks, font, str, (Vector2){(WIN_WIDTH - strlen(str) * fontSize / 2) / 2.0f, (WIN_HEIGHT - fontSize) / 2.0f}, fontSize, 1, color);
        UnloadFont(font);
        return imgMasks;
    }
    
public:
    void run() {
        InitWindow(WIN_WIDTH, WIN_HEIGHT, "贪吃蛇");
        SetRandomSeed(time(NULL));
        SetTargetFPS(60);
        //SetConfigFlags(FLAG_MSAA_4X_HINT);
        
        InitData();
        
        const int num = 4;
        Image images[num];
        Texture textures[num];
        images[0] = GenImageBoard();
        images[1] = GenImageLayer("暂停", ORANGE, 96);
        images[2] = GenImageLayer("成功", GREEN, 96);
        images[3] = GenImageLayer("失败", RED, 96);
        for (int i = 0; i < num; i++) {
            textures[i] = LoadTextureFromImage(images[i]);
        }
        
        float delta = 0.0f;
        while(!WindowShouldClose()) {
            // 监听事件
            ListenEvent();
            // 更新数据 (根据speed取值范围[1-10]调整数据更新速度[0.35s-0.05s])
            delta += GetFrameTime();
            if (delta >= 0.35f - speed * 0.03) {
                UpdateData();
                delta = 0.0f;
            }
            // 绘制
            Image imgSnake = GenImageColor(WIN_WIDTH, WIN_HEIGHT, WHITE);
            DrawSnake(&imgSnake);
            Texture textureSnake = LoadTextureFromImage(imgSnake);
            BeginDrawing();
            {
                ClearBackground(BLANK);
                DrawTexture(textures[0], 0, 0, WHITE);
                DrawTexture(textureSnake, 0, 0, WHITE);
                switch (running) {
                case STATE_PAUSE:
                    DrawTexture(textures[1], 0, 0, WHITE);
                    break;
                case STATE_FAIL:
                    DrawTexture(textures[3], 0, 0, WHITE);
                    break;
                case STATE_SUCCESS:
                    DrawTexture(textures[2], 0, 0, WHITE);
                    break;
                default:
                    break;
                }
            }
            EndDrawing();
            UnloadImage(imgSnake);
            UnloadTexture(textureSnake);
        }
        CloseWindow();
        // 释放资源
        UnloadFileData(fontFileData);
        for(int i = 0; i <= num; i++){
            UnloadImage(images[i]);
            UnloadTexture(textures[i]);
        }
    }
};