在繼上篇[C語言]貪吃蛇_結構數組實現大半年后,鏈表實現的版本也終於出爐了。兩篇隔了這么久除了是懶癌晚期的原因外,對整個游戲流程的改進,模塊的精簡也花了一些時間(都是借口)。
優化模塊的前沿鏈接:
一、游戲流程
貪吃蛇游戲的原理很簡單,即在一張地圖內,有一條蛇和隨機出現的食物,玩家操控蛇的移動,當蛇吃到了食物后,蛇長度增加。游戲過程中,蛇不能撞牆,也不能咬到自身。
反映到程序中,就是這樣一張簡略的流程圖(結構數組實現):
在這個流程中,有許多的不足。當蛇已經存在並且接受了一個合法的輸入時,根據下一步是否吃到食物來判斷是否需要清除尾巴是合理的,但在控制台里,貪吃蛇每次循環移動其實都只需對兩個位置進行操作:一個是接受操作后的蛇頭,無論下一步在哪兒,這都是必須要打印的一個;另一個是蛇尾,這則需要根據蛇頭是否吃到食物來決定去留。所以每次循環都重新打印所有節點是很多余的,因此需要改進。
我們可以這樣改:在接受輸入后,先把一定會移動的蛇頭打印出來,再判斷蛇尾的去留。最后在蛇(鏈表)各個節點中,依次賦得前一個節點的值。流程圖移動模塊如下:
按照這個流程圖,蛇每次移動就只需要操作控制台上的兩個節點了。另外可以將在控制台某坐標打印一個特殊符號抽象成一個函數:
-
#define SPACE 0
-
#define NODE 1
-
#define FOOD 2
-
#define WALL 3
-
-
void PrintIn(int size,int x,int y);
-
-
-
void PrintIn(int size,int x,int y)
-
{
-
//size
-
//清除節點:0 打印蛇身:1
-
//打印食物:2 打印牆壁:3
-
char *arr[4] = {" ","⊙","●","■"};
-
Pos(x,y);
-
printf("%s",arr[size]);
-
}
二、初始化
1.初始化地圖
在[C語言]貪吃蛇_結構數組實現中我提到過,因為控制台一個字符的寬高所占像素點不同,所以再看控制台上想輸出一個規整的正方形,就得讓寬高之比為2:1。並且為了輸出的正方形更完整,就需要使用一些占兩個普通字符的特殊字符。
-
#define WIDTH 60
-
#define HEIGHT 30
-
-
void CreateMap(void);
-
-
void CreateMap(void)
-
{
-
int i;
-
for(i=0;i<WIDTH;i+=2)// 上下30 寬
-
{
-
PrintIn(WALL,i,0);
-
PrintIn(WALL,i,HEIGHT-1);
-
}
-
for(i=1;i<HEIGHT-1;i++)//左右 28+2 高
-
{
-
PrintIn(WALL,0,i);
-
PrintIn(WALL,WIDTH-2,i);
-
}
-
}
2.初始化蛇
在初始化蛇之前,我們得給蛇一個定義:蛇應該是一個鏈表,其中每個節點都包含了一個坐標。所以有如下定義:
-
typedef struct {
-
int x;
-
int y;
-
}Place; //坐標
-
-
typedef struct node{
-
Place place;
-
struct node *next;
-
}Node; //節點
-
-
typedef struct snake{
-
Node *head;
-
int size; //長度
-
}Snake; //指向一條蛇
-
因此當我們聲明
-
Snake snake;
時,我們其實就聲明了一條蛇。
好了,現在可以給蛇賦予節點了。原理也很簡單,在鏈表尾部加三個節點就好。我們規定蛇頭在右,共有三個節點,位置居中,所以蛇頭的坐標應該為(28,14),后兩個節點依次為(26,14)、(24,14)。
-
bool InitializeSnake(Snake *psnake)
-
{
-
Node *pnew;
-
Node *scan;
-
-
for(int i = 0;i<3;i++)
-
{
-
scan = (psnake->head);
-
pnew = (Node *)malloc(sizeof(Node));
-
if(pnew == NULL)
-
{
-
printf("pnew == NULL");
-
system("pause");
-
return false;
-
}
-
pnew->place.x = 28-2*i;
-
pnew->place.y = 14;
-
pnew->next = NULL;
-
psnake->size++;
-
PrintIn(NODE,pnew->place.x,pnew->place.y);
-
if(scan == NULL)
-
psnake->head = pnew;
-
else
-
{
-
while(scan->next != NULL)
-
scan = scan->next;
-
scan->next = pnew;
-
}
-
}
-
return true;
-
}
3.初始化食物
食物可用一個全局變量來表示,該變量存儲一個坐標值。因此可用上之前定義的Place結構。
-
typedef Place Food;
-
-
Food food = {0,0};
而坐標值的范圍只要保證兩點就好:在地圖內;不與蛇身重合。
-
void CreateFood(void)
-
{
-
int flag = 0;
-
srand((unsigned int)time(0));
-
while(1)
-
{
-
do{
-
food.x = rand()%(WIDTH-5)+2;
-
}while(food.x%2!=0);
-
food.y = rand()%(HEIGHT-2)+1;
-
Node *scan = snake.head;
-
while(scan !=NULL)
-
{
-
if(scan->place.x == food.x &&
-
scan->place.y == food.y)
-
{
-
flag = -1;
-
break;
-
}
-
scan = scan->next;
-
}
-
if(flag>=0)
-
{
-
PrintIn(FOOD,food.x,food.y);
-
break;
-
}
-
}
-
// AfterEatFood();
-
}
二、蛇的移動——輸入的甄別
蛇的移動本質很簡單,就是不斷更新蛇的位置,並打印。所以我們需要一個循環:
-
while(true)
-
{
-
//。。。
-
}
其次我們需要接收輸入,用來控制游戲進行
這里介紹一個函數
-
1. int kbhit(void);
-
2. // 檢查當前是否有鍵盤輸入,若有則返回一個非0值,否則返回0
這是一個非阻塞函數,有鍵按下時返回非0,但此時按鍵碼仍然在鍵盤緩沖隊列中。所以在確定鍵盤有響應之后,再用一個char變量將輸入從緩沖區中調出來。
-
1. if(kbhit())
-
2. ch = getch();
現在我們規定游戲中'w' 's' 'a' 'd'控制方向,空格暫停,所以對於用戶的輸入,我們需要判斷是否合法。我用了一個數組+循環來代替一連串的if:
-
char ch,direction = ' ';
-
char charr[5] = {'w','s','a','d',' '};
-
int flag = 0;
-
if(kbhit())
-
ch = getch();
-
for(int i = 0;i<5;i++) //判斷輸入是否為規定的五個字符
-
{
-
if(ch == charr[i])
-
{
-
flag = 1;
-
break;
-
}
-
}
當我們得到的輸入合法時,我們仍需判斷現在的輸入方向是否與之前的方向相反,畢竟在我設計的這個游戲里,蛇身可不能折疊往自己身上碾過去。
在我用數組實現的那個版本里,我用了一大串if-else來避免相反的輸入,這雖然簡單,卻很無腦。所以我用一個更簡單的方法代替了它。在我們規定為正確輸入的五個字符中,ASCII碼分別為a:97,d:100,w:119,s:115,space:32,其中ad是沖突的一對,ws是沖突的一對。ad的差值為±3,ws的差值為±4,空格直接暫停,因此不予考慮。所以我們只需要判斷,如果輸入ch的值與方向direction的差值為±3或者±4,那么就可以斷定輸入不合法,丟棄。
-
if(flag == 1) //確認輸入正常
-
{
-
if(!(direction-ch==4||direction-ch==-4||direction-ch==3||direction-ch==-3))
-
{ //排除與方向相反的輸入
-
direction = ch;
-
}
-
else if(ch == ' ')
-
continue;
-
}
之前版本10行的事情,現在有意義的代碼只有5行。
三、蛇的移動
為了方便對移動的坐標進行操作,我們聲明一個數組,用來存儲不同方向下坐標的變化:
-
int dir_value[2][4] = {
-
{0,0,-2,2},
-
{-1,1,0,0}
-
};
不同下標分別對於w s a d,因為長度60的WIDTH其實只有30個單位,所以x值一次加2。
1、畫面上的移動
由於蛇身每個節點都一個樣,所以沒有必要每次循環都把所有的節點重新輸出一遍,只需要更新頭節點和尾節點就好。在游戲中,無論是撞牆、還是其他情況,蛇只要移動了,那么他頭節點的坐標一定會改變,因此我們可以在移動后先把新的蛇頭打印出來。至於蛇尾,如果蛇移動后並沒有吃到食物,蛇尾則刪除,吃到了的話蛇尾則保留。所以在打印了頭部之后再判斷頭部是否吃到食物,再對蛇尾進行處理。
-
switch(direction)
-
{
-
case 'w':
-
PrintIn(NODE,snake.head->place.x+dir_value[0][0],snake.head->place.y+dir_value[1][0]); //打印頭部
-
if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y)
-
{
-
//AddNode(&snake); //尾插法
-
//CreateFood();
-
}
-
else //沒有吃到
-
{
-
Node *tail = GetTail(&snake);
-
PrintIn(SPACE,tail->place.x,tail->place.y); //畫面上消除尾部節點
-
}
-
//...
-
}
2、畫面外的移動
在內存中,我們則需要更新各個節點的坐標。如果吃到了食物,則加入一個節點(我用的尾插法),並將前一節點的值賦給后一節點。先前的頭節點坐標值賦給第二節點,頭節點則根據輸入,更新新的坐標值。沒有吃到的話,也直接賦值,尾節點坐標值因為下一步就要更新,所以可丟棄不管,只需得到前一節點坐標就好。
-
case 'w':
-
PrintIn(NODE,snake.head->place.x+dir_value[0][0],snake.head->place.y+dir_value[1][0]);
-
if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y)
-
{
-
AddNode(&snake); //尾插法
-
CreateFood();
-
}
-
else
-
{
-
Node *tail = GetTail(&snake); //得到尾節點
-
PrintIn(SPACE,tail->place.x,tail->place.y);
-
}
-
RenewSnake(&snake); //鏈表各節點值的跟新
-
snake.head->place.x += dir_value[0][0]; //蛇頭更新
-
snake.head->place.y += dir_value[1][0];
-
break;
其中RenewSnake()函數用來更新一個鏈表(蛇),使前一個節點的值賦給后一個節點,對這個只需要兩個臨時變量就可以。
從這簡單的流程圖可看出一點端倪,現在我們把步驟完善一下。
因此我們得到了一些普適性的方法,代碼如下:
-
void RenewSnake(Snake *psnake)
-
{
-
int x_index[2] = {0,0},y_index[2] = {0,0};
-
Node *scan = psnake->head;
-
-
int i = 1;
-
x_index[i%2] = scan->place.x;
-
y_index[i%2] = scan->place.y;
-
-
for(i = 1;i<psnake->size;i++)
-
{
-
x_index[(i+1)%2] = scan->next->place.x;
-
y_index[(i+1)%2] = scan->next->place.y;
-
-
scan->next->place.x = x_index[i%2];
-
scan->next->place.y = y_index[i%2];
-
-
scan = scan->next;
-
}
-
}
同理,其余三個方向也是如此。
四、移動后的操作
在這個游戲中,我們需要這么幾個變量:
-
int length = -1;
-
int score = -10;
-
int speed = 250;
其中,length其實可以不需要。我們需要在吃到食物后進行一系列的操作,如加分,重新生成食物等等。所以在移動時的判斷里加入一些函數。
-
if(snake.head->place.x+dir_value[0][0] == food.x && snake.head->place.y+dir_value[1][0] == food.y)
-
{
-
AddNode(&snake); //尾插法
-
CreateFood();
-
}
生成食物還需要加分等操作,所以我們可以把加分等操作的函數(AfterEatFood();)放到該函數末尾。不過這樣的話,游戲開始生成的第一個食物就需要注意了,因此我們的兩個全局變量都是負值。
-
void AfterEatFood()
-
{
-
Pos(WIDTH+20,HEIGHT-20);
-
printf("%d = %d",++length,snake.size);
-
Pos(WIDTH+16,HEIGHT-18);
-
if(speed>150)
-
score += 10;
-
else
-
score += 20;
-
printf("%d",score);
-
if(speed>100)
-
speed-=5;
-
Pos(WIDTH+16,HEIGHT-16);
-
printf("%d",speed);
-
}
在蛇移動后,我們還需判斷蛇是否撞牆或者咬到自身。撞牆是蛇頭與邊界坐標的比較,咬到自身則可以用一個循環。
-
if(ThroughWall(&snake) == true)
-
{
-
Pos(0,30);
-
system("pause");
-
exit(0);
-
}
-
if(BiteItself(&snake)==true)
-
{
-
Pos(0,30);
-
system("pause");
-
exit(0);
-
}
-
bool ThroughWall(Snake *psnake)
-
{
-
if(psnake->head->place.x == 0 || psnake->head->place.x == WIDTH-2 ||
-
psnake->head->place.y == 0 || psnake->head->place.y == HEIGHT-1)
-
{
-
Pos(25,15);
-
printf("撞牆,游戲結束!");
-
return true;
-
}
-
else
-
{
-
Pos(0,HEIGHT);
-
printf(" "); //將閃爍不停的光變放到地圖外面---迷之操作=。=
-
return false;
-
}
-
}
-
-
bool BiteItself(Snake *psnake)
-
{
-
Node *scan = psnake->head;
-
-
while(scan->next != NULL)
-
{
-
scan = scan->next;
-
if(scan->place.x == psnake->head->place.x &&
-
scan->place.y == psnake->head->place.y)
-
{
-
Pos(25,15);
-
printf("咬到自身,游戲結束!");
-
return true;
-
}
-
}
-
return false;
-
}
最后在循環末尾加入Sleep,控制游戲的節奏。
-
Sleep(speed);