專案總結-----貪吃蛇
最近寫了一個小的專案貪吃蛇,當然這個專案不是完完全全以自己的知識範圍所寫出來的,先說一下以我的能力來想的話,最大的問題在哪。首先我聽到貪吃蛇之後,最大的難點就在整個專案的輸出情況,也就是蛇是怎麼顯示出來的,之前有寫過三子棋的經歷,之前的整個遊戲的介面是通過二維陣列來實現的,貪吃蛇同樣和三子棋是平面遊戲,通過陣列實現也不是沒有可能,但是有一點是很難解決的,就是你需要不斷的讓你的蛇來移動甚至增加你蛇的長度。但是這也就意味著當你的蛇每移動一次的時候就需要讓你的整個二維數組裡的內容變化一下,同樣你的牆也應該存在於二維陣列中,這樣當你每次移動蛇或者吃食物,或者重新整理食物的時候,也就是你的介面變化一次你的二維陣列都要進行相應的改變,然後進行輸出。這樣對一個一直在移動的貪吃蛇遊戲來說,是很麻煩的,雖然沒有驗證,但是我認為這種方式會讓貪吃蛇看起來十分的卡頓。所以這次我們用了其他的方法來解決這件事情。
第一個函式是游標移動函式,SetConsoleCursorPosition function(),這個函式可以將你的游標移動到你想移動到的位置,這時候再將你蛇,或者食物進行輸出就可以了。這個函式是需要傳入兩個引數,但是引數並不是你想要移動的X、Y位置座標,這裡的引數是我之前所沒有接觸到的。SetConsoleCursorPosition(hout,coord);這裡的函式,第二個引數是一個結構體,這個是由系統所定義好的結構體,結構體中包括了兩個一個是x,一個是y,這就是你所想要移動到的座標,但是hout是什麼呢?Hout是我們所說的控制代碼。因為我現在的知識量還不夠特別深入詳細的明白控制代碼的作用。在使用這個函式的時候,只需要將coord中的X和Y兩個元素賦值就可以進行操作。
HANDLE hout;
COORD coord;
coord.X=3;
coord.Y=3;
hout=GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hout,coord);
這裡就可以將你的游標移動的3,3,位置然後直接呼叫輸出函式就可以
這是我之前在寫貪吃蛇的過程中介紹過的一次游標移動函式。
之後就是第二個函式,GetAsyncKeyState(VK_UP),這個函式是用來接收鍵盤的輸入值,這裡我們通過鍵盤的上下左右來操作蛇,直接就是UP、DOWN、RIGHT、LEFT來代表鍵盤的上下左右,這全都是由系統函式所規定的,我們直接呼叫就可以。
介紹完這兩個函式之後,剩下的知識都是用的我當前所能完成的內容。
首先,需要完成的是遊戲的介面,
typedef struct UI
{
int MarginTop;//DOS框上和左邊為了美觀留了一部分表示留的空間的大小
int MarginLeft;
int GameWidth;//這裡表示的是遊戲區域的字元個數,因為小方塊並不是橫豎都佔一個字元,方便計算
int GameHeight;
int WindowWidth;
int WindowHeight;//表示整個視窗的寬度和高度
char *SnakeBlock;//儲存的是蛇、牆、以及食物的內容雖然都一樣但是為了分清楚 用指標不用指定大小方便儲存
char *FoodBlock;
char *WallBlock;
int BlockWidth;//每個小方塊的寬度,因為是目前已知小方塊並不是正方形,還是為了上邊GameWidth的計算
}UI;
這是定義的有關介面的結構體結構體中包括的一些變數我都用註釋來說明了,當然還是那句話程式並不是唯一的,只要能實現就可以,所以這裡的變數大家可以增加可以減少,並沒有一定的確定值。
static void _SetPos(int x, int y)
{
COORD position;
position.X = x;
position.Y = y;
HANDLE handle;
handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, position);
}
這是貪吃蛇中我所用的游標移動函式,在輸出牆的時候就可以使用了,這樣能讓程式方便很多。
static void _DisplayWall(const UI *pUI)
{
int i = 0;
//牆的上邊界
_SetPos(pUI->MarginLeft, pUI->MarginTop);
for (i=0; i < pUI->GameWidth + 2; i++)
{
printf("%s", pUI->WallBlock);
}
//牆的下邊界
_SetPos(pUI->MarginLeft, pUI->MarginTop+pUI->GameHeight);
for (i=0; i < pUI->GameWidth + 2; i++)
{
printf("%s", pUI->WallBlock);
}
//牆的左邊界
for (i = 0; i < pUI->GameHeight + 1; i++)
{
_SetPos(pUI->MarginLeft, pUI->MarginTop+i);
printf("%s", pUI->WallBlock);
}
//牆的右邊界
for (i = 0; i < pUI->GameHeight + 1; i++)
{
_SetPos(pUI->MarginLeft+(pUI->GameWidth+1)*pUI->BlockWidth, pUI->MarginTop + i);
printf("%s", pUI->WallBlock);
}
}
在輸出牆的時候我們通過不斷的移動座標然後實現對牆體的輸出,當輸出上下邊界的時候就很容易,移動到指定位置之後通過for迴圈不斷的輸出就可以,但是當輸出左邊界和右邊界的時候就需要不斷的換行來移動游標進行輸出。
static void _DisplayDesc(const UI *pUI)
{
char *message[5] =
{
"蛇不可以撞牆不可以撞自己",
"通過座標建控制蛇移動",
"ESC 退出遊戲 SPACE 暫停遊戲",
"版權所有@賈浩男",
"貪吃蛇版本號1.0"
};
int i = 0;
for (i = 0; i < 5; i++)
{
_SetPos(pUI->MarginLeft + (pUI->GameWidth + 2)*pUI->BlockWidth + 3, pUI->GameHeight / 2 - 3 + i);
printf("%s", message[i]);
}
}
這裡是輸出遊戲的一些簡單介紹,這裡內容也是根據自己的情況來進行修改,游標移動的位置建議自己畫一張圖來計算,這裡的位置大家自己看著順眼就好。
static void _CoordinatePosAtXY(const UI *pUI, int x, int y)
{
_SetPos(pUI->MarginLeft + (1 + x) * pUI->BlockWidth,
pUI->MarginTop + 1 + y);
}
這個函式可有可無,我之前在游標移動函式的部落格中也說過,游標移動函式的X,Y位置都是在DOS框的左上角,但是我在貪吃蛇的時候遊戲介面的左上角角才是我的0,0值這樣才更容易理解。
也就是說我們系統預設的是綠色的座標點,我們現在想以紅色是座標點,這時候就需要一個函式來進行轉換,如果清楚margintop和marginleft兩個引數是代表什麼的話,這裡還是很好理解的。並且大家還會看到,為什麼上邊的pUI->MarginLeft + (1 + x) * pUI->BlockWidth還乘了一個pUI->BlockWidth,這裡如果不清楚的話很難想到,因為我們這裡所輸出的方塊,雖然看起來是一個正方形,但是這個圖形其實是一個寬是1,長是2的圖形,所以這裡其實他並不是一個正方形。
// 重置游標到螢幕下方,主要是為了顯示的美觀
static void _ResetCursorPosition(const UI *pUI)
{
_SetPos(0,pUI->GameHeight+pUI->MarginTop+1);
}
這裡是將游標移動到大家看不到的地方,也就是在遊戲框介面的下方,其實大家可以不寫這個函式,然後執行這個函式會發現,當你的蛇移動的時候,蛇中會有一個移動的游標,影響美觀。但是這裡不能向下移動的太多,如果移動的太多的話,螢幕會隨著游標的下移,走到下邊、
UI * UIInitialize(int width, int height)
{
int marginleft = 1;
int marginheight = 1;
int RightWidth = 10;
int DownHeight = 2;
UI *pUI = (UI*)malloc(sizeof(UI));
pUI->GameWidth = width;
pUI->GameHeight = height;
pUI->MarginLeft = marginleft;
pUI->MarginTop = marginheight;
pUI->FoodBlock = "█";
pUI->WallBlock = "█";
pUI->SnakeBlock = "█";
pUI->BlockWidth = 2;
pUI->WindowWidth = pUI->MarginLeft + (pUI->GameWidth + 2)*pUI->BlockWidth + RightWidth;
pUI->WindowHeight = pUI->MarginTop + pUI->GameHeight + DownHeight + 2;
return pUI;
}
這是UI介面的結構體初始化函式,這裡很多值都是可以根據自己來改變的,
// 顯示遊戲嚮導
void UIDisplayWizard(const UI *pUI)
{
int i = 0;
char *message[3]
{
"歡迎來到我的貪吃蛇的遊戲",
"用上、下、左、右控制蛇的移動",
"當前每個食物獲得十分"
};
i = 0;
_SetPos((pUI->WindowWidth - strlen(message[i])) / 2, pUI->WindowHeight / 2);
printf("%s\n", message[i]);
_SetPos((pUI->WindowWidth - strlen(message[i])) / 2, pUI->WindowHeight / 2+2);
system("pause");
system("cls");
i = 1;
_SetPos((pUI->WindowWidth - strlen(message[i])) / 2, pUI->WindowHeight / 2-2);
printf("%s\n", message[i]);
i = 2;
_SetPos((pUI->WindowWidth - strlen(message[i])) / 2, pUI->WindowHeight / 2);
printf("%s\n", message[i]);
_SetPos((pUI->WindowWidth - strlen(message[i])) / 2, pUI->WindowHeight / 2 + 2);
system("pause");
system("cls");
}
遊戲嚮導介面是自己可以隨意定的,可有可無,位置也是可以根據自己來改變的。
// 顯示分數資訊
void UIDisplayScore(const UI *pUI, int score, int scorePerFood)
{
_SetPos(pUI->MarginLeft + (pUI->GameWidth + 2)*pUI->BlockWidth + 3, pUI->GameHeight / 2 - 4);
printf("總得分:%3d 每個食物得分:%2d", score, scorePerFood);
_ResetCursorPosition(pUI);
}
// 顯示遊戲整體,包括牆、右邊的資訊,不包括蛇和食物的顯示
void UIDisplayGameWindow(const UI *pUI, int score, int scorePerFood)
{
_DisplayWall(pUI);
UIDisplayScore(pUI, score, scorePerFood);
_DisplayDesc(pUI);
_ResetCursorPosition(pUI);
}
//之後的x,y都是經過_CoordinatePosAtXY(const struct UI *pUI, int x, int y)轉換的xy
// 在x,y處顯示食物,x,y都是字元個數
void UIDisplayFoodAtXY(const UI *pUI, int x, int y)
{
_CoordinatePosAtXY(pUI, x, y);
printf("%s", pUI->FoodBlock);
_ResetCursorPosition(pUI);
}
// 在x,y處顯示蛇的一個結點,x,y都是字元個數
void UIDisplaySnakeBlockAtXY(const UI *pUI, int x, int y)
{
_CoordinatePosAtXY(pUI, x, y);
printf("%s", pUI->SnakeBlock);
_ResetCursorPosition(pUI);
}
// 在遊戲區域居中顯示遊戲退出訊息
void UIShowMessage(const UI *pUI, const char *message)
{
_SetPos(pUI->MarginLeft + (1 + pUI->GameWidth / 2)*pUI->BlockWidth - sizeof(message) / 2, (pUI->MarginTop + pUI->GameHeight) / 2);
printf("%s", message);
_ResetCursorPosition(pUI);
}
// 銷燬 UI 資源
void UIDeinitialize( UI *pUI)
{
free(pUI);
pUI = NULL;
}
// 清空x,y處的顯示塊,x,y都是字元個數
void UICleanBlockAtXY(const UI *pUI, int x, int y)
{
_CoordinatePosAtXY(pUI, x, y);
int i = 0;
printf(" ");
_ResetCursorPosition(pUI);
}
這裡我想把這個函式單獨提出來說一下,其他的函式相對來說都是容易理解的,這個函式看起來也很簡單,但是這裡有一個點,就是當你輸出的時候printf(" ");這裡輸出的是兩個空格,而不是一個,如果輸出一個的話,會有問題,這也是我之前所說的,構成的小方塊是一個寬是1的,這裡需要兩個小方塊,所以會輸出兩個空格。
UI介面到這裡就結束了,主要就是游標移動函式的應用,以及一些美觀性問題。
int main()
{
UI *pUI = UIInitialize(28, 27);
UIDisplayWizard(pUI);
UIDisplayGameWindow(pUI, 110, 10);
system("pause");
return 0;
}
這裡可以寫一個函式來驗證一下之前所寫的一些函式是否正確。
typedef struct Position//座標結構體
{
int x;
int y;
}Position;
typedef struct Node//蛇連結串列結構體包含兩部分 座標和下一個
{
Position position;
Node * pNext;
}Node;
enum Direction//列舉型別包含蛇的移動方向 上下左右四個
{
UP,
DOWN,
LEFT,
RIGHT
};
typedef struct Snake//蛇結構體包含兩個內容一個是蛇移動的方向以及蛇的身體
{
enum Direction direction;
Node *pBody;
}Snake;
之後是蛇體部分的,蛇要包括兩個部分,一是蛇的身體,一是蛇移動的方向。蛇的方向因為只有四種,所以這裡我們用了列舉型別來代表蛇的移動方向。另一個就是蛇的身體,蛇的身體這裡不用想那必然是連結串列。連結串列代表著蛇的身體,所以這個結構體也包括兩個一個是每個結構體也就是蛇身體每一塊的座標位置,另一個就是連結串列中必備的next指標用來找到下一個結點。座標結構體自然不用說,兩個X座標和Y座標。
static void _InitializeSnake(Snake *pSnake)
{
int i = 0;
int FirstLength = 3;//初始時蛇長度為3不為0
int x = 5;
int y = 5;//遊戲初始時蛇的座標位置注意不要太大不然可能測試的時候會和牆重合
Node *pNode;
pSnake->direction = RIGHT;
pSnake->pBody = NULL;
for (; i < FirstLength; i++)
{
pNode = (Node*)malloc(sizeof(Node));
pNode->position.x = x + i;
pNode->position.y = y ;
pNode->pNext = pSnake->pBody;//這裡用的是頭插,先將pNode指向蛇身體,然後讓蛇身體指標指向新加入的結點
pSnake->pBody = pNode;
}
}
首先是對蛇結構體的初始化,這裡我對蛇初始化長度為3,在初始位置是在(5,5)位置重新整理蛇。蛇因為是3個方塊所以是需要通過for迴圈來實現,然後通過頭插的方式每次不斷的在連結串列頭插入資料。
static void _GenerateFood(struct Game *pGame)
{
int x = 0;
int y = 0;
do
{
x = rand() % (pGame->Width-5);
y = rand() % (pGame->Height-5);
} while (!_IsOverlapSnake(x, y, &pGame->snake));
pGame->FoodPosition.x = x;
pGame->FoodPosition.y = y;
}
這裡是生成食物的函式,這裡是通過do...while迴圈來實現,因為食物的位置是不能隨意刷的有限需要在重新整理之後進行檢查,一直到食物方塊和蛇的身體不重合是才可以。
static bool _IsOverlapSnake(int x, int y, const struct Snake *pSnake)
{
Node *pNode;
for (pNode = pSnake->pBody; pNode != NULL; pNode = pNode->pNext)
{
if ((pNode->position.x == x) && (pNode->position.y == y))
return false;
}
return true;
}
這就時判斷你所生成的方塊和蛇身體是不是發生了重疊,通過for迴圈也好while迴圈也好,來不斷的遍歷你的蛇身體,判斷每一個蛇塊是不是有重疊,如果有一個那就不行。
static struct Position _GetNextPosition(const struct Snake *pSnake)
{
int Nowx, Nowy;
int Nextx, Nexty;
Position NextPosition;
Nowx = pSnake->pBody->position.x;
Nowy = pSnake->pBody->position.y;
Nextx = Nowx;
Nexty = Nowy;//如果不將Now給了NEXT的話,在switch語句結束之後,沒辦法判斷是x變了還是y變了,沒辦法賦值
switch (pSnake->direction)
{
case UP:
Nexty = Nowy - 1;
break;
case DOWN:
Nexty = Nowy + 1;
break;
case LEFT:
Nextx = Nowx - 1;
break;
case RIGHT:
Nextx = Nowx + 1;
break;
}
NextPosition.x = Nextx;
NextPosition.y = Nexty;//直接將next值賦值進去,無論變沒變,
return NextPosition;
}
需要注意的地方已經在函式中用註釋表明了,通過蛇結構體中的列舉型別來判斷當前蛇的移動方向,然後開始改變蛇身體的座標。
static bool _IsWillEatFood(struct Position nextPosition, struct Position foodPosition)
{
if ((nextPosition.x == foodPosition.x) && (nextPosition.y == foodPosition.y))
return true;
return false;
}
這個是判斷蛇是否吃到了食物的,這裡和上邊所結合,是因為每次移動的時候你都會判斷一個他的方向後獲得一個他下一個移動的位置,這裡就用上邊所獲得的下一個位置的座標來判斷,是不是你食物所在的位置,如果是的話就返回真,如果不是的話就返回假。
// 增長蛇並且進行顯示
static void _GrowSnakeAndDisplay(struct Snake *pSnake, struct Position foodPosition, const struct UI *pUI)
{
Node *pHead = (Node*)malloc(sizeof(Node));
pHead->position.x = foodPosition.x;
pHead->position.y = foodPosition.y;
pHead->pNext = pSnake->pBody;
pSnake->pBody = pHead;
UIDisplaySnakeBlockAtXY(pUI, foodPosition.x, foodPosition.y);
return;
}
// 增長蛇頭
static void _AddHead(struct Snake *pSnake, struct Position nextPosition, const struct UI *pUI)
{
Node *Shead = (Node*)malloc(sizeof(Node));
Shead->position.x = nextPosition.x;
Shead->position.y = nextPosition.y;
Shead->pNext = pSnake->pBody;
pSnake->pBody = Shead;
UIDisplaySnakeBlockAtXY(pUI, nextPosition.x, nextPosition.y);
return;
}
// 刪除蛇尾
static void _RemoveTail(struct Snake *pSnake, const struct UI *pUI)
{
Node *Tail = pSnake->pBody;
while (Tail->pNext->pNext != NULL)//這裡要找倒數第二個元素對倒數第一個元素進行操作
{
Tail = Tail->pNext;
}
UICleanBlockAtXY(pUI, Tail->pNext->position.x, Tail->pNext->position.y);
free(Tail->pNext);
Tail->pNext = NULL;//一定要將倒數第二個元素的next指向空
}
// 移動蛇並且進行顯示
static void _MoveSnakeAndDisplay(struct Snake *pSnake, struct Position nextPosition, const struct UI *pUI)
{
_RemoveTail(pSnake, pUI);
_AddHead(pSnake, nextPosition, pUI);//每次只需要移動蛇的頭和尾 刪去尾巴增加對應位置的頭
}
這四個函式是對蛇移動的操作,這裡的在移動蛇的時候和陣列不太一樣,用陣列的話反倒會很簡單,直接將特定位置的二維陣列設定為方塊,某些不再需要顯示的地方賦值為空,但是連結串列就不一樣了,如果你在移動的時候不改變連結串列而是改變連結串列的位置會很麻煩,因為在結構體宣告的時候我已經說了,蛇每一個方塊都包括一個他的座標,這裡你要是改的話,會很麻煩,需要將每一個位置的方塊的座標都要改變。所以這裡在移動的時候我們採用的是插入蛇頭刪除蛇尾中間不操作的方式來實現的。所以在移動蛇的時候先刪除尾再插入頭直接呼叫這兩個函式就可以了。
// 增長蛇並且進行顯示
static void _GrowSnakeAndDisplay(struct Snake *pSnake, struct Position foodPosition, const struct UI *pUI)
{
Node *pHead = (Node*)malloc(sizeof(Node));
pHead->position.x = foodPosition.x;
pHead->position.y = foodPosition.y;
pHead->pNext = pSnake->pBody;
pSnake->pBody = pHead;
UIDisplaySnakeBlockAtXY(pUI, foodPosition.x, foodPosition.y);
return;
}
// 增長蛇頭
static void _AddHead(struct Snake *pSnake, struct Position nextPosition, const struct UI *pUI)
{
Node *Shead = (Node*)malloc(sizeof(Node));
Shead->position.x = nextPosition.x;
Shead->position.y = nextPosition.y;
Shead->pNext = pSnake->pBody;
pSnake->pBody = Shead;
UIDisplaySnakeBlockAtXY(pUI, nextPosition.x, nextPosition.y);
return;
}
// 刪除蛇尾
static void _RemoveTail(struct Snake *pSnake, const struct UI *pUI)
{
Node *Tail = pSnake->pBody;
while (Tail->pNext->pNext != NULL)//這裡要找倒數第二個元素對倒數第一個元素進行操作
{
Tail = Tail->pNext;
}
UICleanBlockAtXY(pUI, Tail->pNext->position.x, Tail->pNext->position.y);
free(Tail->pNext);
Tail->pNext = NULL;//一定要將倒數第二個元素的next指向空
}
// 移動蛇並且進行顯示
static void _MoveSnakeAndDisplay(struct Snake *pSnake, struct Position nextPosition, const struct UI *pUI)
{
_RemoveTail(pSnake, pUI);
_AddHead(pSnake, nextPosition, pUI);//每次只需要移動蛇的頭和尾 刪去尾巴增加對應位置的頭
}
這裡是判斷蛇是否還活著,活著就讓遊戲繼續執行,沒活著就要退出遊戲了,蛇只有兩種死法一種是被自己撞死,也就是蛇撞到了自己,或者是撞到了牆。這裡還是隻需要呼叫這兩個函式就可以。
// 蛇是否撞牆了
static bool _IsKilledByWall(const struct Node *pHead, int width, int height)
{
if (pHead->position.x < 0 || pHead->position.x >= width) {
return false;
}
if (pHead->position.y < 0 || pHead->position.y >= height) {
return false;
}
return true;
}
// 蛇是否撞自己了
static bool _IsKilledBySelf(const struct Node *pHead, const struct Snake *pSnake)
{
Node *pNode = pSnake->pBody->pNext;
while (pNode)//從第二個元素開始一直到最後一個元素
{
if ((pHead->position.x == pNode->position.x) && (pHead->position.y == pNode->position.y))
return true;
pNode = pNode->pNext;
}
return false;
}
這裡其實思路都很簡單,是否撞到牆只是通過座標來判斷就可以了,看看蛇是不是在你所設定的座標系之內,在的話就沒死不在就死了,蛇是否撞到自己也是比較簡單的通過蛇頭的座標來遍歷,從第二個開始,看看第一個的蛇頭是不是存在可能和蛇其他位置相同的座標,如果有的話,那就是撞到自己,就死了。
static void _HandleDirective(struct Game *pGame)
{
if (GetAsyncKeyState(VK_UP) &&(pGame->snake.direction != DOWN))
{
pGame->snake.direction = UP;
}
else
{
if (GetAsyncKeyState(VK_DOWN) && (pGame->snake.direction != UP))
{
pGame->snake.direction = DOWN;
}
else
{
if (GetAsyncKeyState(VK_LEFT) && (pGame->snake.direction != RIGHT))
{
pGame->snake.direction = LEFT;
}
else
{
if (GetAsyncKeyState(VK_RIGHT) && (pGame->snake.direction != LEFT))
{
pGame->snake.direction = RIGHT;
}
}
}
}
return;
}
這是來操作蛇方向的函式,通過讀取鍵盤的資料來操作蛇,但是這裡需要注意的是,如果你的蛇當前方向是向上走,那你現在按下是不能操作的,同樣其他方面也是,不能讓你的蛇向相反方向移動。
之後就進入遊戲整體的操作階段。
typedef struct Game//遊戲結構體包括 蛇、食物、介面
{
int Score;//當前得分
int ScorePerFood;//每一個食物的得分
int Width;//遊戲區域寬度用來傳參
int Height;//遊戲區域高度用來傳參
Snake snake;
Position FoodPosition;//食物實際上就是在某一個座標上進行輸出字元
}Game;
enum ExitStatus {
QUIT,//使用者退出
KILLED_BY_WALL,// 撞牆死亡
KILLED_BY_SELF// 撞自己死亡
};
蛇結構體和蛇狀態列舉型別
Game * CreateGame()
{
Game *pGame = (Game*)malloc(sizeof(Game));
pGame->Width = 27;
pGame->Height = 28;
pGame->Score = 0;
pGame->ScorePerFood = 10;
_InitializeSnake(&pGame->snake);
_GenerateFood(pGame);
return pGame;
}
一個初始化函式,初始化的同時要初始化你的蛇,你的第一個食物。
void StartGame(Game *pGame)
{
int Time = 300;
UI *pUI = UIInitialize(pGame->Width, pGame->Height);
enum ExitStatus exitStatus = QUIT;//給退出狀態賦值
UIDisplayWizard(pUI);//列印初始介面提示資訊
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
UIDisplayFoodAtXY(pUI, pGame->FoodPosition.x, pGame->FoodPosition.y);//顯示出來一個食物
_DisplaySnake(pUI, &pGame->snake);//顯示蛇
while (1)
{
if (GetAsyncKeyState(VK_ESCAPE))
break;
else
{
if (GetAsyncKeyState(VK_SPACE))
{
_Pause();
}
}
_HandleDirective(pGame);//判斷方向指令
Position NextPosition = _GetNextPosition(&pGame->snake);//獲取下一個將要移動的方向
if (_IsWillEatFood(NextPosition, pGame->FoodPosition)) {
pGame->Score += pGame->ScorePerFood;
UIDisplayScore(pUI, pGame->Score, pGame->ScorePerFood);
_GrowSnakeAndDisplay(&pGame->snake, pGame->FoodPosition, pUI);
_GenerateFood(pGame);
UIDisplayFoodAtXY(pUI, pGame->FoodPosition.x, pGame->FoodPosition.y);
}
else {
_MoveSnakeAndDisplay(&pGame->snake, NextPosition, pUI);
}
if (!_IsSnakeAlive(pGame, &exitStatus)) {
break;
}
if (GetAsyncKeyState(VK_F1))
{
Time = Time - 50;
pGame->ScorePerFood = pGame->ScorePerFood + 10;
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
}
if (GetAsyncKeyState(VK_F2))
{
if (Time < 300)
{
Time = Time + 10;
pGame->ScorePerFood = pGame->ScorePerFood - 10;
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
}
}
Sleep(Time);
}
char *messages[3];
messages[QUIT] = "遊戲結束";
messages[KILLED_BY_WALL] = "遊戲結束,撞到牆了";
messages[KILLED_BY_SELF] = "遊戲結束,撞到自己了";
UIShowMessage(pUI, messages[exitStatus]);
}
這就是最後一個大的函數了。
UI *pUI = UIInitialize(pGame->Width, pGame->Height);
enum ExitStatus exitStatus = QUIT;//給退出狀態賦值
UIDisplayWizard(pUI);//列印初始介面提示資訊
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
UIDisplayFoodAtXY(pUI, pGame->FoodPosition.x, pGame->FoodPosition.y);//顯示出來一個食物
_DisplaySnake(pUI, &pGame->snake);//顯示蛇
先初始化資訊,然後開始建立牆,之後進行牆體右側資訊輸出,以及生成食物生成蛇。
while (1)
{
if (GetAsyncKeyState(VK_ESCAPE))
break;
else
{
if (GetAsyncKeyState(VK_SPACE))
{
_Pause();
}
}
_HandleDirective(pGame);//判斷方向指令
Position NextPosition = _GetNextPosition(&pGame->snake);//獲取下一個將要移動的方向
if (_IsWillEatFood(NextPosition, pGame->FoodPosition)) {
pGame->Score += pGame->ScorePerFood;
UIDisplayScore(pUI, pGame->Score, pGame->ScorePerFood);
_GrowSnakeAndDisplay(&pGame->snake, pGame->FoodPosition, pUI);
_GenerateFood(pGame);
UIDisplayFoodAtXY(pUI, pGame->FoodPosition.x, pGame->FoodPosition.y);
}
else {
_MoveSnakeAndDisplay(&pGame->snake, NextPosition, pUI);
}
if (!_IsSnakeAlive(pGame, &exitStatus)) {
break;
}
if (GetAsyncKeyState(VK_F1))
{
Time = Time - 50;
pGame->ScorePerFood = pGame->ScorePerFood + 10;
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
}
if (GetAsyncKeyState(VK_F2))
{
if (Time < 300)
{
Time = Time + 10;
pGame->ScorePerFood = pGame->ScorePerFood - 10;
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
}
}
Sleep(Time);
}
之後蛇的操作是通過while迴圈來實現的,迴圈括號內永遠為真,如果需要終止遊戲只需要在迴圈裡用break即可。
if (GetAsyncKeyState(VK_ESCAPE))
break;
else
{
if (GetAsyncKeyState(VK_SPACE))
{
_Pause();
}
}
這是在開始進行判斷,是不是按了鍵盤的ESC如果是就退出遊戲,如果安了空格那就將遊戲暫停。
static void _Pause()
{
while (1)
{
Sleep(300);
if (GetAsyncKeyState(VK_SPACE))
{
break;
}
}
}
暫停函式這裡也是十分的巧妙,因為這裡的暫停是一種偽暫停,這裡其實程式碼量還是相對來說小的,所以計算機計算起來還是比較快的,所以你必須要將你的程式進行一定的暫停,如果不暫停的話蛇可能瞬間就會出界。所以Sleep(Time);while迴圈最後是要將你的程式進行一定的暫停,這裡暫停也是不斷的將你的程式進行暫停,不斷的小暫停用while迴圈來組成一個大暫停,知道遇到空格才會退出while迴圈。
其他理解起來並不困難。
if (GetAsyncKeyState(VK_F1))
{
Time = Time - 50;
pGame->ScorePerFood = pGame->ScorePerFood + 10;
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
}
if (GetAsyncKeyState(VK_F2))
{
if (Time < 300)
{
Time = Time + 10;
pGame->ScorePerFood = pGame->ScorePerFood - 10;
UIDisplayGameWindow(pUI, pGame->Score, pGame->ScorePerFood);//列印遊戲除去蛇以外的牆體、右側資訊
}
}
這是我們的加速操作和減速操作,通過F1和F2來實現,操作就是將你的程式暫停時間減短就可以。
int main()
{
srand((unsigned int)time(NULL));
struct Game *pGame = CreateGame();
StartGame(pGame);
DestroyGame(pGame);
system("pause");
return 0;
}
這是整個專案的主函式。
其實我認為,當前我們所寫的貪吃蛇是技術性還是比較低的,畢竟大部分程式碼都是我們所能看懂的,但是自己確很難獨自完成,這是自己程式碼能力差的一個體現,經常會有有思路但是寫不出程式碼的情況,我認為自己在專案上的經驗還是很低,在遇到大型程式是還是沒有頭緒,還需要很大的提升和鍛鍊。