專案地址:https://gitee.com/daycen/stm32-tetris/tree/master
使用Keil uVision5開啟即可
一、概述
本文介紹了一個基於STM32的俄羅斯方塊遊戲實現例子
整體方案的硬體部分由一個最小系統、按鍵開關模組以及2.2寸TFTLCD螢幕組成,軟體部分設計由繪圖、邏輯、整合控制三大部分組成,由一個二維繪圖函式繪製出遊戲畫面,並由碰撞判斷、狀態儲存等機制實現遊戲的正常執行。
需求:
開發一款基於STM32F103的遊戲機,能夠遊玩經典遊戲《TETRIS》
(1) 顯示:通過螢幕顯示遊戲UI等資訊供使用者進行遊玩;
(2) 控制:使用者可通過獨立按鍵進行操控;
(3) 使用者進行遊戲時會有分數記錄、難度等級等提示;
(4) 遊戲過程中可暫停遊戲;
指標:
(1) 能夠通過螢幕正常顯示遊戲資訊;
(2) 對按鍵的操作能及時響應;
(3) 遊戲結束時要顯示玩家得分;
(4) 遊戲過程可任意暫停;
總體方案:
五向按鍵模組
螢幕模組
STM32最小系統
二、軟體與演算法介紹
繪圖工具(顯示部分)
1、畫圓函式&畫點(線)函式
為實時顯示影像,我們需要一個可以在任意的指定座標畫點的函式,我們稱之為畫點函式Gui_DrawLine()和畫圓函式Gui_Circle()在這裡,我們使用螢幕供應商提供的畫點/園函式,它們基於Bresenham演算法構建而成。本質上,Bresenham是一種讓計算機實現高效的畫線的一種演算法。
下面我們用一張圖來舉例說明該演算法的基本思想:
假設該線段位於第一象限內且斜率大於0小於1,設起點為(x1,y1),終點為(x2,y2).根據對稱性,可推導至全象限內的線段。
第一步,畫起點(x1,y1);
第二步,準備畫下個點。x座標增1,判斷如果達到終點,則完成。否則,由圖中可知,下個要畫的點要麼為當前點的右鄰接點(B),要麼是當前點的右上鄰接點(U);
判斷:(以跟直線上點M的縱座標距離為依據選擇下一個點的位置)
(1) 如果線段ax+by+c=0與x=x1+1的交點的y座標大於M點的y座標的話,下個點為U(x1+1,y1+1);
(2) 否則,下個點為B(x1+1,y1);
簡單來說,就是判斷U、B跟直線ax+by+c=0與直線x=x1+1的交點M之間的距離遠近(通過兩點間距離公式),選取近的一個作為下一點並畫出,以此類推直到畫出整條直線。
第三步,畫點(U或者B);
第四步,跳回第2步;
結束。
細化的程式碼實現方式在此不做過多討論,網路上已經有很多種較為成熟的程式碼實現方式,下面給出我們使用的供應商提供的具體實現程式碼:
Gui_DrawLine函式
void Gui_DrawLine(u16 x0, u16 y0,u16 x1, u16 y1,u16 Color)
{
int dx, // difference in x's
dy, // difference in y's
dx2, // dx,dy * 2
dy2,
x_inc, // amount in pixel space to move during drawing
y_inc, // amount in pixel space to move during drawing
error, // the discriminant i.e. error i.e. decision variable
index; // used for looping
Lcd_SetXY(x0,y0);
dx = x1-x0;//計算x距離
dy = y1-y0;//計算y距離
if (dx>=0)
{
x_inc = 1;
}
else
{
x_inc = -1;
dx = -dx;
}
if (dy>=0)
{
y_inc = 1;
}
else
{
y_inc = -1;
dy = -dy;
}
dx2 = dx << 1;
dy2 = dy << 1;
if (dx > dy)//x距離大於y距離,那麼每個x軸上只有一個點,每個y軸上有若干個點
{//且線的點數等於x距離,以x軸遞增畫點
// initialize error term
error = dy2 - dx;
// draw the line
for (index=0; index <= dx; index++)//要畫的點數不會超過x距離
{
//畫點
Gui_DrawPoint(x0,y0,Color);
// test if error has overflowed
if (error >= 0) //是否需要增加y座標值
{
error-=dx2;
// move to next line
y0+=y_inc;//增加y座標值
} // end if error overflowed
// adjust the error term
error+=dy2;
// move to the next pixel
x0+=x_inc;//x座標值每次畫點後都遞增1
} // end for
} // end if |slope| <= 1
else//y軸大於x軸,則每個y軸上只有一個點,x軸若干個點
{//以y軸為遞增畫點
// initialize error term
error = dx2 - dy;
// draw the line
for (index=0; index <= dy; index++)
{
// set the pixel
Gui_DrawPoint(x0,y0,Color);
// test if error overflowed
if (error >= 0)
{
error-=dy2;
// move to next line
x0+=x_inc;
} // end if error overflowed
// adjust the error term
error+=dx2;
// move to the next pixel
y0+=y_inc;
} // end for
} // end else |slope| > 1
}
Gui_Circle函式
void Gui_Circle(u16 X,u16 Y,u16 R,u16 fc)
{
unsigned short a,b;
int c;
a=0;
b=R;
c=3-2*R;
while (a<b)
{
Gui_DrawPoint(X+a,Y+b,fc); // 7
Gui_DrawPoint(X-a,Y+b,fc); // 6
Gui_DrawPoint(X+a,Y-b,fc); // 2
Gui_DrawPoint(X-a,Y-b,fc); // 3
Gui_DrawPoint(X+b,Y+a,fc); // 8
Gui_DrawPoint(X-b,Y+a,fc); // 5
Gui_DrawPoint(X+b,Y-a,fc); // 1
Gui_DrawPoint(X-b,Y-a,fc); // 4
if(c<0) c=c+4*a+6;
else
{
c=c+4*(a-b)+10;
b-=1;
}
a+=1;
}
if (a==b)
{
Gui_DrawPoint(X+a,Y+b,fc);
Gui_DrawPoint(X+a,Y+b,fc);
Gui_DrawPoint(X+a,Y-b,fc);
Gui_DrawPoint(X-a,Y-b,fc);
Gui_DrawPoint(X+b,Y+a,fc);
Gui_DrawPoint(X-b,Y+a,fc);
Gui_DrawPoint(X+b,Y-a,fc);
Gui_DrawPoint(X-b,Y-a,fc);
}
}
2、方塊繪製相關函式
根據俄羅斯方塊的遊戲規則,每個方塊由4個小塊構成,一共有19種樣式如下圖
我們設定一個俄羅斯方塊中的一小塊大小為10*10,由此可由遍歷的方法得到繪製一小塊方塊的Draw_realbox()函式如下:
同樣的方法我們需要一個刪除方塊函式用於方塊的消除,即將10*10區域畫上白色即可。刪除方塊函式如下:
有了以上兩個函式,我們只需要在規定的座標處呼叫四次小方塊繪製或刪除函式
即可得到或消除一塊完整的俄羅斯方塊。而方塊有19種,故使用switch語句進行選擇需要何種方塊。圖形繪製函式如下(部分):
同理需要一個圖形刪除函式
3、遊戲引擎(邏輯部分)
狀態儲存機制
我們使用了一個大小為23*16的二位陣列來記錄方塊的位置,便於後續進行碰撞判斷、方塊消除等操作
陣列類似於一個螢幕,裡面為1的地方表示有小方塊存在,我們只需要改變陣列中的0、1即可實現對一個俄羅斯方塊的儲存,為此需要一個繪製和刪除函式,邏輯與在LCD繪製方塊類似,由Draw_a_zhuangtai()和Del_a_zhuangtai()實現
碰撞判斷
有了之前定義的陣列,我們可以使用求和的方式(類似前導零演算法)找出碰撞的方塊。因為在方塊沒發生碰撞之前,對該陣列求和的值為60(陣列邊界)+方塊佔的值(方塊快取)。當方塊發生碰撞,兩個方塊之間的交集會使得方塊佔的值變化(變小),與原值比較後可得出方塊是否碰撞,碰撞則返回一個值。特別的,為了判斷碰撞事件,在panduan()函式中我們還需要對方塊的方向進行判斷,因此panduan()函式還可在需要判斷方向時呼叫。
物體消除
由函式xiaochu()實現,每發生一次碰撞就檢測一次是否滿足消除條件
其中,消除函式的“消除”功能是由呼叫換行函式lie_move()和刪行函式Del_lie()實現的
形狀控制函式
即按指令繪製出規定形狀的俄羅斯方塊的函式,一共有兩個,change()函式用於繪製LCD上的,change_Zhuangtai()函式用於改變狀態陣列中的。
隨機數生成
在遊戲中,方塊需要隨機生成,所以我們需要一個隨機數作為方塊產生的依據,但僅用rand()函式生成的是偽隨機數,所以我們使用srand()函式打亂偽隨機數,同時引入ADC產生的末尾資料,以達到一個較高的隨機性。
如:
srand(Get_Adc(ADC_Channel_1));
what=rand()%19+1;//what代表著不同俄羅斯方塊
4、整合部分
方向控制
方塊可進行左、右、下的移動,因此我們需要在接到指令後再對應座標畫出完整的俄羅斯方塊並將原座標處的圖形消除,只需呼叫之前定義的圖形呼叫/刪除函式即可。值得注意的是,在移動影像的同時,我們也需要對狀態陣列中的資料進行相應的移動,為此我們需要一些整合後的函式如Down()、Left()、Right()、Del(),在此不做列舉說明。
向左移動函式:
向右移動函式:
向下移動函式:
封裝好上述方向移動功能函式後,我們只需呼叫它們即可實現相應方向的改變,呼叫函式如下:
再配合KEY_Scan()函式即可實現方向判斷,該函式流程圖如下:
分數、等級顯示
關於分數、等級等函式,只需通過當前分數、等級變數選擇相應數字在指定座標繪製即可(此處僅列舉display_leave()函式)
開始遊戲
通過begin()函式和first()函式實現。方塊的實際繪製是begin()函式完成的,此外該函式定義了方塊的初始重新整理位置並且對方塊是否觸頂進行判斷以提示遊戲結束。first()函式以負責清空上一次陣列儲存的狀態和基本UI的繪製,由於我們設定的邊界,UI部分在遊戲過程中不會受到重新整理影響,故只需要繪製一次。
暫停/恢復
當觸發暫停功能時,該函式在指定區域繪製暫停提示,修改標誌位game到暫停狀態並且清空定時器使能位實現暫停。
恢復遊戲執行是由star()函式實現的,當恢復時,該函式會把標誌位game改為執行狀態,並重繪部分UI。(受暫停提示介面的重新整理影響必須重繪,不然顯示會有缺失)流程圖如下:
定時器中斷函式
大約10μs中斷一次作為基礎下落的訊號,在主程式中若i大於speed則執行下落函式
主程式
在主函式中,對各項進行初始化、獲取隨機數,通過switch語句對獲取到的相應鍵值進行處理。