上節我們用方向控制函數寫了個小畫圖程序,它雖然簡單好玩,但我們不應該止步於此。革命尚未成功,同志還需努力。
先復習一下貪吃蛇的結構:
開始實現之前,我們先理清一下思路。和前面畫圖程序不同,貪吃蛇可以有很多節,可以用一個足夠大的結構體數組來儲存它。 還需要一個食物坐標。定義如下:
typedef struct Position //坐標結構 { int x; int y; }Pos; Pos array; //移動方向向量 Pos snake[300000] = {}; //蛇的結構體數組,誰能夠無聊到吃299999個食物~_~
long len=1; //蛇的長度
Pos egg; //食物坐標
之前的畫圖程序是四個方向都可以走,可蛇是不能倒著走的,所以方向控制函數要改成這樣:
void command() //獲取鍵盤命令 { if (_kbhit()) //如果有鍵盤消息 switch (_getch()) /*這裡不能用getchar()*/ { case 'a': if (array.x != 1 || array.y != 0) {//如果命令不是倒著走,就修正方向向量,否則不做改變,下同。 array.x = -1; array.y = 0; } break; case 'd': if (array.x != -1 || array.y != 0) { array.x = 1; array.y = 0; } break; case 'w': if (array.x != 0 || array.y != 1) { array.x = 0; array.y = -1; } break; case 's': if (array.x != 0 || array.y != -1) { array.x = 0; array.y = 1; } break; } }
蛇可能不止一節,所以移動函數需要做出改變。仔細一想就知道,除了頭結點外,每個節點的下一個坐標為它前一個結點當前的坐標,而頭節點的坐標等於它本身坐標加上移動向量(這裡是 方向向量*10)
還有個問題是蛇走過的痕跡需要擦除,每走一步,它留下的痕跡應該是走這一步之前蛇的最末一個結點的坐標,我們需要擦除掉它。
結果如下:
void move() //修改各節點坐標以達到移動的目的 { setcolor(BLACK); //覆蓋尾部走過的痕跡 rectangle(snake[len-1].x - 5, snake[len-1].y - 5, snake[len-1].x + 5, snake[len-1].y + 5); for (int i = len-1; i >0; i--) //除了頭結點外,每個節點的下一個坐標為它前一個結點當前的坐標 { snake[i].x = snake[i - 1].x; snake[i].y = snake[i - 1].y; } snake[0].x += array.x*10; //頭節點的坐標等於它本身坐標加上移動向量(這裡是 方向向量*10) snake[0].y += array.y*10; }
另外,我們的蛇是有穿牆術的~~~它的實現方法非常簡單:
void break_wall() { if (snake[0].x >= 640) //如果越界,從另一邊出來 snake[0].x = 0; else if (snake[0].x <= 0) snake[0].x = 640; else if (snake[0].y >= 480) snake[0].y = 0; else if (snake[0].y <= 0) snake[0].y = 480; }
接下來是食物相關函數,這個算是重點。
1. 食物生成
我們希望食物每次出現的位置都是隨機的, 可以這樣實現。
1 srand((unsigned)time(NULL)); 2 egg.x = rand() % 80 * 5 + 100; //頭節點位置隨機化 3 egg.y = rand() % 50 * 5 + 100;
而且食物不能與蛇重合,最好也不要離蛇太近。綜合起來就是這樣:(srand在初始化中會被調用,所以這裡略去了)
void creat_egg() { while (true) { int ok = 0; //這是個標記,用於判斷函數是否進入了某一分支 egg.x = rand() % 80 * 5 + 100; //頭節點位置隨機化 egg.y = rand() % 50 * 5 + 100; for (int i = 0; i < len; i++) //判斷是否離蛇太近 { if (snake[i].x == 0 && snake[i].y == 0) continue; if (fabs(snake[i].x - egg.x) <= 10 && fabs(snake[i].y - egg.y) <= 10) ok = -1; //如果,進入此分支,改變標記 break; } if (ok == 0) //如果不重合了,跳出函數 return; } }
2. 吃到食物
如果吃到食物,那麼需要消除被吃掉的食物,生成新食物,蛇也要增長一節。
我覺得這裡最麻煩的就是蛇變長的實現:是在蛇頭添加一節,還是在蛇尾?添加在蛇頭(尾)的上下左右哪一邊?
想來想去,只有在蛇頭位置,我們可以根據當前方向向量,在移動方向上新添一節。這對應的代碼如下:
//add snake node len += 1; for (int i = len - 1; i >0; i--) //所有數據後移一個單位,騰出snake[0]給新添的一節 { snake[i].x = snake[i - 1].x; snake[i].y = snake[i - 1].y; } snake[0].x += array.x * 10; //這就是新添的這一節的位置 snake[0].y += array.y * 10;
吃到食物的完整代碼如下:
void eat_egg() { if (fabs(snake[0].x - egg.x)<=5 && fabs(snake[0].y - egg.y)<=5) //判斷是否吃到食物,因為食物位置有點小偏差,只好使用范圍判定~~ { setcolor(BLACK); //hide old egg circle(egg.x, egg.y, 5);
creat_egg(); //create new egg //add snake node len += 1; for (int i = len - 1; i >0; i--) { snake[i].x = snake[i - 1].x; snake[i].y = snake[i - 1].y; } snake[0].x += array.x * 10; //每次移動10pix snake[0].y += array.y * 10; } }
游戲結束判定
最後,我們還差一個死亡判定,因為自帶穿牆術,所以實際的死亡判定只有一個,就是咬到自己,代碼如下:
void eat_self() { if (len == 1) //只有一節當然吃不到自己~~ return; for (int i = 1; i < len; i++) if (fabs(snake[i].x - snake[0].x) <= 5 && fabs(snake[i].y - snake[0].y) <= 5) //如果咬到自己(為了不出bug,使用了范圍判定) { outtextxy(250, 200, "GAME OVER!"); //你的蛇死了~ Sleep(3000); //3s時間讓你看看你的死相~~ closegraph(); exit(0); //退出 } }
當然,你也可以直接丟掉這個函數,然後開心地狂咬自己—_—||
最後:畫圖函數
畫出食物和蛇,其實蛇沒必要全部畫出來,只要畫蛇頭就可以了,但這之中有些小問題,誰有興趣可以自己玩玩,我是懶得動了~
void draw() //畫出蛇和食物 { setcolor(BLUE); for (int i = 0; i < len; i++) { rectangle(snake[i].x - 5, snake[i].y - 5, snake[i].x + 5, snake[i].y + 5); } setcolor(RED); //畫蛋(怎麼感覺怪怪的~) circle(egg.x, egg.y, 5); Sleep(100); }
到這裡,游戲大功告成~~ 什麼?你說運行不起來?那是因為少了初始化函數,和游戲循環啦~~這幾個都比較簡單,就直接放下面了:
void init() //初始化 { initgraph(640, 480); //初始化圖形界面 srand((unsigned)time(NULL)); //初始化隨機函數 snake[0].x = rand() % 80 * 5 + 100; //頭節點位置隨機化 snake[0].y = rand() % 50 * 5 + 100; array.x = pow(-1,rand()); //初始化方向向量,左或者右 array.y = 0; creat_egg(); } int main() { init(); while (true) { command(); //獲取鍵盤消息 move(); //修改頭節點坐標-蛇的移動 eat_egg(); draw(); //作圖 eat_self(); } return 0; }
好了,這是真的大功告成了。給你們看看死亡方式之自盡:
完整代碼如下:
可能還有若干bug留存,歡迎大家指正~~
甲鐵城鎮~