在開發貪吃蛇遊戲時,面臨一個經典的工程權衡:是先實現「世界」(遊戲場景),還是先實現「蛇」?雖然先寫世界能讓開發者立即看到食物渲染的效果,獲得即時反饋,但筆者最終選擇先完成蛇的實現。原因在於:若無蛇的邏輯支撐,世界模塊的代碼將不完整;若先寫世界,後續仍需回頭修改世界代碼以適配蛇的行為,反覆折騰效率極低。因此,確定策略:先完成食物與蛇的核心實現,最後統一整合運行效果。
蛇與食物一樣,都需要在世界中創建。核心創建函數 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 同時服務於兩種檢測:
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;
}
未完待續,下一篇將整合世界模塊,展示完整運行效果。