C語言實現貪吃蛇(二)----區域性重新整理
前言:
在上一篇部落格《C語言實現貪吃蛇(一)—-陣列實現》,我們使用陣列來儲存座標,並且不斷的通過全屏重新整理的方式來實現蛇移動的動態效果。但是全屏重新整理使得該遊戲整個過程中的閃爍現象,究其原因,無非就是在於頻繁的清空與列印。
但是想想看,整個遊戲過程中並不需要重複列印整個介面,比如圍牆,比如未被吃掉的食物。要實現蛇的移動,我們只要打印出新的蛇頭,清除原來的蛇尾就好了。食物只有在被吃掉時才需要重新列印,邊界更是隻用列印一次。好了,既然我們看到了可提升的地方,就開始動手優化吧。
準備工作:
為了避免全屏重新整理,我們應直接定位到需要列印的地方進行列印操作,而不是像之前一樣通過迴圈列印。
因此我們將需要一個可以自由移動游標的函式,這樣我們才能做到在需要的地方列印。
TC上有一個叫gotoxy()的很方便的函式,該函式,顧名思義,就是將游標移動到(x,y)位置。然而現在估計很少有人還在使用TC。那麼我們就著手自己編寫一個gotoxy()函式。
void gotoxy(unsigned char x,unsigned char y){
//COORD是Windows API中定義的一種結構,表示一個字元在控制檯螢幕上的座標
COORD cor;
//控制代碼
HANDLE hout;
//設定我們要定位到的座標
cor.X = x;
cor.Y = y;
//GetStdHandle函式獲取一個指向特定標準裝置的控制代碼,包括標準輸入,標準輸出和標準錯誤。
//STD_OUTPUT_HANDLE正是代表標準輸出(也就是顯示屏)的巨集
hout = GetStdHandle(STD_OUTPUT_HANDLE);
//SetConsoleCursorPosition函式用於設定控制檯游標的位置
SetConsoleCursorPosition(hout, cor);
}
如果是沒有接觸過windows程式設計的同學看到這段程式碼可能會有些懵逼,不過沒關係。整段程式碼的邏輯其實十分簡單。
COORD是windows API定義的結構,其宣告如下:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
正如其名字coordinate(座標)一樣,這是一個儲存二維座標的結構體。
HANDLE(控制代碼)在windows程式設計中是一個十分重要的概念。在window程式設計中,對於一個Object(物件)我們只能通過Handle來訪問它。覺得不好理解的同學把控制代碼當作指標來看待就好了。
GetStdHandle函式獲取一個指向特定標準裝置的控制代碼,包括標準輸入,標準輸出和標準錯誤。STD_OUTPUT_HANDLE正是代表標準輸出(也就是顯示屏)的巨集。
SetConsoleCursorPosition函式用於設定控制檯游標的位置。
如果還不懂的同學還是 baidu 一下吧,畢竟我也是差不多的。。。。。
有了gotoxy函式,適當地修改上一篇的程式碼就可以解決閃屏的問題。
程式實現:
現在我們先來看看程式的最主要部分:主函式
int main(void){
int dir = UP; //初始方向預設向上,UP是我們定義的巨集
init_game(); //初始化遊戲
while(1){
dir = get_dir(dir); //獲取方向(我們摁下的方向)
move_snake(dir); //移動蛇身
if(!isalive()){ //判斷蛇的生命狀態
break;
}
}
//清除螢幕
system("cls");
printf("Game Over!\n");
return 0;
}
與上一篇部落格相比,我們的主函式中去掉 print_game 函式,加入 init_game 函式。
我們來看看程式修改後的一些變數和函式宣告(基本上跟之前的差不多的)
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>
//72,80,75,77是方向鍵對應的鍵值
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77
#define SNAKE 1
#define FOOD 2
#define BAR 3
//初始化地圖
char map[17][17] = {0};
//初始化蛇頭座標
unsigned char snake[50] = {77};
//初始化食物座標
unsigned char food = 68;
//蛇長
char len = 1;
//儲存座標數字與x、y的轉換函式
void tran(unsigned char num,unsigned char * x,unsigned char * y);
//初始化遊戲
void init_game(void);
//獲取方向函式(注意當蛇身長度超過一節時不能回頭)
int get_dir(int old_dir);
//移動蛇身函式(遊戲大部分內容在其中)
void move_snake(int dir);
//其中有個生產食物的函式,generate_food(),它利用隨機數生成函式生成食物座標
unsigned char generate_food(void);
//判斷蛇死活的函式(判斷了蛇是否撞到邊界或者自食)
int isalive(void);
//將游標移動到命令列的 (x,y)位置
void gotoxy(unsigned char x,unsigned char y);
int main(void){
int dir = UP; //初始方向預設向上,UP是我們定義的巨集
init_game(); //初始化遊戲
while(1){
dir = get_dir(dir); //獲取方向(我們摁下的方向)
move_snake(dir); //移動蛇身
if(!isalive()){ //判斷蛇的生命狀態
break;
}
}
//清除螢幕
system("cls");
printf("Game Over!\n");
return 0;
}
與前一篇部落格對比,這裡的一下變化:
- 修改了 main() 主函式
- 用 init_game() 替換 print_game()
- 修改了 move_snake() 函式
- 增加了 gotoxy() 游標座標定位函式
main() 函式和 gotoxy() 函式我們已經介紹過了,現在我們看看 init_game() 函式:
//初始化遊戲
void init_game(void);
//初始化遊戲 (列印初始狀態)
void init_game(void)
{
int i, j;
unsigned char x, y, fx, fy;
tran(snake[0], &x, &y);
tran(food, &fx, &fy);
for (j = 0; j<17; j++) {
for (i = 0; i<17; i++) {
//列印圍牆
if (i == 0 || i == 16 || j == 0 || j == 16){
putchar('#');
}
//列印蛇頭
else if (i == x&&j == y){
putchar('*');
}
//列印食物
else if (i == fy&&j == fx){
putchar('$');
}
//空白地方
else{
putchar(' ');
}
}
putchar('\n');
}
}
從 init_game() 函式中和在 main() 呼叫可以看出,該函式的作用僅僅是初始化作用(將一成不變的圍牆畫好,列印食物和蛇頭的初始位置)。後面就沒它什麼事了。
再來看看 move_snake() 函式:
//移動蛇身函式(遊戲大部分內容在其中)
void move_snake(int dir);
void move_snake(int dir){
int last = snake[0],current; //last與current用於之後蛇座標的更新
int i;
int grow=0; //判斷是否要長身體
unsigned char x, y, fx, fy; //蛇座標與食物座標
tran(food, &fx, &fy); //食物座標
tran(snake[0], &x, &y); //蛇頭座標
switch (dir){ //更新蛇頭座標(座標原點是左上角)
case UP:
y--;
break;
case DOWN:
y++;
break;
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
}
//按位抑或(妙!)
snake[0] = ((x ^ 0) << 4) ^ y; //將x,y換回一個數
//蛇吃到了食物
if (snake[0] == food) {
grow = 1;
food = generate_food(); //產生新食物
}
for (i = 0; i<len; i++) { //蛇移動的關鍵,通過將蛇頭原來的座標賦給第二節,原來的第二節賦給第三節,依次下去,完成蛇座標的更新
if (i == 0) //如果只有頭,跳過,因為前面已更新蛇頭座標
continue;
current = snake[i]; //將當前操作的蛇節座標儲存到current裡
snake[i] = last; //完成當前操作蛇節座標的更新
last = current; //last記錄的是上一次操作蛇節的座標,這次操作已經結束,故把current賦給last
}
gotoxy(x, y); //將游標移動到指定位置
putchar('*'); //列印新的蛇頭
if (grow) { //如果要長節的話就不去除舊的蛇尾
snake[len] = last;
len++;
tran(food, &fx, &fy); // 列印食物(上面已經產生新的食物座標)
gotoxy(fx,fy);
putchar('$');
}else {
//這是為了避免當你把蛇繞成一個圈的時候(蛇頭緊跟蛇尾,沒咬到),清除蛇尾順便也把蛇頭清除掉了
if(snake[0] != last){
tran(last, &x, &y);
gotoxy(x, y);
putchar(' '); //去除舊的蛇尾
}
}
//避免游標一直跟著蛇尾(或食物)
gotoxy(0,17);
Sleep(500);
}
從該函式中可以看出,我們利用 gotoxy() 函式,實現了局部重新整理的功能,從而避免了遊戲的閃爍現象。
本部落格參考自《C語言實現貪吃蛇之區域性重新整理篇 》,在後續的部落格中,我將跟隨著原作者的腳步繼續折騰這個遊戲,感興趣的同學可以看看後續的文章。