1. 程式人生 > >C語言實現貪吃蛇(二)----區域性重新整理

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;
}

與前一篇部落格對比,這裡的一下變化:

  1. 修改了 main() 主函式
  2. 用 init_game() 替換 print_game()
  3. 修改了 move_snake() 函式
  4. 增加了 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語言實現貪吃蛇之區域性重新整理篇 》,在後續的部落格中,我將跟隨著原作者的腳步繼續折騰這個遊戲,感興趣的同學可以看看後續的文章。