一、設(shè)計(jì)思路
為劍河等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計(jì)制作服務(wù),及劍河網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)、劍河網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!
蛇身本質(zhì)上就是個結(jié)構(gòu)數(shù)組,數(shù)組里存儲了坐標(biāo)x、y的值,再通過一個循環(huán)把它打印出來,蛇的移動則是不斷地刷新重新打印。所以撞墻、咬到自己只是數(shù)組x、y值的簡單比較。
二、用上的知識點(diǎn)
結(jié)構(gòu)數(shù)組Windows API函數(shù)
三、具體實(shí)現(xiàn)
先來實(shí)現(xiàn)靜態(tài)頁面,把地圖、初始蛇身、食物搞定。
這里需要用到Windows API的知識,也就是對控制臺上坐標(biāo)的修改
//這段代碼來自參考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中定義的一種結(jié)構(gòu),表示在控制臺上的坐標(biāo)
typedef struct _COORD { SHORT X; // horizontal coordinate SHORT Y; // vertical coordinate } COORD;
而代碼中第七行則是獲得屏幕緩沖區(qū)的句柄,第八行是直接修改光標(biāo)位置的函數(shù)。
1.地圖。
有了Pos()函數(shù),打印一個框就不是問題了。假如我們用"-"作為上下邊框,把"|"作為左右邊框,這看起來沒什么不妥,但其實(shí)我們已經(jīng)掉進(jìn)了坑里,直接上代碼及實(shí)際效果圖吧。
//LONG==60 //WIDTH==30 void CreateMap() { int i; for(i=0;i
發(fā)現(xiàn)了問題嗎?這是一條正常的蛇。。。那為什么看起來不正常呢?我們把邊框都換成"#"來看看…
這就清楚多了啊,要知道我們上下邊框可是各有60個"#"的,長60寬30的長方形輸出之后竟然成了個正方形。
原因在這
控制臺上每個字符的長寬比例(像素點(diǎn))是不同的,所以才會出現(xiàn)上圖這種蛋疼的情況。
解決方法其實(shí)也很簡單,我們需要引入一些特殊符號,比如"●""■""⊙"等,這些字符的特點(diǎn)是它占據(jù)兩個普通字符的位置
所以上下邊框就有60/2=30個符號,要讓它仍然是個正方形的話,左右也可以設(shè)為30(28+2)個符號.
代碼及效果圖如下
void CreateMap() { int i; for(i=0;i
這樣看就舒服多了,不過也讓復(fù)雜度提升了一些,上邊框每個符號的坐標(biāo)分別是(0,0)(2,0)(4,0)…(2*n-2,0)這個在蛇的移動及食物的模塊再提。
2.初始化一條蛇
因?yàn)樯咭约笆澄?本質(zhì)上都是一個坐標(biāo),所以我們可以定義一個新的數(shù)據(jù)類型Node,每一個Node都是一個存儲了兩個變量(x、y)的結(jié)構(gòu)體,再通過Node來定義蛇和食物。
typedef struct node{ int x; int y; }Node; Node snake[60];
好了,我們現(xiàn)在定義了一條叫snake的蛇。為了這條蛇肥胖適中長寬比例一致,我們用"⊙"代表蛇的每一節(jié)。剛開始我們令蛇出現(xiàn)在地圖中間位置,蛇頭在右,共3個節(jié)點(diǎn)。所以我們需要求得每個節(jié)點(diǎn)的坐標(biāo)。
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)三個坐標(biāo)處確定了一條蛇。X坐標(biāo)之間減2是因?yàn)?⊙"在X軸占兩個基本值。
3.隨機(jī)出現(xiàn)食物
先創(chuàng)建一個變量來存儲食物的坐標(biāo)
Nodefood;
得到它的坐標(biāo)其實(shí)就是用隨機(jī)值對長、寬取余,使值在區(qū)間(地圖)范圍內(nèi)。
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的坐標(biāo)值求法為rand()%(LONG-6)+2,因?yàn)槭澄?●"也是兩個字符的位置,所以它可能的取值為(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.又因?yàn)樯叩母鞴?jié)坐標(biāo)及移動x坐標(biāo)都是+2,所以食物的x坐標(biāo)必須是偶數(shù),這可以用一個do(…)while()搞定,先取值,再判斷,不行就再取值
Y的坐標(biāo)稍微簡單些,只要保證坐標(biāo)值在1~28就行。
另外求出了坐標(biāo)之后要判斷食物是否與蛇身重合,重合的話重新賦值。
搞完上面的,我們就有了一個基本的(靜態(tài))效果了,現(xiàn)在我們要讓它動起來
注:第86行是設(shè)置控制臺窗口長、寬的系統(tǒng)函數(shù)。
4.讓蛇動起來
蛇每次移動背后發(fā)生的事就是數(shù)組里的值改變,再在每個坐標(biāo)位置打印蛇身。
為了讓蛇一直動,我們就需要一個循環(huán)
while(1) { //獲得輸入,改變坐標(biāo) //在每個坐標(biāo)處輸出 }
首先,我們需要確定方向,而這需要兩個變量,一個是輸入值(可能是任意值),另一個則是確定方向的變量。
這里介紹一個函數(shù)
int kbhit(void); // 檢查當(dāng)前是否有鍵盤輸入,若有則返回一個非0值,否則返回0
這是一個非阻塞函數(shù),有鍵按下時返回非0,但此時按鍵碼仍然在鍵盤緩沖隊(duì)列中。所以在確定鍵盤有響應(yīng)之后,再用一個char變量將輸入從緩沖區(qū)中調(diào)出來。
if(kbhit()) ch = getch();
再對ch做判斷,如果是符合情況(不能往后走等)的輸入,則開始執(zhí)行switch改變坐標(biāo)
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;
這里設(shè)置空格是暫停,而為了讓蛇一開始就移動,我們把direction設(shè)置為d(往右)。
在方向確定了之后,再用一個switch語句進(jìn)行坐標(biāo)判斷
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': //。。。 }
對蛇頭的下一步做判斷,如果吃到了食物的話,則先對分?jǐn)?shù)等全局變量進(jìn)行處理,再把snake[2+length-1](吃到食物后的倒數(shù)第二個變量)的值賦值給snake[2+length](此時新加的尾節(jié))。
再從倒數(shù)第二節(jié)開始,把前一節(jié)的坐標(biāo)值賦給后一節(jié),直到第二節(jié)得到了之前蛇頭坐標(biāo)。在食物被吃了之后,再調(diào)用隨機(jī)出現(xiàn)食物函數(shù)。
如果沒有吃到食物的話,先到之前最后一節(jié)的坐標(biāo)處,輸入空格,算是銷毀它再對各節(jié)重新賦值。在蛇頭后每節(jié)都賦值完成之后,根據(jù)輸入值單獨(dú)對蛇頭賦值,如輸入是'w',則往上,所以蛇頭縱坐標(biāo)減一。
對其余輸入也是同樣的道理,在snake數(shù)組各值都更新之后,再用一個函數(shù)把它打印出來。
這樣移動部分就實(shí)現(xiàn)了,現(xiàn)在只需處理一些小模塊就行。
5.移動后的處理。
這一部分相對簡單,即對判斷蛇是否撞墻、是否咬到自身,再對這種情況做處理,我們用兩個函數(shù)搞定它
int ThroughWall() { if(snake[0].x==0 || snake[0].x==58 || snake[0].y==0 || snake[0].y==29) { Pos(25,15); printf("撞墻 游戲結(jié)束"); 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("咬到自己 游戲結(jié)束"); return 1; } }
當(dāng)返回值為1時,游戲也就GG了。
if(ThroughWall()==1) { Pos(25,WIDTH); system("pause"); exit(0); } if(BiteItself()==1) { Pos(25,WIDTH); system("pause"); exit(0); }
最后再加一行Sleep()函數(shù),對刷新時間(每次重新打印的時間間隔)做處理。speed是一個變量,在每次吃到食物后遞減。
Sleep(speed);
源代碼在這:結(jié)構(gòu)數(shù)組實(shí)現(xiàn)_貪吃蛇源碼
四、總結(jié)與反思。
首先從蛇的結(jié)構(gòu)上來說,結(jié)構(gòu)數(shù)組的實(shí)現(xiàn)直接無視了"效率"這個詞,數(shù)組占用大量空間且有容量限制,并不是一種好辦法。
其次是BUG的問題,在ThroughWall()函數(shù)中,在對蛇頭坐標(biāo)進(jìn)行判斷時在蛇頭移動到(x,1)位置時,游戲直接結(jié)束,且沒有任何提示。
但詭異的是,在判斷后加入 Pos(0,WIDTH);printf(" "); 這兩行不相干的語句后,這個問題解決了,而我對這兩行語句的原有目的則只是想把閃爍不停光標(biāo)放到地圖外面去。
還有就是while()循環(huán)里代碼行太多,特別是switch-case 里各項(xiàng),蛇身的移動(結(jié)構(gòu)數(shù)組個元素坐標(biāo)值的變換)應(yīng)該抽象成一個move()函數(shù)。
五、其他。
這是對我第一份代碼(snakeV1.0)的重構(gòu),程序結(jié)構(gòu)上有較大變化
重構(gòu)期間研究了鏈表實(shí)現(xiàn)_貪吃蛇源碼,在結(jié)構(gòu)上采用了里面的部分思想。
個人空空如也的github:MagicXyxxx的github
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。