首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C語言入門:貪吃蛇遊戲核心邏輯實現(蛇的創建、移動與繪製)

     編輯:關於C語言

前言:開發順序的抉擇

在開發貪吃蛇遊戲時,面臨一個經典的工程權衡:是先實現「世界」(遊戲場景),還是先實現「蛇」?雖然先寫世界能讓開發者立即看到食物渲染的效果,獲得即時反饋,但筆者最終選擇先完成蛇的實現。原因在於:若無蛇的邏輯支撐,世界模塊的代碼將不完整;若先寫世界,後續仍需回頭修改世界代碼以適配蛇的行為,反覆折騰效率極低。因此,確定策略:先完成食物與蛇的核心實現,最後統一整合運行效果。

蛇的創建與初始化

蛇與食物一樣,都需要在世界中創建。核心創建函數 SNK_CreateSnake 負責分配內存、初始化結構體,並調用 SNK_GrowSnake 為蛇添加初始節點(蛇尾),避免「只有蛇頭」的畸形狀態。

Snake * SNK_CreateSnake(World *world, int size, int x, int y) {
    Snake *snake;
    if (world == 0) return 0;
    if ((snake = (Snake *)SDL_malloc(sizeof(Snake))) == 0) return 0;
    INIT_SNAKE(world, size, x, y);
    SNK_GrowSnake(snake);
    return snake;
}

其中宏 INIT_SNAKE 用於初始化 Snake 結構體;SNK_GrowSnake 則負責將蛇長度加一。創建之初蛇僅有蛇頭,必須主動增長一次以生成蛇尾,符合常規遊戲邏輯。

蛇的銷毀與鏈表管理

蛇身由單向鏈表表示,銷毀時需遍歷釋放每個身體節點:

void SNK_DestroySnake(Snake *snake) {
    struct Body *body;
    if (snake != 0) {
        if ((body = snake->body)) REMOVE_BODY(body);
        SDL_free(snake);
        snake = 0;
    }
}

宏 REMOVE_BODY 封裝了鏈表遍歷與釋放邏輯。雖然定義宏顯得「多此一舉」,但為了與增加節點的 APPEND_BODY 宏對應,保持代碼對稱性,這是值得的。

蛇的移動邏輯:頭身分離與尾部優化

移動分兩步:移動蛇頭、移動蛇身。由於蛇頭不參與身體增長,設計上將蛇頭與身體分離。關鍵在於 SNK_MoveSnake 中的方向傳遞機制:

void SNK_MoveSnake(Snake *snake) {
    struct Body *body;
    if (snake != 0) {
        MOVE_SNAKE(snake);
        for (body = snake->body; body; body = body->next) {
            MOVE_SNAKE(body);
            body->direction = (body->next != 0) ? body->next->direction : snake->direction;
        }
    }
}

核心洞察:為了提升追加節點效率,筆者讓 snake->body 始終指向最後一個身體節點(蛇尾)。因此 for 循環實際是從蛇尾向蛇頭方向遍歷。當前節點方向 = 下一節點方向,最終蛇尾跟隨蛇身,蛇身跟隨蛇頭,實現了平滑轉彎。

蛇的繪製

繪製邏輯與食物類似,使用一連串矩形表示蛇身:

void SNK_DrawSnake(Snake *snake) {
    SDL_Rect rect;
    struct Body *body;
    if (snake != 0) {
        rect.x = snake->x; rect.y = snake->y;
        rect.w = rect.h = snake->size;
        if (((snake->world != 0) ? (snake->world->render != 0) : 0)) {
            SDL_SetRenderDrawColor(snake->world->render, snake->color.r, snake->color.g, snake->color.b, snake->color.a);
            SDL_RenderDrawRect(snake->world->render, &rect);
            for (body = snake->body; body; body = body->next) {
                rect.x = body->x; rect.y = body->y;
                SDL_RenderDrawRect(snake->world->render, &rect);
            }
        }
    }
}

蛇的增長策略

SNK_GrowSnake 區分兩種情況:無尾巴時增長尾巴,有尾巴時增長身體。通過判斷 snake->body 是否為空,決定向蛇頭還是蛇尾追加節點:

void SNK_GrowSnake(Snake *snake) {
    struct Body *body;
    if (snake != 0) {
        if ((body = (struct Body *)SDL_malloc(sizeof(struct Body))) == 0) return;
        if (snake->body == 0) {
            APPEND_BODY(snake, body);
        } else {
            APPEND_BODY(snake->body, body);
        }
    }
}

碰撞檢測:雙重用途

SNK_HasIntersection 同時服務於兩種檢測:

  1. 傳入蛇頭位置 → 檢測蛇頭是否咬到自己身體
  2. 傳入食物位置 → 檢測身體是否碰到食物
int SNK_HasIntersection(Snake *snake, SDL_Rect rect) {
    SDL_Rect bodyrect;
    struct Body *body;
    if (snake != 0) {
        bodyrect.w = bodyrect.h = snake->size;
        for (body = snake->body; body; body = body->next) {
            bodyrect.x = body->x; bodyrect.y = body->y;
            if (SDL_HasIntersection(&bodyrect, &rect) != 0) return 1;
        }
    }
    return 0;
}

狀態機設計:簡單而實用

定義三種狀態:SNAKE_DIED(咬到自己)、SNAKE_MOVABLE(正常)、0(撞牆不可移動)。不實現穿牆、不規定撞牆即死,保持極簡邏輯。SNK_GetSnakeStatus 中的 switch 僅在撞牆時進入,目的是允許蛇在邊界轉彎而非強制死亡:

int SNK_GetSnakeStatus(Snake *snake) {
    SDL_Rect headrect;
    if (((snake != 0) ? (snake->world != 0) : 0)) {
        headrect.w = (snake->x > 0 && snake->x < snake->world->w);
        headrect.h = (snake->y > 0 && snake->y < snake->world->h);
        if (headrect.w && headrect.h) {
            headrect.x = snake->x; headrect.y = snake->y;
            headrect.w = headrect.h = snake->size;
            if (SNK_HasIntersection(snake, headrect) != 0) return SNAKE_DIED;
            return SNAKE_MOVABLE;
        } else {
            switch (snake->direction) {
                case SNAKE_UP:    headrect.x = (snake->y > 0); break;
                case SNAKE_DOWN:  headrect.x = ((snake->y + snake->size) < snake->world->h); break;
                case SNAKE_LEFT:  headrect.x = (snake->x > 0); break;
                case SNAKE_RIGHT: headrect.x = ((snake->x + snake->size) < snake->world->w); break;
            }
            return ((headrect.x != 0) ? SNAKE_MOVABLE : 0);
        }
    }
    return 0;
}

未完待續,下一篇將整合世界模塊,展示完整運行效果。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved