8. 核心邏輯實現分析
8.1 游戲主邏輯
程序開始就設置程序支持本地模式,然后進入程序的主邏輯。
主邏輯分為3個過程:
? 游戲開始(GameStart)完成游戲的初始化。
? 游戲運行(GameRun)完成游戲運行邏輯的實現。
? 游戲運行(GameEnd)完成游戲結束的說明、資源的釋放。
#include <locale.h>
void test()
{int ch = 0;srand((unsigned int)time(NULL));do{Snake snake = { 0 };GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("再來?局嗎?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y');SetPos(0, 27);
}int main()
{//修改當前地區為本地模式,為了支持中文寬字符的打印setlocale(LC_ALL, "");//測試邏輯test();return 0;
}
8.2 游戲開始
這個模塊完成游戲的初始化任務。
? 控制臺窗口大小的設置
? 控制臺窗口名稱的設置
? 鼠標光標的隱藏
? 打印歡迎界面
? 創建地圖
? 初始化蛇
? 創建第一個食物
void GameStart(pSnake ps)
{//設置控制臺窗?的??,30?,100列//mode 為DOS命令system("mode con cols=100 lines=30");//設置cmd窗?名稱system("title 貪吃蛇"); //獲取標準輸出的句柄(?來標識不同設備的數值)HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//影藏光標操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(hOutput, &CursorInfo);//獲取控制臺光標信息 CursorInfo.bVisible = false; //隱藏控制臺光標SetConsoleCursorInfo(hOutput, &CursorInfo);//設置控制臺光標狀態//打印歡迎界?WelcomeToGame();//打印地圖CreateMap();//初始化蛇InitSnake(ps);//創造第?個?物CreateFood(ps);
}
8.2.1 打印歡迎界面
在游戲正式開始之前,做一些功能提醒。
void WelcomeToGame()
{SetPos(40, 15);printf("歡迎來到貪吃蛇?游戲");SetPos(40, 25); //讓按任意鍵繼續的出現的位置好看點system("pause");system("cls");SetPos(25, 12);printf("? ↑ . ↓ . ← . → 分別控制蛇的移動, F3為加速,F4為減速\n");SetPos(25, 13);printf("加速將能得到更?的分數。\n");SetPos(40, 25);//讓按任意鍵繼續的出現的位置好看點system("pause");system("cls");
}
8.2.2 創建地圖
創建地圖就是將墻打印出來,因為是寬字符打印,所有使用wprintf函數,打印格式串前使用L
打印地圖的關鍵是要算好坐標,才能在想要的位置打印墻體。
墻體打印的寬字符:
#define WALL L'□'
易錯點:就是坐標的運算。
? 上:(0,0)到(56,0)
? 下:(0,26)到(56,26)
? 左:(0,1)到(0,25)
? 右:(56,1)到(56,25)
創建地圖函數CreateMap
void CreateMap()
{int i = 0;//上(0,0)-(56, 0)SetPos(0, 0);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//下(0,26)-(56, 26)SetPos(0, 26);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//左//x是0,y從1開始增?for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}//x是56,y從1開始增?for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}
8.2.3 初始化蛇身
蛇最開始長度為5節,每節對應鏈表的一個節點,蛇身的每一個節點都有自己的坐標。
創建5個節點,然后將每個節點存放在鏈表中進行管理。創建完蛇身后,將蛇的每一節打印在屏幕上。
? 規定蛇的初始位置從(24,5)開始。
再設置當前游戲的狀態,蛇移動的速度,默認的方向,初始成績,蛇的狀態,每個食物的分數。
? 游戲狀態時OK
? 蛇的移動速度:200ms
? 蛇的默認方向:RIGHT
? 初始成績:0
? 每個食物的分數:10
蛇身打印的寬字符:
#define BODY L'●'
初始化蛇身函數:InitSnake
//初始化蛇
void InitSnake(pSnake ps)
{//創建5個蛇身的結點pSnakeNode cur = NULL;int i = 0;//創建蛇身結點,并初始化坐標//頭插法for (i = 0; i < 5; i++){//創建蛇身結點cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //一次循環開始,cur先賦值指向下一塊空間if (cur == NULL){perror("InitSnake():malloc()");return;}//設置坐標——從左向右創建cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;//頭插法——一開始以右為蛇頭,鏈表從右向左,右邊新創建的是頭if (ps->pSnake == NULL){ps->pSnake = cur;}else{cur->next = ps->pSnake;//給新頭的next賦值——舊頭地址賦給新next,讓新頭指向舊身ps->pSnake = cur;//把新創建的作為蛇頭 //一次循環結束,cur和pSnake指向同一}}//打印蛇身(遍歷)cur = ps->pSnake;//繼續使用遍歷變量while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//初始化貪吃蛇的數據ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;//Windows中Sleep函數的單位是:ms(毫秒)ps->status = OK;
}
8.2.4 創建第一個食物
? 先隨機生成食物的坐標。
????????? x坐標必須是2的倍數。
????????? 食物的坐標不能和蛇身每個節點的坐標重復。
? 創建食物節點,打印食物。
食物打印的寬字符:
#define FOOD L'★'
創建食物的函數:CreateFood
//創建食物
void CreateFood(pSnake ps)
{int x = 0;int y = 0;again://x為奇數執行一次以上的循環//產生的x坐標應該是2的倍數,這樣才可能和蛇頭坐標對齊。do{x = rand() % 53 + 2; //1.隨機坐標 //y = rand() % 24 + 1; y = rand() % 25 + 1; } while (x % 2 != 0);//生成指定范圍內的隨機數:x(2-54且2的倍數),y(1-25) //2./在墻內//坐標和蛇的身體的每個節點的做坐標比較(遍歷)pSnakeNode cur = ps->pSnake; //獲取指向蛇頭的指針//食物不能和蛇身沖突while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;} //3.不在蛇身上//創建食物——類型:蛇身結點類型pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//創建食物if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood;SetPos(x, y);wprintf(L"%lc", FOOD);}
8.3 游戲運行
游戲運行期間,右側打印幫助信息,提示玩家
根據游戲狀態檢查游戲是否繼續,如果是狀態是OK,游戲繼續,否則游戲結束。
如果游戲繼續,就是檢測按鍵情況,確定蛇下一步的方向,或者是否加速減速,是否暫停或者退出游戲。
確定了蛇的方向和速度,蛇就可以移動了。
//游戲運行
void GameRun(pSnake ps)
{//打印幫助信息PrintHelpInfo();do{//當前的分數情況SetPos(62, 10);printf("總分:%5d\n", ps->Score);SetPos(62, 11);printf("食物的分值:%02d\n", ps->FoodWeight);//沒有02則由10變8會顯示由10變80 //檢測按鍵//上、下、左、右、ESC、空格、F3、F4if (KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上鍵并且當前方向不是朝下走{ps->dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->dir != UP){ps->dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT){ps->dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT){ps->dir = RIGHT;}else if (KEY_PRESS(VK_ESCAPE)){ps->status = ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戲要暫定pause();//暫定和回復暫定}else if (KEY_PRESS(VK_F3)){if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight > 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//蛇每次?定之間要休眠的時間,時間短,蛇移動速度就快//走一步SnakeMove(ps);//睡眠一下Sleep(ps->SleepTime);} while (ps->status == OK);//正常狀態下是死循環,一直執行
}
8.3.1 KEY_PRESS
檢測按鍵狀態,我們封裝了一個宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
8.3.2 PrintHelpInfo
void PrintHelpInfo()
{//打印提?信息SetPos(64, 15);printf("不能穿墻,不能咬到??\n");SetPos(64, 16);printf("?↑.↓.←.→分別控制蛇的移動.");SetPos(64, 17);printf("F1 為加速,F2 為減速\n");SetPos(64, 18);printf("ESC :退出游戲.space:暫停游戲.");SetPos(64, 20);printf("?特就業課@版權");
}
8.3.3 蛇身移動SnakeMove(重難點)
先創建下一個節點,根據移動方向和蛇頭的坐標,蛇移動到下一個位置的坐標。
確定了下一個位置后,看下一個位置是否是食物(NextIsFood),是食物就做吃食物處理 (EatFood),如果不是食物則做前進一步的處理(NoFood)。
蛇身移動后,判斷此次移動是否會造成撞墻(KillByWall)或者撞上自己蛇身(KillBySelf),從而影響游戲的狀態。
//蛇的移動
void SnakeMove(pSnake ps)
{//創建下?個節點pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("SnakeMove()::malloc()");return;}pNext->next = NULL;//根據按鍵·反饋蛇的下一個結點的坐標//確定下?個節點的坐標,下?個節點的坐標根據,蛇頭的坐標和?向確定switch (ps->dir){case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;//——縱減一break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break;case LEFT:pNext->x = ps->pSnake->x - 2;//——橫減二pNext->y = ps->pSnake->y;break;case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}//如果下一個坐標處是食物if (NextIsFood(ps, pNext)){//是食物就吃掉EatFood(ps, pNext);}else{//不是食物就正常一步NotEatFood(ps, pNext);}//檢測撞墻KillByWall(ps);//檢測撞到自己KillBySelf(ps);
}
8.3.3.1 NextIsFood
//pSnakeNode psn 是下?個節點的地址
//pSnake ps 維護蛇的指針
int NextIsFood(pSnakeNode psn, pSnake ps)
{return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
8.3.3.2 EatFood
//pSnakeNode psn 是下?個節點的地址
//pSnake ps 維護蛇的指針
void EatFood(pSnakeNode psn, pSnake ps)
{//頭插法psn->next = ps->_pSnake;ps->_pSnake = psn;pSnakeNode cur = ps->_pSnake;//打印蛇while (cur){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}ps->_Socre += ps->_foodWeight;//釋放?物節點free(ps->_pFood);//創建新的?物CreateFood(ps);
}
8.3.3.3 NoFood
將下一個節點頭插入蛇的身體,并將之前蛇身最后一個節點打印為空格,釋放掉蛇身的最后一個節點。
易錯點:這里最容易錯誤的是,釋放掉最后一個結點之后,還需要將指向在最后一個結點的指針改為NULL,保證蛇尾打印可以正常結束,不會越界訪問。
//pSnakeNode psn 是下?個節點的地址
//pSnake ps 維護蛇的指針
//不是食物就正常進一步(1.頭插;2.打印新蛇;3.留白舊尾;4.釋放舊尾)
void NotEatFood(pSnake ps, pSnakeNode pNext)
{//頭插法pNext->next = ps->pSnake;ps->pSnake = pNext;//打印新蛇,留白蛇尾,釋放尾結點——先找到尾結點的前驅結點pSnakeNode cur = ps->pSnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//將尾節點的位置打印成空白字符——注意是兩個空格字符SetPos(cur->next->x, cur->next->y);printf(" ");//釋放結點free(cur->next);cur->next = NULL;//易錯
}
8.3.3.4 KillByWall
判斷蛇頭的坐標是否和墻的坐標沖突
//pSnake ps 維護蛇的指針
//檢測是否撞墻
void KillByWall(pSnake ps)
{if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;}
}//int KillByWall(pSnake ps)
//{
// if (ps->pSnake->x == 0 ||
// ps->pSnake->x == 56 ||
// ps->pSnake->y == 0 ||
// ps->pSnake->y == 26)
// {
// ps->status = KILL_BY_WALL;
// return 1;
// }
// return 0;
//}
8.3.3.5 KillBySelf
判斷蛇頭的坐標是否和蛇身體的坐標沖突
1 //pSnake ps 維護蛇的指針
//檢測是否撞自己
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->pSnake->next;//從第二個節點開始while (cur){if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}//int KillBySelf(pSnake ps)
//{
// pSnakeNode cur = ps->pSnake->next;//從第二個節點開始
// while (cur)
// {
// if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
// {
// ps->status = KILL_BY_SELF;
// return 1;
// }
// cur = cur->next;
// }
// return 0;
//}
這兩個函數建議寫成返回值void的形式——足以實現預定的功能,通過修改貪吃蛇結構體的狀態參數。
寫成int返回值類型,沒必要接收返回值,而且和其他大部分的函數接口沒有保持統一性——其他大部分都是void的返回值類型。
保持接口的一致性有利于對這些函數的使用,不用去記哪些函數應該怎么用,都是一樣的用法。
8.4 游戲結束
游戲狀態不再是OK(游戲繼續)的時候,要告知游戲結束的原因,并且釋放蛇身節點。
//結束判定
void GameEnd(pSnake ps)
{SetPos(15, 12);switch (ps->status){case ESC:printf("主動退出游戲,正常退出\n");break;case KILL_BY_WALL:printf("很遺憾,您撞墻了,游戲結束\n");break;case KILL_BY_SELF:printf("很遺憾,您咬到自己了,游戲結束\n");break;}//釋放貪吃蛇的鏈表資源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;//釋放蛇?的節點while (cur){del = cur;cur = cur->next;free(del);}free(ps->pFood);ps = NULL;
}
9. 參考代碼
完整代碼實現,分3個文件實現
test.c
test.c——貪吃蛇游戲的測試。
#include "Snake.h"
#include <locale.h>//大BUG:搞了一個小時沒搞懂
//誘因:輸入Y / y程序直接結束?
//因為只有兩個“按任意鍵繼續……”,按完兩下空格鍵,但是進入游戲界面還要按一下空格鍵蛇才能動(而且只有按空格鍵才行,不像之前兩次任意鍵)
//所以導致需要先清理游戲一開始的空格字符才能重開循環
//getchar();//清理緩沖區的換行符\n
//ch = getchar();
//getchar();//清理緩沖區的換行符\n
//發現兩個任意鍵中其中任何一個使用了空格鍵,在游戲一開始都需要多按一下空格鍵(唯一可行鍵)void test()
{//創建變量,接收用戶輸入的“進行/退出”選項int ch = 0; //getchar()的返回值是int類型,但是用char ch來接收也行;但是getchar()接收失敗返回的EOF是int類型,故此最好使用int類型的變量來接收返回值//srand((unsigned int)time(NULL));加不加這一句,取決于用戶希望每次運行游戲是希望同樣的進程(食物出現的位置、順序都和上一次玩的時候一模一樣),還是不同的進程。do{//用貪吃蛇類型,創建貪吃蛇Snake snake = { 0 };GameStart(&snake);//游戲開始前的初始化——需要改變局部變量的值——傳址調用//getchar();為了測試前面代碼功能的時候,程序執行到這里能夠停下來GameRun(&snake);//玩游戲的過程(開始——進行——結束判定)GameEnd(&snake);//善后的工作(資源釋放……)SetPos(20, 15);printf("再來一局嗎?(Y/N):");ch = getchar();//返回值為int,讀取成功沒事,讀取失敗返回EOF(-1),所以最好將ch設置為int類型getchar();//清理緩沖區的換行符\n——如果沒有第二個getchar(),如果后續代碼再次調用 getchar(),它會直接讀到 \n,而不是等待用戶新輸入。} while (ch == 'Y' || ch == 'y');//SetPos(0, 27);
}int main()
{//修改當前地區為本地模式,為了支持中文寬字符的打印setlocale(LC_ALL, "");//測試邏輯test();//程序退出的提示語,從(0,27)開始打印出來(不加這句代碼,退出語的打印會破壞游戲界面)SetPos(0, 27);return 0;
}
snake.h
snake.h——貪吃蛇游戲中類型的聲明、函數的聲明。
#pragma once#define _CRT_SECURE_NO_WARNINGS 1#include <locale.h>
#include <stdlib.h>
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'//蛇默認的起始坐標
#define POS_X 24
#define POS_Y 5#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )//游戲狀態
enum GAME_STATUS
{OK = 1, //正常運行ESC, //退出KILL_BY_WALL, //撞墻死KILL_BY_SELF //撞蛇死
};//方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};//定義·蛇身結點類型
typedef struct SnakeNode
{int x;int y; //每個蛇結點在控制臺中都有一個地址(坐標),是連續的。struct SnakeNode* next; //每個蛇結點在內存(堆)中都有一個地址, 不是連續的。
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;//貪吃蛇——維護整個游戲的狀態
typedef struct Snake
{pSnakeNode pSnake; //維護整條蛇的指針(鏈表——全結點(利用頭結點維護整個鏈表))pSnakeNode pFood; //指向食物的指針——和蛇身節點是同一個類型,因為它是未來的蛇身結點,而且均是依靠(x,y)坐標來維護int Score; //當前累積的分數int FoodWeight; //一個食物的分數int SleepTime; //蛇休眠的時間,休眠的時間越短,蛇的速度越快,休眠的時間越長,蛇的速度越慢enum GAME_STATUS status;//游戲當前的狀態(進行 / 暫停 / 結束),暫停可以歸類為正常運行enum DIRECTION dir; //蛇當前走的方向(當前向上就不能立刻向下,當前向左就不能立刻向右)//...
}Snake, * pSnake; //壞處就是容易造成混亂——pSnake* phead到底是—級指針還是二級指針很混亂//定位控制臺的光標位置
void SetPos(int x, int y);//游戲開始前的準備
void GameStart(pSnake ps);//打印歡迎界面
void WelcomeToGame();//繪制地圖
void CreateMap();//初始化貪吃蛇
void InitSnake(pSnake ps);//創建食物
void CreateFood(pSnake ps);//游戲運行的整個邏輯
void GameRun(pSnake ps);//打印幫助信息
void PrintHelpInfo();//蛇移動的函數- 每次走一步
void SnakeMove(pSnake ps);//判斷蛇頭的下一步要走的位置處是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);//下一步要走的位置處就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext);//下一步要走的位置處不是食物,不吃食物
void NotEatFood(pSnake ps, pSnakeNode pNext);//檢測是否撞墻
void KillByWall(pSnake ps);//檢測是否撞自己
void KillBySelf(pSnake ps);//游戲結束——資源釋放
void GameEnd(pSnake ps);
snake.c
snake.c——貪吃蛇游戲的相關函數的實現。
//getchar();為了測試前面代碼功能的時候,程序執行到這里能夠停下來
#include "snake.h"//定位控制臺的光標位置
void SetPos(int x, int y)
{//獲得設備句柄HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);//根據句柄設置光標的位置COORD pos = { x, y };SetConsoleCursorPosition(hanlde, pos);
}//打印歡迎信息
void WelcomeToGame()
{//歡迎信息SetPos(38, 10);printf("歡迎來到貪吃蛇小游戲\n");SetPos(40, 20); //讓按任意鍵繼續的出現的位置好看點system("pause"); system("cls");//功能介紹信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 來控制蛇的移動,F3是加速,F4是減速");SetPos(15, 11);printf("加速能得到更高的分數");SetPos(38, 20); //讓按任意鍵繼續的出現的位置好看點system("pause");system("cls");
}//打印地圖
void CreateMap()
{//定位一次,打印一排坐標int i = 0;//上排墻壁:(0,0)-(56,0)SetPos(0, 0);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下排墻壁:(0,26)-(56,26)SetPos(0, 26);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//定位一次,打印一個坐標//左排墻壁:x是0,y從1開始增長for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右排墻壁:x是56,y從1開始增長for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}//getchar();為了測試這個功能的時候,程序執行到這里能夠停下來
}//初始化蛇
void InitSnake(pSnake ps)
{//創建5個蛇身的結點,并初始化坐標//頭插法pSnakeNode cur = NULL;int i = 0;for (i = 0; i < 5; i++){//創建蛇身結點cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //一次循環開始,cur先賦值指向下一塊空間if (cur == NULL){perror("InitSnake():malloc()");return;}//設置每個結點的坐標——從左向右創建cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;//頭插法——一開始以右為蛇頭,鏈表從右向左,右邊新創建的是頭if (ps->pSnake == NULL){ps->pSnake = cur;}else{cur->next = ps->pSnake;//給新頭的next賦值——舊頭地址賦給新next,讓新頭指向舊身ps->pSnake = cur;//把新創建的作為蛇頭 //一次循環結束,cur和pSnake指向同一}}//打印蛇身(遍歷)cur = ps->pSnake;//繼續使用遍歷變量while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//貪吃蛇的其他信息初始化ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;//Windows中Sleep函數的單位是:ms(毫秒)ps->status = OK;//getchar();為了測試這個功能的時候,程序執行到這里能夠停下來
}//創建食物
void CreateFood(pSnake ps)
{int x = 0;int y = 0;again://產生的x坐標應該是2的倍數,這樣才可能和蛇頭坐標對齊。//x為奇數執行一次以上的循環do{//1.隨機坐標//2.在墻內——生成指定范圍內的隨機數x = rand() % 53 + 2; //x(2-54且2的倍數) ,則先生成0-52的隨機數 //y = rand() % 24 + 1; //y(1-25),則先生成0-24的隨機數 y = rand() % 25 + 1; } while (x % 2 != 0); //坐標和蛇的身體的每個節點的做坐標比較(遍歷)pSnakeNode cur = ps->pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;} //3.不在蛇身上//創建食物,直接復用“蛇身結點類型”pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood; //把食物的地址,給到貪吃蛇結構體內部對應的指針,進行維護SetPos(x, y);wprintf(L"%lc", FOOD);//getchar();為了測試這個功能的時候,程序執行到這里能夠停下來
}//游戲初始化
void GameStart(pSnake ps)
{//設置控制臺的信息,窗口大小,窗口名system("mode con cols=100 lines=31"); //貪吃蛇游戲框58列、27行system("title 貪吃蛇");//獲取標準輸出的句柄(用來標識不同設備的數值)HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//隱藏光標CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(handle, &CursorInfo); //獲取控制臺光標信息CursorInfo.bVisible = false; //隱藏控制臺光標SetConsoleCursorInfo(handle, &CursorInfo); //設置控制臺光標狀態//打印歡迎界面WelcomeToGame();//繪制地圖CreateMap();//初始化蛇——需要對參數進行操作,所以:1.需要參數;2.傳址調用InitSnake(ps);//創建食物——需要對參數進行操作,所以:1.需要參數;2.傳址調用CreateFood(ps);
}//打印幫助信息
void PrintHelpInfo()
{SetPos(62, 15);printf("1.不能穿墻,不能咬到自己");SetPos(62, 16);printf("2.用 ↑.↓.←.→ 來控制蛇的移動");SetPos(62, 17);printf("3.F3是加速,F4是減速");SetPos(64, 18);printf("4.ESC :退出游戲.space:暫停游戲.");SetPos(62, 20);printf("版權@比特就業課");
}//暫停狀態
void pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}//判定行進方向的下一個坐標是否是食物
//pSnakeNode pNext 是下?個節點的地址
//pSnake ps 維護蛇的指針
int NextIsFood(pSnake ps, pSnakeNode pNext)
{if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)return 1;//下一個坐標處是食物elsereturn 0;
}//是食物就吃掉 (1.頭插;2.打印新蛇;3.釋放食物;4.創建食物)
//pSnakeNode pNext 是下?個節點的地址
//pSnake ps 維護蛇的指針
void EatFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;//新頭的next指向舊頭ps->pSnake = pNext;//新頭賦值給舊頭//打印蛇(遍歷)pSnakeNode cur = ps->pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->Score += ps->FoodWeight;//釋放舊的食物——因為“移動下一步”這個函數中創建了“下一格結點”——其坐標恰好與食物結點坐標重合,將其頭插入貪吃蛇鏈表//而在食物創建函數中也創建了一個結點,該結點需要被釋放free(ps->pFood);//新建食物CreateFood(ps);
}//不是食物就正常進一步——頭插+尾刪
//1.頭插;2.打印新蛇;3.留白舊尾;4.釋放舊尾
//pSnakeNode pNext 是下?個節點的地址
//pSnake ps 維護蛇的指針
void NotEatFood(pSnake ps, pSnakeNode pNext)
{//頭插法//1.改變新結點的指針域指向//2.改變維護鏈表的指針的指向pNext->next = ps->pSnake;ps->pSnake = pNext;//打印新蛇,留白蛇尾+釋放尾結點——先找到尾結點的前驅結點pSnakeNode cur = ps->pSnake;while (cur->next->next) //確實是沒有打印最后一個結點——即上一步的倒數第2個結點,相當于直接復用了上一步的打印結果{SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//將尾節點的位置打印成空白字符——注意是兩個空格字符——不覆蓋打印,會直接復用上一步的打印結果SetPos(cur->next->x, cur->next->y);printf(" ");free(cur->next); //注意找到前驅之后,先將該結點坐標打印空白字符,再釋放;否則先釋放就找不到那個結點坐標了 cur->next = NULL; //這一句容易漏寫,易錯
}//檢測是否撞墻
void KillByWall(pSnake ps)
{//走完之后,蛇頭和墻的坐標重合if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;}
}//檢測是否撞自己
//pSnake ps 維護蛇的指針
void KillBySelf(pSnake ps)
{//從第二個節點開始——由于不接收上立刻下、左立刻右的指令,理論上從第3個結點開始也可以pSnakeNode cur = ps->pSnake->next;while (cur){//走完之后,蛇身的某一點和蛇頭重合if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}//蛇的移動
//pSnake ps 維護蛇的指針
void SnakeMove(pSnake ps)
{//創建下一結點pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("SnakeMove()::malloc()");return;}pNext->next = NULL;//確定下?個節點的坐標,下?個節點的坐標根據——蛇頭的坐標和方向確定switch (ps->dir){case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;//——縱減一break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break;case LEFT:pNext->x = ps->pSnake->x - 2;//——橫減二pNext->y = ps->pSnake->y;break;case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}//移動到的下一個結點有4種情況:1.啥也沒有;2.食物;3.墻壁;4.蛇身//下一個坐標處是否是食物if (NextIsFood(ps, pNext)){//是食物就吃掉EatFood(ps, pNext);}else{//不是食物就正常一步NotEatFood(ps, pNext);}//走完之后再檢測//檢測撞墻——只需要改變狀態,不需要返回值KillByWall(ps);//檢測撞到自己——只需要改變狀態,不需要返回值KillBySelf(ps);//走一步函數結束,休眠一下,立刻進行下一次循環的條件判定——貪吃蛇的現態
}//游戲運行
//基本邏輯:1.睡眠一下;2.走一步 或者 1.走一步;2.睡眠一下
//過程中還要:打印幫助信息、檢測按鍵
void GameRun(pSnake ps)
{//打印幫助信息PrintHelpInfo();do{//打印當前的分數情況SetPos(62, 10);printf("總分:%5d\n", ps->Score);SetPos(62, 11);printf("食物的分值:%02d\n", ps->FoodWeight);//沒有02則由10變8會顯示由10變80 //檢測按鍵//上、下、左、右、ESC、空格、F3、F4if (KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上鍵并且當前方向不是朝下走{ps->dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->dir != UP){ps->dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT){ps->dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT){ps->dir = RIGHT;}else if (KEY_PRESS(VK_ESCAPE)){ps->status = ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戲要暫定pause();//暫定和回復暫定}else if (KEY_PRESS(VK_F3)){if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight > 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//走一步SnakeMove(ps);//睡眠一下Sleep(ps->SleepTime);} while (ps->status == OK);//正常狀態下是死循環,一直執行
}//結束判定
void GameEnd(pSnake ps)
{SetPos(15, 12);switch (ps->status){case ESC:printf("主動退出游戲,正常退出\n");break;case KILL_BY_WALL:printf("很遺憾,撞墻了,游戲結束\n");break;case KILL_BY_SELF:printf("很遺憾,咬到自己了,游戲結束\n");break;}//釋放貪吃蛇的鏈表資源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;while (cur){del = cur;//釋放本結點之前,先保存下一結點;否則找不到cur = cur->next;free(del);}free(ps->pFood);ps = NULL; //就不用再ps->pSnake=NULL、ps->pFood=NULL
}
參考:漢字字符集編碼查詢;中文字符集編碼:GB2312、BIG5、GBK、GB18030、Unicode
10.?擴展
完