因為已經寫了食物的實現,所以我不知道到底是該先寫世界的實現還是蛇的實現。因為世界就是一個窗口,可以立刻在世界中看到食物的樣子,對於大多數人來說,如果寫完代碼立刻就能看到效果,那就再好不過了。可是,我最後還是選擇了先寫蛇的實現這篇筆記。如果先寫世界的實現,我就無法按照現在的思路完完整整的寫下去,因為沒有蛇,世界部分的代碼就不完整,看完食物的效果後,我還是得寫蛇的實現,然後又得修改世界部分的代碼,來查看蛇的效果。反反復復,實在折騰不起。所以我打算把食物和蛇的實現都寫完,最後統一看運行效果。
蛇和食物一樣,得在世界中創建,所以代碼基本差不多。
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宏來增加蛇的身體節點,所以為了和增加節點相對應,我定義了移除節點這個宏。
移動蛇的位置分為兩部,移動蛇頭和移動蛇的身體。主要是由於定義的時候我沒有把蛇頭當作身體的一部分,因為身體可以增長,而蛇頭不能增長,所以只能這樣了。
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表示蛇頭,MOVE_SNAKE(snake)表示移動蛇頭的位置,MOVE_SNAKE(body)表示移動身體的位置。
移動身體需要遍歷鏈表,不過這裡設置身體方向不知道是否有人看懂? 當前身體節點的方向等於下一個身體節點的方向,仔細想想,這是什麼意思?
我對蛇的分析是,蛇只會在尾部追加節點,如果snake->body指向第一個節點first, first指向第二個節點second, 那麼我追加第三個節點就要從snake->body開始遍歷兩次,追加第四個節點就要從snake->body開始遍歷三次。所以我改變了這個沒有效率的行為,我讓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);
}
}
}
}
對於蛇的增長,有兩個意思:沒有尾巴時,增長的是尾巴。有尾巴時,增長的是身體。
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);
}
}
}
接下來是檢查碰撞的函數,它主要有兩個用途:1. 當參數rect是蛇頭位置時,用來檢測蛇頭是否咬到自己的身體。2. 當參數rect是食物位置時,用來檢測身體是否碰到食物。咬到自己或者碰到食物,返回1, 否則返回0。
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表示蛇碰到世界的邊界,不可以移動。我沒有實現蛇可以從一邊回到另一邊這種功能,也沒有規定蛇碰到牆就死了。一切盡可能保持簡單!
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;
}
這裡switch語句只有當蛇碰到世界的邊界時才會進入,這一段主要是為了實現一個功能:當蛇碰到世界的邊界時,蛇無法再向前移動,但是蛇可以再次轉彎。
未完,待續!