一、項(xiàng)目介紹
這是一個(gè)可以單人進(jìn)行的俄羅斯方塊小游戲。
按左右鍵移動(dòng)方塊,按上鍵可以旋轉(zhuǎn)方塊,按下鍵可以加速方塊的下落(需要控制好按下的時(shí)長(zhǎng),否則下一個(gè)方塊也會(huì)加速落下)。方塊碰到屏幕底部,或者碰到已經(jīng)堆積的方塊就會(huì)停下,此時(shí)上方會(huì)落下下一個(gè)方塊。右側(cè)會(huì)對(duì)下一個(gè)方塊的種類進(jìn)行提示。
編譯環(huán)境:visual c++ 6.0
第三方庫(kù):Easyx2022 注意需要提前安裝easyX,如沒(méi)有基礎(chǔ)可以先了解easyX圖形編程
二、運(yùn)行截圖
三、源碼解析
游戲邏輯:
1. 生成界面,初始化程序
2. 游戲開(kāi)始循環(huán)。每次循環(huán)檢測(cè)輸入按鍵做出反應(yīng),之后睡眠50毫秒,再開(kāi)始下一次循環(huán)。
3. 按住上鍵調(diào)用旋轉(zhuǎn)的函數(shù)。
4. 按住左右鍵,改變方塊的橫坐標(biāo),調(diào)用對(duì)應(yīng)函數(shù)。
5. Esc 鍵退出程序。
6. 在循環(huán)當(dāng)中,利用計(jì)時(shí)器判斷是否過(guò)了500毫秒,如果過(guò)了則下落一格。下落一格則檢測(cè)底部是否碰撞,碰撞則重新生成方塊,改變對(duì)下一個(gè)方塊的提示,清除連成一行的方塊并得分,檢測(cè)游戲是否結(jié)束。
7. 檢測(cè)是否按住下鍵,按住則加速下落。
8. 失敗后循環(huán)結(jié)束,退出游戲。
根據(jù)這些我們需要完成的功能,我們定義兩個(gè)類。其一為游戲類,內(nèi)置地圖,分?jǐn)?shù),時(shí)間等整局游戲的變量,以及設(shè)置地圖,判讀滿行,清除行等操作函數(shù)。
另一個(gè)類是方塊類,設(shè)置方塊的坐標(biāo),類型,旋轉(zhuǎn)方向,顏色等特征,以及對(duì)應(yīng)的初始化,添加,移動(dòng),旋轉(zhuǎn)等操作函數(shù)。
接下來(lái)是相關(guān)步驟的詳細(xì)解析。
界面生成以及初始化
SetWindowText(initgraph(350, 440), "俄羅斯方塊dotcpp.com"); // 設(shè)置繪圖顏色 setbkcolor(WHITE); cleardevice(); setlinecolor(BLACK); // 生成游戲界面和數(shù)據(jù) srand(time(NULL)); Block::generateBlockData(); Game game; game.drawMap(); game.drawPrompt(); Block b(game); Block nextBlock(game, 11, 2); // 下一方塊 clock_t start = 0; // 時(shí)鐘開(kāi)始時(shí)間 clock_t end; // 時(shí)鐘結(jié)束時(shí)間 ExMessage msg; nextBlock.draw();
這里用到了一些Easyx當(dāng)中的函數(shù)。如無(wú)注明,后文中非本程序定義的函數(shù)也都位于Easyx當(dāng)中。
setbkcolor()用于設(shè)置當(dāng)前設(shè)備繪圖背景色。
Cleardevice()使用當(dāng)前背景色清空繪圖設(shè)備.
Setlinecolor()用于設(shè)置當(dāng)前設(shè)備畫(huà)線顏色。
ExMessage結(jié)構(gòu)體用于保存鼠標(biāo)信息。
在此處我們還定義了幾個(gè)相關(guān)的函數(shù)。
Block::generateBlockData()用于設(shè)定不同種類方塊的信息。blockData是一個(gè)三維數(shù)組,用于保存所有方塊的數(shù)據(jù),第一位是方塊種類,二三位則是橫縱坐標(biāo),調(diào)節(jié)這些數(shù)據(jù)與游戲內(nèi)容一致。
roundrect用于畫(huà)無(wú)填充的圓角矩形。
rectangle用于畫(huà)無(wú)填充的矩形。
setfillcolor用于設(shè)置當(dāng)前設(shè)備填充顏色。
fillrectangle用于畫(huà)有邊框的填充矩形。
game.drawPrompt()用于繪制提示界面,主要是各種提示。
其中最主要用到的outtextxy是在x,y坐標(biāo)處輸出文字。
其余的還有settextstyle設(shè)置當(dāng)前文字樣式。
gettextstyle獲取當(dāng)前文字樣式。
settextcolor設(shè)置當(dāng)前文字顏色。
Block::draw()用來(lái)繪制方塊。
還是用到了setfillcolor和fillrectangle,根據(jù)方塊數(shù)據(jù)對(duì)應(yīng)的xy坐標(biāo),用雙層循環(huán)的方式輸出結(jié)果。注意在Y坐標(biāo)為負(fù)時(shí)不繪制。
2. 游戲循環(huán)
while (true) { b.clear(); clearrectangle(20, 20, 220, 420); game.drawMap(); …… b.draw(); game.clearLine(); FlushBatchDraw(); // 刷新緩沖區(qū) Sleep(50); // 每 50 毫秒接收一次按鍵 }
b是Block類的一個(gè)對(duì)象。
clearrectangle用于清空矩形區(qū)域。
FlushBatchDraw用于執(zhí)行未完成的繪制任務(wù)。
其中也用到了Block類中定義的兩個(gè)函數(shù)Block::draw和Block::clear。前者已經(jīng)在前文中提到過(guò),而后者則是把繪制函數(shù)換成了clearrectangle,以此來(lái)清除繪制的方塊。通過(guò)調(diào)用clear再調(diào)用draw,可以及時(shí)地顯示出方塊的變化。
在之后還調(diào)用了Game::clearLine()這個(gè)函數(shù)。其目的是判斷哪一行已經(jīng)滿了,并將其清除以及增加得分。
void Game::clearLine() { int line = -1; // 判斷哪一行滿行 for (int j = 0; j < MAP_HEIGHT; j++) { if (checkLine(j)) { line = j; break; } } if (line != -1) { // 將上一行移至滿行 for (int j = line; j > 0; j--) { for (int i = 0; i < MAP_WIDTH; i++) { map[i][j] = map[i][j - 1]; } } score += 10; // 將游戲分?jǐn)?shù)加 10 } drawPrompt(); }
首先用循環(huán)判定哪一行是滿的,其中用到了Game::checkLine(),其內(nèi)部也是一個(gè)循環(huán),依次檢測(cè)某一y坐標(biāo)對(duì)應(yīng)的所有x坐標(biāo)是否都為1。如果檢測(cè)到某行是滿的,跳出循環(huán),繼續(xù)函數(shù)的后續(xù)部分。不用擔(dān)心沒(méi)有判斷所有行是否填滿,因?yàn)樵?0毫秒之后,該函數(shù)會(huì)再次被調(diào)用。
然后,將被消除那一行上方的所有行都向下移動(dòng)一行。循環(huán)從j行開(kāi)始,以y坐標(biāo)遞減的方式執(zhí)行,讓map變量(所有方塊的位置)對(duì)應(yīng)坐標(biāo)位置的數(shù)據(jù)等于其y坐標(biāo)減一的數(shù)據(jù)。最后如果消除,則將游戲分?jǐn)?shù)加10。調(diào)用繪制提示界面的函數(shù)drawPrompt(),將得分顯示在旁邊。
3. 按下上鍵
這里先看我們?nèi)绾潍@取鍵盤信息。
while (peekmessage(&msg, EM_KEY) && msg.message == WM_KEYDOWN)
{
switch (msg.vkcode)
{
peekmessage用于獲取一個(gè)消息,并立即返回。其中參數(shù)&msg表示用指針的形式保存獲取到的信息,而 EM_KEY意味這是鍵盤信息。這個(gè)函數(shù)獲取消息的返回值為True,如果沒(méi)有獲取到,則返回False。
右邊的msg為ExMessage結(jié)構(gòu)體的對(duì)象,msg.message == WM_KEYDOWN表明獲取到的信息為鍵盤按下。上面的代碼意味著,如果你向程序發(fā)出了指令,并且該指令是鍵盤按下時(shí),循環(huán)才會(huì)執(zhí)行。
msg.vkcode代表按鍵的虛擬鍵碼。很顯然,
case 'W':
case VK_UP:
b.rotate();
break;
意味著如果你按下上鍵或者W鍵,才會(huì)執(zhí)行b.rotate()。
b.rotate()用于改變b(Block)類型的數(shù)據(jù)。執(zhí)行會(huì)改變其中block[4][4]這個(gè)二維數(shù)組,這個(gè)數(shù)組用1,0來(lái)表示在對(duì)應(yīng)位置是否存在方塊。對(duì)每種情況分類討論,得出旋轉(zhuǎn)之后的結(jié)果。
4.按下左右鍵,方塊左右移動(dòng)
// 左鍵移動(dòng)
case 'A':
case VK_LEFT:
b.move(1);
break;
// 右鍵移動(dòng)
case 'D':
case VK_RIGHT:
b.move(2);
break;
在Block類中,我們定義了move函數(shù)。其中只有一個(gè)參數(shù),0 表示下移一格,1 表示左移一格,2 表示右移一格,當(dāng)下移檢測(cè)到碰撞時(shí)返回 true。
switch (direction)
{
case 0:
y++;
if (checkCollision())
{
y--;
return true;
}
break;
其邏輯很簡(jiǎn)單,就是根據(jù)輸入的不同情況,改變x或者y的坐標(biāo)(x,y是整個(gè)大方塊的坐標(biāo))。之后進(jìn)行碰撞檢測(cè),如果碰撞,取消移動(dòng)。上面是其中下方碰撞情況,注意如果是向左或向右移動(dòng),則需要返回False,因?yàn)榉祷豑rue時(shí),方塊便落到地圖上了。
Block::checkCollision()用于碰撞檢測(cè)。 bool Block::checkCollision() const { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { // 判斷方塊是否與地圖發(fā)生碰撞,頂部不判斷 if ((game.getMap(x + i, y + j) || 20 + BLOCK_WIDTH * (x + i) < 20 || 20 + BLOCK_WIDTH * (x + i) + BLOCK_WIDTH > 220 || 20 + BLOCK_WIDTH * (j + y) + BLOCK_WIDTH > 420) && block[i][j]) { return true; } } } return false; }
用雙重循環(huán),判定每一個(gè)小方塊是否超出地圖邊界,或者與地圖上方塊重疊。
四、完整源碼
C語(yǔ)言網(wǎng)提供由在職研發(fā)工程師或ACM藍(lán)橋杯競(jìng)賽優(yōu)秀選手錄制的視頻教程,并配有習(xí)題和答疑,點(diǎn)擊了解:
一點(diǎn)編程也不會(huì)寫(xiě)的:零基礎(chǔ)C語(yǔ)言學(xué)練課程
解決困擾你多年的C語(yǔ)言疑難雜癥特性的C語(yǔ)言進(jìn)階課程
從零到寫(xiě)出一個(gè)爬蟲(chóng)的Python編程課程
只會(huì)語(yǔ)法寫(xiě)不出代碼?手把手帶你寫(xiě)100個(gè)編程真題的編程百練課程
信息學(xué)奧賽或C++選手的 必學(xué)C++課程
藍(lán)橋杯ACM、信息學(xué)奧賽的必學(xué)課程:算法競(jìng)賽課入門課程
手把手講解近五年真題的藍(lán)橋杯輔導(dǎo)課程