【補檔STM32】STM32F103俄羅斯方塊遊戲實現

項目地址://gitee.com/daycen/stm32-tetris/tree/master

使用Keil uVision5打開即可

一、概述

​ 本文介紹了一個基於STM32的俄羅斯方塊遊戲實現例子

​ 整體方案的硬件部分由一個最小系統、按鍵開關模塊以及2.2寸TFTLCD屏幕組成,軟件部分設計由繪圖、邏輯、整合控制三大部分組成,由一個二維繪圖函數繪製出遊戲畫面,並由碰撞判斷、狀態儲存等機制實現遊戲的正常運行。

需求:

開發一款基於STM32F103的遊戲機,能夠遊玩經典遊戲《TETRIS》

(1) 顯示:通過屏幕顯示遊戲UI等信息供用戶進行遊玩;

(2) 控制:用戶可通過獨立按鍵進行操控;

(3) 用戶進行遊戲時會有分數記錄、難度等級等提示;

(4) 遊戲過程中可暫停遊戲;

指標:

(1) 能夠通過屏幕正常顯示遊戲信息;

(2) 對按鍵的操作能及時響應;

(3) 遊戲結束時要顯示玩家得分;

(4) 遊戲過程可任意暫停;

總體方案:

img

img

img

五向按鍵模塊

img

顯示屏模塊

img

STM32最小系統

img

二、軟件與算法介紹

繪圖工具(顯示部分)

1、畫圓函數&畫點(線)函數

為實時顯示圖像,我們需要一個可以在任意的指定坐標畫點的函數,我們稱之為畫點函數Gui_DrawLine()和畫圓函數Gui_Circle()在這裡,我們使用屏幕供應商提供的畫點/園函數,它們基於Bresenham算法構建而成。本質上,Bresenham是一種讓計算機實現高效的畫線的一種算法。

下面我們用一張圖來舉例說明該算法的基本思想:

img

假設該線段位於第一象限內且斜率大於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之間的距離遠近(通過兩點間距離公式),選取近的一個作為下一點並畫出,以此類推直到畫出整條直線。

img

第三步,畫點(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種樣式如下圖

img

我們設置一個俄羅斯方塊中的一小塊大小為10*10,由此可由遍歷的方法得到繪製一小塊方塊的Draw_realbox()函數如下:

img

l1

同樣的方法我們需要一個刪除方塊函數用於方塊的消除,即將10*10區域畫上白色即可。刪除方塊函數如下:

img

l2

有了以上兩個函數,我們只需要在規定的坐標處調用四次小方塊繪製或刪除函數

即可得到或消除一塊完整的俄羅斯方塊。而方塊有19種,故使用switch語句進行選擇需要何種方塊。圖形繪製函數如下(部分):

img

同理需要一個圖形刪除函數

img

3、遊戲引擎(邏輯部分)

狀態儲存機制

我們使用了一個大小為23*16的二位數組來記錄方塊的位置,便於後續進行碰撞判斷、方塊消除等操作

img

數組類似於一個顯示屏,裏面為1的地方表示有小方塊存在,我們只需要改變數組中的0、1即可實現對一個俄羅斯方塊的保存,為此需要一個繪製和刪除函數,邏輯與在LCD繪製方塊類似,由Draw_a_zhuangtai()Del_a_zhuangtai()實現

碰撞判斷

有了之前定義的數組,我們可以使用求和的方式(類似前導零算法)找出碰撞的方塊。因為在方塊沒發生碰撞之前,對該數組求和的值為60(數組邊界)+方塊占的值(方塊緩存)。當方塊發生碰撞,兩個方塊之間的交集會使得方塊占的值變化(變小),與原值比較後可得出方塊是否碰撞,碰撞則返回一個值。特別的,為了判斷碰撞事件,在panduan()函數中我們還需要對方塊的方向進行判斷,因此panduan()函數還可在需要判斷方向時調用。

img

img

物體消除

由函數xiaochu()實現,每發生一次碰撞就檢測一次是否滿足消除條件

img

其中,消除函數的「消除」功能是由調用換行函數lie_move()和刪行函數Del_lie()實現的

形狀控制函數

即按指令繪製出規定形狀的俄羅斯方塊的函數,一共有兩個,change()函數用於繪製LCD上的,change_Zhuangtai()函數用於改變狀態數組中的。

img

img

隨機數生成

在遊戲中,方塊需要隨機生成,所以我們需要一個隨機數作為方塊產生的依據,但僅用rand()函數生成的是偽隨機數,所以我們使用srand()函數打亂偽隨機數,同時引入ADC產生的末尾數據,以達到一個較高的隨機性。

如:

srand(Get_Adc(ADC_Channel_1));

what=rand()%19+1;//what代表着不同俄羅斯方塊

4、整合部分

方向控制

方塊可進行左、右、下的移動,因此我們需要在接到指令後再對應坐標畫出完整的俄羅斯方塊並將原坐標處的圖形消除,只需調用之前定義的圖形調用/刪除函數即可。值得注意的是,在移動圖像的同時,我們也需要對狀態數組中的數據進行相應的移動,為此我們需要一些整合後的函數如Down()Left()Right()Del(),在此不做列舉說明。

向左移動函數:

img

向右移動函數:

img

向下移動函數:

img

封裝好上述方向移動功能函數後,我們只需調用它們即可實現相應方向的改變,調用函數如下:

img

再配合KEY_Scan()函數即可實現方向判斷,該函數流程圖如下:

img

分數、等級顯示

關於分數、等級等函數,只需通過當前分數、等級變量選擇相應數字在指定坐標繪製即可(此處僅列舉display_leave()函數)
img

開始遊戲

通過begin()函數first()函數實現。方塊的實際繪製是begin()函數完成的,此外該函數定義了方塊的初始刷新位置並且對方塊是否觸頂進行判斷以提示遊戲結束。first()函數以負責清空上一次數組保存的狀態和基本UI的繪製,由於我們設置的邊界,UI部分在遊戲過程中不會受到刷新影響,故只需要繪製一次。

img

l4

img

l3

暫停/恢復

當觸發暫停功能時,該函數在指定區域繪製暫停提示,修改標誌位game到暫停狀態並且清空定時器使能位實現暫停。

img

恢復遊戲運行是由star()函數實現的,當恢復時,該函數會把標誌位game改為運行狀態,並重繪部分UI。(受暫停提示界面的刷新影響必須重繪,不然顯示會有缺失)流程圖如下:

img

定時器中斷函數

大約10μs中斷一次作為基礎下落的信號,在主程序中若i大於speed則執行下落函數

img

主程序

在主函數中,對各項進行初始化、獲取隨機數,通過switch語句對獲取到的相應鍵值進行處理。

img

三、實物圖

img

img

img

img