因為已經寫了食物的實現,所以我不知道到底是該先寫世界的實現還是蛇的實現。因為世界就是一個窗口,可以立刻在世界中看到食物的樣子,對於大多數人來說,如果寫完代碼立刻就能看到效果,那就再好不過了。可是,我最後還是選擇了先寫蛇的實現這篇筆記。如果先寫世界的實現,我就無法按照現在的思路完完整整的寫下去,因為沒有蛇,世界部分的代碼就不完整,看完食物的效果後,我還是得寫蛇的實現,然後又得修改世界部分的代碼,來查看蛇的效果。反反復復,實在折騰不起。所以我打算把食物和蛇的實現都寫完,最後統一看運行效果。
蛇和食物一樣,得在世界中創建,所以代碼基本差不多。
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語句只有當蛇碰到世界的邊界時才會進入,這一段主要是為了實現一個功能:當蛇碰到世界的邊界時,蛇無法再向前移動,但是蛇可以再次轉彎。
未完,待續!