[C語言]貪吃蛇_結構數組實現
一、設計思路
蛇身本質上就是個結構數組,數組裏存儲了坐標x、y的值,再通過一個循環把它打印出來,蛇的移動則是不斷地刷新重新打印。所以撞墻、咬到自己只是數組x、y值的簡單比較。
二、用上的知識點
- 結構數組
- Windows API函數
三、具體實現
先來實現靜態頁面,把地圖、初始蛇身、食物搞定。
這裏需要用到Windows API的知識,也就是對控制臺上坐標的修改
- //這段代碼來自參考1
- void Pos(int x, int y)
- {
- COORD pos;
- HANDLE hOutput;
- pos.X = x;
-
pos.Y = y;
- hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
- SetConsoleCursorPosition(hOutput, pos);
-
}
COORD是Windows API中定義的一種結構,表示在控制臺上的坐標
- typedef struct _COORD {
- SHORT X; // horizontal coordinate
- SHORT Y; // vertical coordinate
-
} COORD;
而代碼中第七行則是獲得屏幕緩沖區的句柄,第八行是直接修改光標位置的函數。
1.地圖。
有了Pos()函數,打印一個框就不是問題了。假如我們用"-"作為上下邊框,把"|"作為左右邊框,這看起來沒什麽不妥,但其實我們已經掉進了坑裏,直接上代碼及實際效果圖吧。
- //LONG==60
- //WIDTH==30
- void CreateMap()
- {
- int i;
- for(i=0;i<LONG;i++)//上下兩行
- {
- Pos(i,1);
- printf("-");
- Pos(i,WIDTH-1);
- printf("-");
- }
- for(i=2;i<WIDTH-1;i++)//左右兩列
- {
- Pos(0,i);
-
printf("|");
- Pos(LONG-1,i);
- printf("|");
- }
-
}
發現了問題嗎?這是一條正常的蛇。。。那為什麽看起來不正常呢?我們把邊框都換成"#"來看看…
這就清楚多了啊,要知道我們上下邊框可是各有60個"#"的,長60寬30的長方形輸出之後竟然成了個正方形。
原因在這
控制臺上每個字符的長寬比例(像素點)是不同的,所以才會出現上圖這種蛋疼的情況。
解決方法其實也很簡單,我們需要引入一些特殊符號,比如"●""■""⊙"等,這些字符的特點是它占據兩個普通字符的位置
所以上下邊框就有60/2=30個符號,要讓它仍然是個正方形的話,左右也可以設為30(28+2)個符號.
代碼及效果圖如下
- void CreateMap()
- {
- int i;
- for(i=0;i<LONG;i+=2)
- {
- Pos(i,0);
- printf("■");
- Pos(i,WIDTH-1);
- printf("■");
- }
- for(i=1;i<WIDTH-1;i++)
- {
- Pos(0,i);
- printf("■");
- Pos(LONG-2,i);
- printf("■");
- }
-
}
這樣看就舒服多了,不過也讓復雜度提升了一些,上邊框每個符號的坐標分別是(0,0)(2,0)(4,0)…(2*n-2,0)這個在蛇的移動及食物的模塊再提。
2.初始化一條蛇
因為蛇以及食物 本質上都是一個坐標,所以我們可以定義一個新的數據類型Node,每一個Node都是一個存儲了兩個變量(x、y)的結構體,再通過Node來定義蛇和食物。
- typedef struct node{
- int x;
- int y;
- }Node;
-
Node snake[60];
好了,我們現在定義了一條叫snake的蛇。為了這條蛇肥胖適中長寬比例一致,我們用"⊙"代表蛇的每一節。剛開始我們令蛇出現在地圖中間位置,蛇頭在右,共3個節點。所以我們需要求得每個節點的坐標。
- void InitializeSnake()
- {
- int i;
- for(i=0;i<3;i++)
- {
- snake[i].x = (LONG/2-i*2);//(30,15)(28,15)(26,15)
- snake[i].y = WIDTH/2;
- Pos(snake[i].x,snake[i].y);
- printf("⊙");
- }
-
}
這樣我們就在(30,15)(28,15)(26,15)三個坐標處確定了一條蛇。X坐標之間減2是因為"⊙"在X軸占兩個基本值。
y\x |
26 |
27 |
28 |
29 |
30 |
31 |
15 |
⊙ |
⊙ |
⊙ |
3.隨機出現食物
先創建一個變量來存儲食物的坐標
Node food;
得到它的坐標其實就是用隨機值對長、寬取余,使值在區間(地圖)範圍內。
- void CreateFood()
- {
- int i;
- srand((unsigned int)time(0));
- while(1)
- {
- do{
- food.x = rand()%(LONG-6)+2;
- }while(food.x%2!=0);
- food.y = rand()%(WIDTH-2)+1;
- for(i=0;i<3+length;i++)
- if(food.x==snake[i].x && food.y==snake[i].y)
- {
- i=-1;
- break;
- }
- if(i>=0)
- {
- Pos(food.x,food.y);
- printf("●");
- break;
- }
- }
- //AfterEatFood();
-
}
X的坐標值求法為rand()%(LONG-6)+2,因為食物"●"也是兩個字符的位置,所以它可能的取值為(2,y)(4,y)…(56,y)上下變寬共30個字符,從0開始,每個+2,所以最後一個為(58,y)
Rand()%(LONG)的取值範圍為0~59而x=1,x=2,x=58,x=59是地圖範圍,所以得對LONG-6(60-6=54)取余,這樣取值範圍就是0~54,再加2,就成了2~56.又因為蛇的各節坐標及移動x坐標都是+2,所以食物的x坐標必須是偶數,這可以用一個do(…)while()搞定,先取值,再判斷,不行就再取值
Y的坐標稍微簡單些,只要保證坐標值在1~28就行。
另外求出了坐標之後要判斷食物是否與蛇身重合,重合的話重新賦值。
搞完上面的,我們就有了一個基本的(靜態)效果了,現在我們要讓它動起來
註:第86行是設置控制臺窗口長、寬的系統函數。
4.讓蛇動起來
蛇每次移動背後發生的事就是數組裏的值改變,再在每個坐標位置打印蛇身。
為了讓蛇一直動,我們就需要一個循環
- while(1)
- {
- //獲得輸入,改變坐標
- //在每個坐標處輸出
- }
首先,我們需要確定方向,而這需要兩個變量,一個是輸入值(可能是任意值),另一個則是確定方向的變量。
這裏介紹一個函數
- int kbhit(void);
- // 檢查當前是否有鍵盤輸入,若有則返回一個非0值,否則返回0
這是一個非阻塞函數,有鍵按下時返回非0,但此時按鍵碼仍然在鍵盤緩沖隊列中。所以在確定鍵盤有響應之後,再用一個char變量將輸入從緩沖區中調出來。
- if(kbhit())
- ch = getch();
再對ch做判斷,如果是符合情況(不能往後走等)的輸入,則開始執行switch改變坐標
- if(ch==‘w‘&&direction!=‘s‘)
- direction = ch;
- else if(ch==‘s‘&&direction!=‘w‘)
- direction = ch;
- else if(ch==‘a‘&&direction!=‘d‘)
- direction = ch;
- else if(ch==‘d‘&&direction!=‘a‘)
- direction = ch;
- else if(ch==‘ ‘)
- continue;
這裏設置空格是暫停,而為了讓蛇一開始就移動,我們把direction設置為d(往右)。
在方向確定了之後,再用一個switch語句進行坐標判斷
- switch(direction)
- {
- case ‘w‘:
- if(snake[0].x==food.x && snake[0].y-1==food.y)
- {
- length++;
- score+=10;
- snake[2+length].x = snake[2+length-1].x;
- snake[2+length].y = snake[2+length-1].y;
- for(i=length+3-2;i>0;i--)
- {
- snake[i].x = snake[i-1].x;
- snake[i].y = snake[i-1].y;
- }
- CreateFood();
- }
- else
- {
- Pos(snake[2+length].x,snake[2+length].y);
- printf(" ");
- for(i=length+3-1;i>0;i--)
- {
- snake[i].x = snake[i-1].x;
- snake[i].y = snake[i-1].y;
- }
- }
- snake[0].y -=1;
- break;
- case ‘s‘:
- //。。。
- case ‘a‘:
- //。。。
- case ‘d‘:
- //。。。
-
}
對蛇頭的下一步做判斷,如果吃到了食物的話,則先對分數等全局變量進行處理,再把snake[2+length-1](吃到食物後的倒數第二個變量)的值賦值給snake[2+length](此時新加的尾節)。
再從倒數第二節開始,把前一節的坐標值賦給後一節,直到第二節得到了之前蛇頭坐標。在食物被吃了之後,再調用隨機出現食物函數。
如果沒有吃到食物的話,先到之前最後一節的坐標處,輸入空格,算是銷毀它再對各節重新賦值。在蛇頭後每節都賦值完成之後,根據輸入值單獨對蛇頭賦值,如輸入是‘w‘,則往上,所以蛇頭縱坐標減一。
對其余輸入也是同樣的道理,在snake數組各值都更新之後,再用一個函數把它打印出來。
這樣移動部分就實現了,現在只需處理一些小模塊就行。
5.移動後的處理。
這一部分相對簡單,即對判斷蛇是否撞墻、是否咬到自身,再對這種情況做處理,我們用兩個函數搞定它
- int ThroughWall()
- {
- if(snake[0].x==0 || snake[0].x==58 ||
- snake[0].y==0 || snake[0].y==29)
- {
- Pos(25,15);
- printf("撞墻 遊戲結束");
- return 1;
- }
- Pos(0,WIDTH);
- printf(" ");
-
}
- int BiteItself()
- {
- int i;
- for(i=3;i<=2+length;i++)
- if((snake[0].x==snake[i].x) && (snake[0].y==snake[i].y))
- {
- Pos(25,15);
- printf("咬到自己 遊戲結束");
- return 1;
- }
- }
當返回值為1時,遊戲也就GG了。
- if(ThroughWall()==1)
- {
- Pos(25,WIDTH);
- system("pause");
- exit(0);
- }
- if(BiteItself()==1)
- {
- Pos(25,WIDTH);
- system("pause");
- exit(0);
- }
最後再加一行Sleep()函數,對刷新時間(每次重新打印的時間間隔)做處理。speed是一個變量,在每次吃到食物後遞減。
Sleep(speed);
源代碼在這:結構數組實現_貪吃蛇源碼
四、總結與反思。
首先從蛇的結構上來說,結構數組的實現直接無視了"效率"這個詞,數組占用大量空間且有容量限制,並不是一種好辦法。
其次是BUG的問題,在ThroughWall()函數中,在對蛇頭坐標進行判斷時在蛇頭移動到(x,1)位置時,遊戲直接結束,且沒有任何提示。
但詭異的是,在判斷後加入 Pos(0,WIDTH);printf(" "); 這兩行不相幹的語句後,這個問題解決了,而我對這兩行語句的原有目的則只是想把閃爍不停光標放到地圖外面去。
還有就是while()循環裏代碼行太多,特別是switch-case 裏各項,蛇身的移動(結構數組個元素坐標值的變換)應該抽象成一個move()函數。
五、其他。
這是對我第一份代碼(snakeV1.0)的重構,程序結構上有較大變化
重構期間研究了鏈表實現_貪吃蛇源碼,在結構上采用了裏面的部分思想。
個人空空如也的github:MagicXyxxx的github,今後會不定期更新一些亂七八糟的玩意兒,開心就好。
[C語言]貪吃蛇_結構數組實現