用C语言实现俄罗斯方块游戏
一、工程聲明
這篇文章包括搭建游戲背景、方塊的建模、按鍵響應(yīng)、方塊的隨機(jī)生成和下一塊方塊的生成等,以此記錄一次C語言的練習(xí)。本次工程涉及到圖形化,可以使用EasyX庫(kù)來畫出簡(jiǎn)單的圖形,這個(gè)庫(kù)的下載和使用均在百度可查詢到。
本工程使用的軟件為Visual Studio 2019,新建C++空項(xiàng)目編寫代碼。文件的工程結(jié)構(gòu)如下圖所示,一共有三個(gè)文件,一個(gè)頭文件和兩個(gè)C++文件。之所以采用cpp后綴,是因?yàn)镋asyX庫(kù)只可以在C++文件使用,但是工程代碼都是C語言編寫的。
基本概念:
- 方格:指的是游戲區(qū)域的小格子,大小為20*20;
- 方塊:指的是俄羅斯方塊,由4*4共16個(gè)小格子構(gòu)成,大小為80*80;
- 游戲區(qū)域:背景為網(wǎng)格線為游戲區(qū)域,大小為300*500,存在25*15=375個(gè)方格;
- 圖形區(qū)域:圖形區(qū)域包括游戲區(qū)域和文字顯示部分,大小為500*500;
- 方塊的坐標(biāo)點(diǎn):定義方塊左下角的坐標(biāo)點(diǎn)為整個(gè)方塊的坐標(biāo)點(diǎn);
- 方塊的行序號(hào)和列序號(hào):數(shù)格子的數(shù)量,第一個(gè)格子的行序號(hào)為0,列序號(hào)為0;序號(hào)與坐標(biāo)點(diǎn)對(duì)應(yīng)的關(guān)系為:行序號(hào)*20=坐標(biāo)點(diǎn)的y,列序號(hào)*20=坐標(biāo)點(diǎn)的x;
二、游戲背景的搭建
//game.c void context(void) {initgraph(500, 500); // 初始化圖形模式setorigin(0, 500); //重新定義原點(diǎn)setaspectratio(1, -1); //將Y上半部分設(shè)定為正半軸setlinestyle(PS_SOLID); //設(shè)定畫線樣式為虛線setlinecolor(WHITE); //線條顏色為白色for (size_t i = 0; i <= 25; i++) //畫出游戲區(qū)域網(wǎng)格線,寬300,高500line(0, 0 + 20 * i, 300, 0 + 20 * i); //橫線for (size_t i = 0; i <= 15; i++)line(0 + 20 * i, 0, 0 + 20 * i, 500); //豎線for (size_t i = 0; i <= 4; i++) //畫出下一個(gè)方塊網(wǎng)格線,寬80,高80line(370, 300 + 20 * i, 450, 300+20 * i); //橫線for (size_t i = 0; i <= 4; i++)line(370 + 20 * i, 300, 370 + 20 * i, 380); //豎線setaspectratio(1, 1);settextstyle(25, 0, _T("Consolas"));outtextxy(310, -430, _T("SCORE:0 points"));outtextxy(310, -410, _T("下一個(gè)方塊:"));outtextxy(310, -270, _T("w 轉(zhuǎn)換方向"));outtextxy(310, -240, _T("s 加快下降速度"));outtextxy(310, -210, _T("a 左移"));outtextxy(310, -180, _T("d 右移"));outtextxy(310, -150, _T("h 重啟"));setaspectratio(1, -1); }三、方塊的建模
?
?
?
研究俄羅斯方塊的各種形狀,會(huì)發(fā)現(xiàn)他們都在在4*4的方格當(dāng)中的,考慮到C語言的short數(shù)據(jù)類型剛好有2個(gè)字節(jié),16bit的長(zhǎng)度。所以我們可以用short數(shù)據(jù)類型來表示我們的方塊。比如下面的俄羅斯方塊可以表示為,1000 1000 1000 1000,轉(zhuǎn)化為十六進(jìn)制就是8888H。
?
考慮到每個(gè)俄羅斯方塊有不同的方向,不同的方向?qū)?yīng)不同的姿態(tài),所以我新建一個(gè)二維數(shù)組來表示所有的俄羅斯方塊。一共有7鐘俄羅斯方塊,每種有4個(gè)方向的變化,有一些變化還是原來的樣子。在game.h頭文件,聲明展示俄羅斯方塊和位置結(jié)構(gòu)體,以方塊左下角那個(gè)點(diǎn)的坐標(biāo)為整個(gè)方塊的坐標(biāo)點(diǎn)。
?
//main.c unsigned short Diamond[7][4] = {{0x000f,0x8888,0x000f,0x8888},{0x008E,0x0c88,0x00E2,0x044c},{0x002E,0x088c,0x00E8,0x0c44},{0x00CC,0x00CC,0x00CC,0x00CC},{0x006C,0x08c4,0x006c,0x08c4},{0x004E,0x08c8,0x00E4,0x04c4},{0x00C6,0x04c8,0x00c6,0x04c8} }; //game.h //方塊的坐標(biāo) typedef struct LOCATE {int x;int y; } Location;//俄羅斯方塊展示函數(shù) void DisplayDiamond(unsigned short diamond, Location Loca, int cur_color);俄羅斯方塊展示函數(shù)用到的主要是EasyX里面的fillrectangle函數(shù),可以畫有邊框的填充矩形。
?
下面是俄羅斯方塊展示函數(shù)具體的代碼,判斷short數(shù)據(jù)中每一位是否是1,是的話就填充一個(gè)小格子,不是就跳過,循環(huán)操作,進(jìn)行四行四列共16次判斷。擦除用的也是這個(gè)函數(shù),只不過顏色換為了背景色(黑色),就變相達(dá)到了擦除效果。
int color[15][35] = { 0 }; //定義網(wǎng)格屬性//俄羅斯方塊展示函數(shù) void DisplayDiamond(unsigned short diamond, Location Loca, int draw_color) {int num_x, num_y;int k = 0;setfillcolor(draw_color); //設(shè)定填充顏色 for (int i = 3; i >= 0; i--) //四行填充{for (int j = 0; j < 4; j++) //四列填充{if (diamond & (0x8000 >> k))//判斷格子是否存在1,20是格子邊長(zhǎng){fillrectangle(Loca.x + 20 * j, Loca.y + 20 * i, Loca.x + 20 * (j + 1), Loca.y + 20 * (i + 1));num_x = Loca.x / 20 + j;num_y = Loca.y / 20 + i;color[num_x][num_y] = draw_color;} k++;}} }?
設(shè)定填充的顏色,這是EasyX的內(nèi)置函數(shù),直接引用就行;
雙重循環(huán),進(jìn)行四行、四列填充;
在函數(shù)內(nèi)部進(jìn)行判斷,對(duì)short數(shù)據(jù)類型的diamond(俄羅斯方塊)的每一位進(jìn)行判斷,為1則進(jìn)行填充,為0則跳過;
color是一個(gè)二維數(shù)組,用來記錄格子對(duì)應(yīng)的顏色,共25行15列,剛好對(duì)應(yīng)格子的數(shù)量。該變量是一個(gè)全局變量,所以game.c文件的函數(shù)都可以引用或賦值該變量。
展示一下全部俄羅斯方塊,驗(yàn)證一下前面建模是否正確,由下圖可以看到,形狀是正確的,符合我們的設(shè)想。
?
?
?
四、按鍵響應(yīng)
?
移動(dòng)和翻轉(zhuǎn)需要鍵盤來進(jìn)行選擇,所以這涉及到與鍵盤的交互作用,所以需要包含頭文件conio.h。conio.h不是C標(biāo)準(zhǔn)庫(kù)中的頭文件,是vc下的一個(gè)頭文件。conio是Console Input/Output(控制臺(tái)輸入輸出)的簡(jiǎn)寫,其中定義了通過控制臺(tái)進(jìn)行數(shù)據(jù)輸入和數(shù)據(jù)輸出的函數(shù),主要是一些用戶通過按鍵盤產(chǎn)生的對(duì)應(yīng)操作,比如getch()函數(shù)等等。
1.實(shí)現(xiàn)方塊的自由下落
//main.c while (MoveEnable(CurDiamond, CurLocation, EnableDown) == 0){DisplayDiamond(CurDiamond, CurLocation, BLACK); //先擦除CurLocation.y -= 20;DisplayDiamond(CurDiamond, CurLocation, cur_color); //再更新Sleep(speed);if (_kbhit()) //如果敲擊了鍵盤,就會(huì)返回1{userHit = _getch(); //獲取敲擊鍵盤字符Keyboard(userHit, &row, &column, &CurLocation, &speed);CurDiamond = Diamond[row][column];}}- 用MoveEnable函數(shù)進(jìn)行判斷是否能下降,能下降就進(jìn)入循環(huán),不能下降就跳出循環(huán)。MoveEnable函數(shù)是自己定義的一個(gè)移動(dòng)判斷函數(shù),后續(xù)會(huì)進(jìn)行講解;
- 循環(huán)內(nèi)部第一步先擦除,即把原來有顏色的俄羅斯方塊,填充為黑色,相當(dāng)于擦除效果;
- 把坐標(biāo)點(diǎn)的y減去20,然后重新畫出該方塊,進(jìn)行一定的延時(shí),實(shí)現(xiàn)下降效果;
- 接下來進(jìn)行鍵盤的響應(yīng),_kbhit是EasyX的內(nèi)部函數(shù),主要用于檢查是否有按鍵輸入;
- 將獲得的按鍵字符傳入Keyboard字符處理函數(shù),獲得相應(yīng)的效果。
?
以上是字符處理函數(shù),里面僅有判斷是否可以下移部分代碼,是否可以左移/右移代碼類似,這里就不進(jìn)行展示了。
swith語句選擇究竟是判斷下降、左移、右移;下降判斷,其實(shí)就是找出每一列最下面那個(gè)有色小格子到底在第幾行,找到之后就對(duì)其下面那個(gè)格子進(jìn)行判斷,如果下面那個(gè)格子也是有色小格子,就說明不能下降了;如果下面那個(gè)格子是黑色小格子,那就可以下降;接下來要找第二列的底部小格子,因?yàn)橛锌赡艿谝涣袥]被堵住,第二列被堵住了。當(dāng)其中一列下面存在有色格子,就可以把stop變量置1,然后跳出循環(huán),不用判斷后面的列了。函數(shù)返回bool數(shù)據(jù)類型,stop這個(gè)變量。如果是1,代表存在阻礙,不能再移動(dòng)了。如果是0,代表不存在阻礙,可以繼續(xù)移動(dòng)。以上便是函數(shù)的邏輯。
實(shí)現(xiàn)方塊的改變方向和速度改變
//game.c //響應(yīng)鍵盤函數(shù) void Keyboard(char userHit, int *row, int *column, Location *CurLocation, int *speed) {unsigned short Diamond[7][4] = {{0x000f,0x8888,0x000f,0x8888},{0x008E,0x0c88,0x00E2,0x044c},{0x002E,0x088c,0x00E8,0x0c44},{0x00CC,0x00CC,0x00CC,0x00CC},{0x006C,0x08c4,0x006c,0x08c4},{0x004E,0x08c8,0x00E4,0x04c4},{0x00C6,0x04c8,0x00c6,0x04c8} };unsigned short CurDiamond;switch(userHit){case 'w': //改變方向{CurDiamond = Diamond[*row][*column];DisplayDiamond(CurDiamond, *CurLocation, BLACK); //先擦除if ((*column) < 3)(*column)++;else(*column) = 0;CurDiamond = Diamond[*row][*column]; DisplayDiamond(CurDiamond, *CurLocation, cur_color); //再更新break;}case 's': //增加下降速度{if ((*speed) > 50 ){(*speed) -= 50;} break;}case 'h': //重新開始{for (size_t i =0; i < 25; i++) //25行{for (size_t j = 0; j < 15; j++) //15列{color[j][i] = BLACK; //每一個(gè)格子填充黑色setfillcolor(color[j][i]);fillrectangle(20 * j, 20 * i, 20 * (j + 1), 20 * (i + 1));}}break;}} }以上是響應(yīng)鍵盤函數(shù)。首先對(duì)傳入的字符進(jìn)行判斷,看到底應(yīng)該進(jìn)行左移、右移、加快下降速度、改變方向、重啟操作。因?yàn)槲覀冄b有俄羅斯方塊的模型是7*4的二維數(shù)組,對(duì)應(yīng)是7種俄羅斯方塊的四種方向。所以改變方向其實(shí)就是改變Dianmond的列號(hào),我們只要在0-4循環(huán)就可以達(dá)到這種效果。因?yàn)橐研泻土卸急4嫦聛?#xff0c;傳出去函數(shù)外部,所以這里用的是指針。
速度是通過影響中間的間隔時(shí)間來改變的,只要間隔時(shí)間短,下降速度就快。所以每次按下s,speed變量減少50,速度就會(huì)變快。同樣的,speed也要傳出去函數(shù)外部,用的也是指針。按下‘h’是重新開始,就是清空已經(jīng)下落的俄羅斯方塊,所以只要把游戲區(qū)域的所有格子填充黑色就可以達(dá)到效果,同時(shí)要改變格子的屬性color。
左右移動(dòng)和方塊的下落是同樣的原理,只不過改變的是x,每次x改變20,就是一格子的長(zhǎng)度。先進(jìn)行擦除,后重新更新方塊,就可以達(dá)到預(yù)期效果。
五、方塊的隨機(jī)生成和下一塊方塊的生成
//main.cunsigned short CurDiamond; //下一個(gè)方塊unsigned short NewDiamond; //下一個(gè)方塊context(); //畫出游戲背景row = 0;column = 0;CurDiamond = Diamond[row][column];cur_color = BLUE;while(1){ RandomDiamond(&new_row, &new_column, &CurLocation, &speed); //隨機(jī)生成方塊NewDiamond = Diamond[new_row][new_column];DisplayDiamond(NewDiamond, { 370,300 }, new_color); //畫出下一塊方塊while (MoveEnable(CurDiamond, CurLocation, EnableDown) == 0){//自由下落+按鍵響應(yīng)代碼}row = new_row;column = new_column;cur_color = new_color;CurDiamond = Diamond[row][column];DisplayDiamond(NewDiamond, { 370,300 }, BLACK); //先擦除FullJudge();}方塊的隨機(jī)生成用的是自定義的RandomDiamond函數(shù),生成方塊在Diamond數(shù)組的行和列,將值賦給NewDiamond,然后在下一塊方塊區(qū)域畫出來。while部分進(jìn)行的curDiamond方塊的自由下落和按鍵響應(yīng),當(dāng)不能下落之后,就會(huì)跳出while循環(huán)。將上面隨機(jī)生成的新行號(hào)和列號(hào)賦給現(xiàn)在的row和column,生成的顏色也賦給cur_color。然后就可以擦除下一塊區(qū)域的方塊,等待新的生成。
在最后,會(huì)進(jìn)行滿行判斷,滿行則消失,這個(gè)后面再講。
//game.c void RandomDiamond(int* row, int* column, Location* CurLocation, int* speed) {static int num = 0;int color_Diamond[7] = { BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, YELLOW };*row = rand() % 7; //生成0到6隨機(jī)數(shù)*column = rand() % 4; //生成0到3隨機(jī)數(shù) //*row = 0;*CurLocation = { 140,500 };*speed = 200;if (num == 6) num = 0;new_color = color_Diamond[num++]; }以上是RandomDiamond俄羅斯方塊隨機(jī)生成函數(shù),一開始會(huì)生成隨機(jī)的row和column,這個(gè)就是相當(dāng)于隨機(jī)的俄羅斯方塊形狀。同時(shí)也會(huì)生成顏色,顏色用的是數(shù)組內(nèi)部循環(huán),于是就生成了五顏六色的俄羅斯方塊。
六、滿行消除和更新
//game.c void FullJudge() {int color_num=0;for (size_t i = 0; i < 25; i++) //25行{for (size_t j = 0; j < 15; j++) //15列if (color[j][i] != BLACK) color_num++; if (color_num == 15) //遇到滿行{setfillcolor(BLACK); //設(shè)定填充黑色for (size_t j = 0; j < 15; j++) //填充該行15列,即消除該行 { fillrectangle(j*20, i*20 ,(j+1)*20, (i+1)*20);color[j][i] = BLACK;}Updata(i);//更新i--; //重新判斷該行}color_num = 0;} }//消除滿行后,上面往后降,重新判斷該行 void Updata(int row) {//從滿行的第row行開始for (size_t i = row; i < 25; i++){for (size_t j = 0; j < 15; j++) //15列{color[j][i] = color[j][i + 1]; //每一個(gè)格子等于上面格子的顏色setfillcolor(color[j][i]);fillrectangle( 20 * j, 20 * i, 20 * (j + 1), 20 * (i + 1));} }static int game_point = 0;game_point++; //更新分?jǐn)?shù)setaspectratio(1, 1);TCHAR s1[5];settextstyle(25, 0, _T("Consolas"));swprintf_s(s1, _T("%d"), game_point);outtextxy(380, -426, s1);setaspectratio(1, -1); }滿行判斷就是一行行來判斷,首先判斷第一行,對(duì)每個(gè)格子的color進(jìn)行判斷,如果統(tǒng)計(jì)格子顏色為非黑色(即存在方塊)的數(shù)量為列數(shù)量15,這就是遇到了滿行情況。判斷是滿行情況,那就對(duì)該行所有格子填充黑色,就相當(dāng)于消除。
消除之后,要把上面的格子落下來,相當(dāng)于整體往下移了一格,這就是Updata函數(shù)的任務(wù),其中還得更新一下分?jǐn)?shù),一行一分。整體下移之后要重新對(duì)該行進(jìn)行判斷,也許說不定下落的那一行又是滿行。之后循環(huán)判斷完25行,任務(wù)就完成了。
七、判斷真實(shí)寬度和真實(shí)高度
//game.c //計(jì)算方塊的真實(shí)高度 int GetHigh(unsigned short diamond) {int a=0;for (size_t i = 0; i < 4; i++){if ( (diamond & (0x000f << 4 * i)) > 0)a++;}return a; }//計(jì)算方塊的真實(shí)寬度 int GetWidth(unsigned short diamond) {int a = 0;for (size_t i = 0; i < 4; i++){if ((diamond & (0x8888 >> i)) > 0)a++;}return a; }從以下俄羅斯方塊能看出,雖然每個(gè)俄羅斯方塊都占據(jù)了4*4小格子的空間大小,但是實(shí)際上俄羅斯方塊的真實(shí)高度和寬度并不一定是4。在進(jìn)行左右移動(dòng)的時(shí)候,就會(huì)需要判斷俄羅斯方塊的真實(shí)高度和寬度,也就是寬度為1,高度為4。判斷的邏輯就是和0x000f、0x8888進(jìn)行與邏輯運(yùn)算,然后是否大于0。如果等于0,說明這一行/列都是0,那就是沒被占用的空間。
以上就是俄羅斯方塊代碼的大部分內(nèi)容,并不是全部代碼,有一些只講了邏輯,大家可以自由發(fā)揮。
八、運(yùn)行效果
?
總結(jié)
以上是生活随笔為你收集整理的用C语言实现俄罗斯方块游戏的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: templates(0.1)
- 下一篇: 自考感悟,话谈备忘录模式