一:文章大概
使用C語言在windows環境的控制臺中模擬實現經典小游戲
實現基本功能:
1.貪吃蛇地圖繪制
2.蛇吃食物的功能(上,下,左,右方向控制蛇的動作)
3.蛇撞墻死亡
4.計算得分
5.蛇身加速,減速
6.暫停游戲
二:所用知識點
C語言函數,枚舉,結構體,動態內存管理,預處理指令,鏈表,Win32API......
三:Win32API介紹
3.1Win32API
Windows這個多作業系統除了協調應用程序的執行、分配內存、管理資源之外,它同時也是一個很大的服務中心,調用這個服務中心的各種服務(每?種服務就是一個函數),可以幫應用程序達到開啟視窗、描繪圖形、使用周邊設備等目的,由于這些函數服務的對象是應用程序(Application),所以便稱之為Application Programming Interface,簡稱API函數。WIN32API也就是Microsoft Windows32位平臺的應用程序編程接口。
3.2控制臺程序(console)
平常我們運行程序起來的黑框其實就是控制臺程序
我們可以使用cmd命令來設置控制臺的長寬:設置控制臺的大小,eg:30行,100列
mode con cols = 100 lines = 30
也可以通過命令設置控制臺名字:
title 貪吃蛇
這些能在控制臺執行的命令,也可以在C語言函數system來執行。例如:
int main()
{//設置控制臺的長寬:設置控制臺窗口大小,30行,100列system("mode con cols=100 lines=30");//設置cmd窗口名稱system("title 貪吃蛇");return 0;
}
其中getchar的作用是。防止程序直接執行完,如果沒有getchar,程序直接結束,看不出效果
3.3控制臺屏幕上的坐標COORD
COORD是WindowsAPI中定義的一個結構體,表示一個字符在控制臺屏幕上的坐標
對應的頭文件在windows.h
typedef struct _COORD
{short X;short Y;
}COORD,*PCOORD;
//COORD是把這個結構體重命名為COORD,而*PCOORD是這個結構體的指針
給坐標賦值
COORD pos = {10,15};
3.4GetStdHandle
GetStdHandle是WindowsAPI函數。它用于從一個特定的標準設備(標準輸入,標準輸出或標準錯誤)中取得一個句柄(用來表示不同設備的數值),使用這個句柄可以操作設備。
HANDLE GetStdHandle(DWORD nStdHandle);
這個函數的參數就三種
//實例
HANDLE hOutput = NULL;
//獲取標準輸出的句柄(用來標識不同設備數值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
3.5GetConsoleCursorlnfo
檢索有關指定控制臺屏幕緩沖區的光標的大小和可見性的信息
BOOL WINAPI GetConsoleCursorInfo
{HANDLE hConsoleOutput;//第一個參數是句柄
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo//第二個參數是指針
};
//PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO結構體的指針,該結構體接受有關主機有游標(光標)的信息
HANDLE hOutput = NULL;//獲取標準輸出的句柄(用來標識不同設備的數值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(hOutput, &CursorInfo);//獲取控制臺光標信息
3.5.1 CONSOLE_CURSOR_INFO
這個結構體,包含有關光標的信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO
dwSize,由光標填充的字符單元格的百分比。此比值介于1~100之間。光標外觀會變化,范圍從完全填充單元格到單元格底部的水平線條。
bVisible,游標可見性,如果光標可見,則此成員為TRUE。
包含該結構體的頭文件是stdbool.h
CursorInfo.bVisible = false;//隱藏控制臺光標
3.6 SetConsoleCursorInfo
設置指定控制臺屏幕緩沖區的光標的可見性
BOOL WINAPI SetConsoleCursorInfo
{
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
};
3.7 SetConsoleCursorPosition
設置指定控制臺屏幕緩沖區中的光標位置,我們將想要的光標信息放在COORD類型的pos中,調用SetConsoleCursorPosition函數將光標設置到指定位置。
BOOL WINAPI SetConsoleCursorPosition
{
HANDLE hConsoleOutput,
COORD pos
};
實例:
3.8封裝一個設置光標位置的函數
//設置光標位置的函數
void SetPos(short x, short y)
{COORD pos = { x, y };HANDLE hOutput = NULL;//獲取標準輸出的句柄(用來標識不同設備的數值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//設置標準輸出上光標的位置SetConsoleCursorInfo(hOutput, pos);
}
3.9 GetAsyncKeyState
獲取按鍵情況,GetAsyncKeyState的函數原型如下:
SHORT GetAsyncKeyState
{int vKey;
};
將鍵盤上每個鍵的虛擬鍵值傳遞給函數,函數通過返回值來分辨按鍵的狀態。
GetAsyncKeyState的返回值是short類型,在上一次調用GetAsyncKeyState函數后,如果返回的是16位short數據中,最高位是1,說明按鍵的狀態是按下,如果最高是0,說明按鍵狀態是抬起;如果最低被置為1則說明,該按鍵按過,否則為零
如果要判斷一個鍵是被按過,可以檢測GetAsyncKeyState返回值的最低值是否為1
#define KEY_PRESS(VK) ( ( ( GetAsyncKeyState(VK) )&0x1) ? 1 : 0)
四:貪吃蛇游戲設計與分析
4.1地圖
我們貪吃蛇大綱是要這個樣子,那我們的地圖如何布置呢?
這里不得不講一下控制窗口的一些知識點,如果想在控制臺中指定位置輸出信息,我們得知道該位置的坐標,所以首先介紹一下控制臺得坐標信息
控制臺得坐標如下圖所示,橫向的是x軸,從左向右依次增長,縱向是y軸,從上到下依次增長
在游戲地圖上,我們打印墻體使用寬字符:□,打印蛇使用寬字符●,打印食物使用寬字符★
普通的字符是占?個字節的,這類寬字符是占用2個字節。
這里再簡單的講一下C語言的國際化特性相關的知識,過去C語言并不適合非英語國家(地區)使用C語言最初假定字符都是自己的。但是這些假定并不是在世界的任何地方都適用。
C語言字符默認是采用ASCII編碼的,ASCII字符集采用的是單字節編碼,且只使用了單字節中的低7位,最高位是沒有使用的,可表示為0xxxxxxxx;可以看到,ASCII字符集共包含128個字符,在英語國家中,128個字符是基本夠用的,但是,在其他國家語言中,比如,在法語中,字母上方有注音符號,它就無法用ASCII碼表示。于是,一些歐洲國家就決定,利用字節中閑置的最高位編入新的符號。比如,法語中的é的編碼為130(二進制10000010)。這樣?來,這些歐洲國家使用的編碼體系,可以表示最多256個符號。但是,這里又出現了新的問題。不同的國家有不同的字母,因此,哪怕它們都使用256個符號的編碼方式,代表的字母卻不一樣。比如,130在法語編碼中代表了é,在希伯來語編碼中卻代表了字母Gimel,在俄語編碼中又會代表另?個符號。但是不管怎樣,所有這些編碼方式中,0--127表示的符號是一樣的,不一樣的只是128--255的這一段。
至于亞洲國家的文字,使用的符號就更多了,漢字就多達10萬左右。一個字節只能表示256種符號,肯定是不夠的,就必須使用多個字節表達?個符號。比如,簡體中文常見的編碼方式是GB2312,使用兩個字節表示一個漢字,所以理論上最多可以表示256x25=65536個符號。
后來為了使C語言適應國際化,C語言的標準中不斷加入了國際化的支持。比如:加入和寬字符的類型wchar_t 和寬字符的輸入和輸出函數,加入和<locale.h>頭文件,其中提供了允許程序員針對特定地區(通常是國家或者說某種特定語言的地理區域)調整程序行為的函數。
4.1.1<locale.h>本地化
<locale.h>提供的函數用于控制C語言標準庫中對于不同地區會產生不一樣行為的部分
在標準中可以,以來地區的部分有以下幾項:
1.數字量的格式
2.貨幣量的格式
3.字符集
4.日期和時間的表示形式
4.1.2類項
通過修改地區,程序可以改變它的行為來適應世界的不同地區。但是地區的改變可能會影響庫的許多部分,其中一部分可能是我們不希望改變的。所以C語言支持針對不同的類項進行修改,下面是一宏,指定一個類項:
LC_COLLATE//影響字符串比較函strcoll()和strxfrm()
LC_CTYPE//影響字符串處理函數
LC_MONETARY//影響貨幣形式
LC_NUMERIC//影響printf的數字格式
LC_TIME//影響時間格式strftime()和wcsftime()
LC_ALL//針對所有類項修改
4.1.3 setlocale函數
char* setlocale(int category , char* locale);
setlocale函數用來修改當前地區,可以針對一個類項修改,也可以針對所有類項。
setlocale的第一個參數可以是前面說明的類項的中的一個,那么每次只會影響一個類項,如果第一個參數是LC_ALL,就會影響所有類項。
C語言標準給第二個參數僅定義了兩個可能的取值:“C” 和 “”。
在任意程序執行開始,都會隱藏執行調用:
setlocale(LC_ALL,"C");
當地區設置為“C”時,庫函數按正常方式執行,小數點就是一個點。
當程序執行起來后想改地區,就只能顯示調用setlocale函數。用“ ”作為第二個參數,調用setlocale函數就可以切換到本地模式,這種模式下程序會適應本地環境。比如:切換到我們的本地模式后就會支持寬字符(漢字)的輸出等。
setlocale(LC_ALL," ");//切換到本地模式
其中setlocale的返回值是一個字符串,表示格式設置好了,如果調用失敗,返回NULL
setlocale:可以用來查詢當地地區,這時,第二個參數是NULL
int main()
{char* loc;loc = setlocale(LC_ALL, NULL);printf("默認本地信息是:%s\n", loc);loc = setlocale(LC_ALL, "");printf("默認本地信息是:%s\n", loc);return 0;
}
4.1.4寬字符的打印
那如果想在屏幕上打印寬字符,怎么打印呢?
寬字符打印要用wprintf,打印字符:%lc,打印字符串:%ls
寬字符的字面量必須加上前綴L,前綴L在單引號前,表示寬字符,如果不加就會當成窄字符
從輸出結果看,我們發現一個普通字節占一個字符的位置,但是打印一個漢字字符,占用兩個字符的位置,那么我們如果要在貪吃蛇中使用寬字符,就要處理好地圖上坐標的計算
4.1.5地圖坐標
我們可以假設實現一個棋盤27行,58列的棋盤
4.2蛇身和食物
初始化狀態,假設蛇的長度是5,蛇的每個節點是●,在固定的一個坐標處,比如(24,5)處開始出現蛇,連續五個節點。
注:蛇的每個節點的x坐標必須是2的倍數,否則可能出現蛇的一個節點有一半出現在墻內,另一半出現在墻外的現象,坐標不好對齊。
關于食物,就是在墻體內隨機生成一個坐標(x必須是2的倍數),坐標不能和蛇的身體重合,然后打印★
4.3數據結構設計
在游戲運行過程中,蛇每吃一次食物,蛇的身體就會變長一節如果我們使用鏈表儲存蛇的信息,那么蛇的每一節其實就是鏈表的一個節點,每個節點只要記錄好蛇身節點在地圖上的坐標就行,所以蛇的節點結構如下:
//貪吃蛇身節點的定義
typedef struct SnackNode
{int x;int y;struct SanckNode* next;
}SnackNode,*pSnackNode;
要管理整條貪吃蛇,我們可以再封裝一個Sanck的結構體來維護整條蛇
//貪吃蛇
typedef struct Snack
{pSnackNode pSnack;//蛇身體pSnackNode pfood;//食物出現的位置enum DIRECTION Dir;//蛇下一歩要走的方向enum GAME_STATE State;//游戲狀態int Sore;//總分數int foodWeight;//每一個食物的分數int SleepTime;//睡眠時間(速度)
}Snack,*pSnack;
//方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};
//游戲狀態
enum GAME_STATE
{OK,//游戲正常運行KILL_BY_WALL,//撞墻死了KILL_BY_SELF,//咬到自己END_NOMAL//正常結束
};
4.4游戲流程設計
五:核心邏輯,代碼分析實現
5.1代碼主邏輯
void test()
{int ch = 0;srand((unsigned int)time(NULL));//根據時間產生隨機值do{//創建貪吃蛇Snack snack = { 0 };//游戲主邏輯GameStart(&snack);GameRun(&snack);GameEnd(&snack);SetPos(20, 15);printf("再來一局嗎?(Y/N):");ch = getchar();getchar();//清理'\n'} while (ch == 'Y' || ch == 'y');SetPos(0, 27);}
int main()
{//修改當前地區為本地模式,為了支持中文寬字符的打印setlocale(LC_ALL, "");//代碼測試test();return 0;
}
5.2游戲開始
void GameStart(pSnack 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);//設置控制臺光標狀態//打印歡迎界面WelcomToGame();//打印地圖CreatMap();//初始化蛇InitSnack(ps);//創建第一個食物CreatFood(ps);}
5.2.1打印歡迎界面
在游戲正式開始之前,做一些功能的提醒
//打印歡迎界面
void WelcomToGame()
{SetPos(40, 15);printf("歡迎來到貪吃蛇小游戲");SetPos(40, 25);system("pause");//暫停,按任意鍵繼續system("cls");//清理屏幕信息SetPos(25, 12);printf("用↑,↓,←,→分別控制蛇的運動,F3為加速,F4為減速\n");SetPos(25, 13);printf("加速將獲得更高的分數");SetPos(40, 25);system("pause");system("cls");getchar();
}
5.2.2創建地圖
創建地圖就是把墻打印出來,,因為是寬字符的打印,所以用wprintf函數,打印格式串前使用L
打印地圖的關鍵是算好坐標,才能在想要的位置打印墻體
#define WALL L'■'
創建地圖函數CreatMap
void CreatMap()
{int i = 0;//上(0,0)-(56,0)SetPos(0, 0);for (i = 0; i < 58; i+=2){wprintf(L"%lc", WALL);}//下(0,26)-(56,26)SetPos(0, 26);for (i = 0; i < 58; i+=2){wprintf(L"%lc", WALL);}//左//x是0,y是從1開始增長for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}
5.2.3初始化蛇身
設蛇最開始的長度是5,每節對應鏈表的一個節點,蛇身的每個節點都有自己的坐標。
創建5個節點,然后將每個節點存放在鏈表中進行管理。創建完蛇身后,將蛇的每個節點打印在了屏幕上。再設置當前游戲的狀態,蛇移動的速度,默認的方向,初始狀態,蛇的狀態,每個食物的分數。
蛇身打印的寬字符:
#define BODY L'●'
初始化蛇身InitSnack
//初始化蛇
#define POS_X 24
#define POS_Y 5void InitSnack(pSnack ps)
{pSnackNode cur = NULL;int i = 0;//創建蛇身節點,并初始化坐標//頭插for (i = 0; i < 5; i++){//創建蛇身的節點cur = (pSnackNode)malloc(sizeof(SnackNode));if (cur == NULL){perror("InitSnack()::fail");return;}//設置坐標cur->next = NULL;cur->x = POS_X + i * 2;cur->y = POS_Y;//頭插法if (ps->pSnack == NULL){ps->pSnack = cur;}else{cur->next = ps->pSnack;ps->pSnack = cur;}}//打印蛇身cur = ps->pSnack;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//初始化其他貪吃蛇數據ps->SleepTime = 200;ps->Sore = 0;ps->foodWeight = 10;ps->State = OK;ps->Dir = RIGHT;ps->pfood = NULL;
}
5.2.4創建第一個食物
先隨機生成食物的坐標
? ? ? ? x必須是2的倍數
? ? ? ? 食物的坐標不能和蛇身的每個節點坐標重合
創建食物節點,打印食物
食物打印的寬字符:
#define FOOD L'★'
創建食物的函數CreatFood
}
//創建第一個食物
void CreatFood(pSnack ps)
{int x = 0;int y = 0;
again://產生的x坐標應該是2的倍數,這樣蛇吃食物時,食物和蛇才可能對齊do{x = rand() % 53 + 2;//2~54y = rand() % 25 + 1;//1~25} while(x%2!=0);//獲取蛇頭指針pSnackNode cur = ps->pSnack;while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}pSnackNode pFood = (pSnackNode)malloc(sizeof(SnackNode));//創建食物if (pFood == NULL){perror("malloc failed");return;}pFood->x = x;pFood->y = y;SetPos(x, y);wprintf(L"%lc", FOOD);ps->pfood = pFood;
}
5.3游戲運行
游戲運行時,右側打印幫助信息,提示玩家
根據游戲狀態檢查游戲是否繼續,如果是OK,游戲繼續,否則游戲結束
如果游戲繼續,就是檢查按鍵情況,確定蛇下一步的方向,或者是加速減速,是否暫停或者是退出游戲
確定了蛇的方向,蛇就可以繼續移動了
//游戲運行
void GameRun(pSnack ps)
{//右側打印信息PrintHelpInfo();do{SetPos(64, 10);printf("得分:%d ", ps->Sore);printf("每個食物的得分:%d", ps->foodWeight);//按鍵為向上,且蛇頭不向下if (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_SPACE)){pause();}else if (KEY_PRESS(VK_ESCAPE)){ps->State = END_NOMAL;}//加速,但是加速也不能一直加,要有一個最高值else if (KEY_PRESS(VK_F3)){if (ps->SleepTime >= 50){ps->SleepTime -= 20;ps->foodWeight += 10;}}else if (KEY_PRESS(VK_F4)){if (ps->SleepTime <350){ps->SleepTime += 30;ps->foodWeight -= 10;if (ps->SleepTime == 350)ps->foodWeight = 1;}}//蛇每次一定要休眠,時間越短,移動速度越快Sleep(ps->SleepTime);SnackMove(ps);} while(ps->State == OK);}
5.3.1暫停
//暫停
void pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}
5.3.2 PrintHelpInfo
// 右側幫助信息
void PrintHelpInfo()
{SetPos(64, 12);printf("不能穿墻,不能咬到自己");SetPos(64, 13);printf("用↑↓←→分別控制蛇的運動");SetPos(64, 14);printf("F3為加速,F4為減速");SetPos(64, 15);printf("ESC:退出游戲,Space:暫停游戲");}
5.3.3蛇身移動
先創建下一個節點,根據移動方向,蛇身移動到下一個位置的坐標
確定了下一個位置后,看下一個位置是否是食物(NextFood),是食物就做吃食物的處理(EatFood),不是食物(NoFood)就做前進一步的處理?。
蛇身移動后,判斷此次移動是否會造成撞墻(KillByWall)或者撞上蛇自身(KillBySelf),從而影響游戲狀態??
//蛇的移動
void SnackMove(pSnack ps)
{//創建下一個節點pSnackNode pNextNode = (pSnackNode)malloc(sizeof(SnackNode));if (pNextNode == NULL){perror("SnackMove malloc fail");return;}pNextNode->next = NULL;//確定下一個節點的坐標,蛇頭的坐標和方向確定switch (ps->Dir){case UP:{pNextNode->x = ps->pSnack->x;pNextNode->y = ps->pSnack->y - 1;}break;case DOWN:{pNextNode->x = ps->pSnack->x;pNextNode->y = ps->pSnack->y + 1;}break; case LEFT:{pNextNode->x = ps->pSnack->x-2;pNextNode->y = ps->pSnack->y;}break;case RIGHT:{pNextNode->x = ps->pSnack->x+2;pNextNode->y = ps->pSnack->y;}break;}//如果下一個節點是食物if (NextFood(pNextNode, ps)){EatFood(pNextNode, ps);}else{NoFood(pNextNode, ps);}KillByWall(ps);KillBySelf(ps);
}
5.3.3.1NextFood? ? ? ?
//判斷下一個是不是食物
int NextFood(pSnackNode psn, pSnack ps)
{//psn是下一個節點的地址//ps是維護蛇的指針return (psn->x == ps->pfood->x)&&(psn->y == ps->pfood->y);
}
5.3.3.2 EatFood
//吃食物
void EatFood(pSnackNode psn, pSnack ps)
{//頭插法psn->next = ps->pSnack;ps->pSnack = psn;pSnackNode cur = ps->pSnack;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->Sore += ps->foodWeight;//釋放食物節點free(ps->pfood);//再建立新的食物CreatFood(ps);
}
5.3.3.3 NoFood
將下一個節點插入蛇的身體,并將蛇身最后一個節點打印為空格,放棄蛇身的最后一個節點
//不吃食物
void NoFood(pSnackNode psn, pSnack ps)
{//頭插法psn->next = ps->pSnack;ps->pSnack = psn;pSnackNode cur = ps->pSnack;//打印蛇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;
}
int KillByWall(pSnack ps)
{if ((ps->pSnack->x == 0)|| (ps->pSnack->x == 56)|| (ps->pSnack->y == 0)|| (ps->pSnack->y == 26)){ps->State = KILL_BY_WALL;return 1;}return 0;
}
5.3.3.4KillByWall
int KillByWall(pSnack ps)
{if ((ps->pSnack->x == 0)|| (ps->pSnack->x == 56)|| (ps->pSnack->y == 0)|| (ps->pSnack->y == 26)){ps->State = KILL_BY_WALL;return 1;}return 0;
}
5.3.3.5KillBySelf
int KillBySelf(pSnack ps)
{pSnackNode cur = ps->pSnack->next;while (cur){if ((ps->pSnack->x == cur->x)&& (ps->pSnack->y == cur->y)){ps->State = KILL_BY_SELF;return 1;}cur = cur->next;}return 0;
}
5.4游戲結束
游戲狀態不再是OK時,要告知游戲結束的原因,并釋放蛇身節點
//游戲結束
void GameEnd(pSnack ps)
{pSnackNode cur = ps->pSnack->next;SetPos(24, 12);switch (ps->State){case END_NOMAL:printf("您主動退出");break;case KILL_BY_SELF:printf("您撞到自己了,游戲結束");break;case KILL_BY_WALL:printf("您撞墻了,游戲介紹");break;}//釋放蛇節點while (cur){pSnackNode del = cur;cur = cur->next;free(del);}free(ps->pfood);ps->pfood = NULL;
}