Excel+VBA製作小遊戲:俄羅斯方塊
接下來說回怎麼用VBA在Excel里實現這個俄羅斯方塊,我用了一個類模塊來負責方塊的對象,一個模塊負責操作的方法。首先還是準備界面!
一、準備界面
這次實現的俄羅斯方塊在功能細節上比之前幾個都要豐富,包括了積分、行數、速度等級、排名、下一個方塊等小功能的實現。所以整體界面分兩大部分:
1、固定界面
在遊戲中不會變動的部分,我們可以先準備好。我們先把A~P16列的列寬設置為2,把整體界面模仿成近似正方形小方塊的像素格。然後需要一個10*20的遊戲界面,把A1:J20塗白,粗外側框線。
然後L2:O2合併後居中,做得分;L3:O3合併後居中,顯示得分0;底色塗白,整體加上所有框線。行數、等級、積分榜等內容如法炮製,在積分榜下面有一點小不同。積分榜內容,左邊輸入1、2、3,右邊每行合併單元格。
最下方L17:O20這個4*4的塗白加邊框,當作下一個方塊的提示區。做成像題圖那樣就差不多了。
2、變化界面
主遊戲界面在遊戲中是一直在變化的,這個我們需要用方法來完成。得分、行數、等級,以及提示區域也是在遊戲中不斷變化的,排行榜是遊戲結束後更新的。我們先來畫一下遊戲開始和結束的界面,新增一個模塊命名為Tetris,加入代碼如下:
Private Sub UI_Init()nApplication.ScreenUpdating = FalsennWith Sheet1.Range("A1:J20")n .Clearn .Interior.colorIndex = 2n .Font.Size = 10n .BorderAround Weight:=xlMediumn .Borders(xlEdgeTop).LineStyle = xlNonenEnd WithnnSheet1.Range("L3") = 0nSheet1.Range("L6") = 0nSheet1.Range("L9") = 1nnWith Sheet1.Range("L17:O20")n .Clearn .Interior.colorIndex = 2n .BorderAround Weight:=xlThinnEnd WithnnSheet1.Range("K21").SelectnnApplication.ScreenUpdating = TruenEnd SubnnPrivate Sub Over_Init()nApplication.ScreenUpdating = FalsennWith Sheet1.Range("C8:H11")n .Mergen .Value = "遊戲結束!"n .HorizontalAlignment = xlCentern .VerticalAlignment = xlCentern n .BorderAround Weight:=xlMediumn .Interior.colorIndex = 2n .Font.colorIndex = 1n .Font.Bold = Truen .Font.Size = 16nEnd WithnnApplication.ScreenUpdating = TruenEnd Subn
遊戲開始的時候我們重置了主界面、分數區域和提示區域,遊戲結束的時候畫了一個遊戲結束的提示框。養成好習慣,在畫界面前關閉屏幕更新,畫完之後再打開,頁面才不會有刷新閃爍的感覺。
二、方塊對象
1、初始化
新建一個類模塊,命名為Block,需要的一些公共變數如下:
公共變數nPublic typeArr As Variant 類型數組nPublic colorArr As Variant 顏色數組nPublic shapeArr As Variant 坐標數組nPublic rotatedShape As Variant 旋轉後坐標nPublic dirs As Variant 移動坐標nn常量nConst PI As Double = 3.14nn成員nPublic x As Integer 橫坐標/行數nPublic y As Integer 縱坐標/列數nPublic color As Integer 顏色nPublic status As Integer 狀態nPublic typeName As String 方塊類型nPublic typeIndex As Integer 類型索引n
在對象創建時,我們需要用默認初始化方法給類型、顏色、坐標數組、移動坐標賦值。
Private Sub Class_Initialize()nntypeArr = Array("I", "J", "L", "O", "S", "Z", "T")ncolorArr = Array(8, 5, 44, 6, 4, 7, 3)nshapeArr = Array(Array(Array(0, 0), Array(0, 1), Array(0, 2), Array(0, -1)), _n Array(Array(0, 0), Array(0, 1), Array(0, -1), Array(-1, -1)), _n Array(Array(0, 0), Array(0, 1), Array(0, -1), Array(-1, 1)), _n Array(Array(0, 0), Array(-1, 1), Array(-1, 0), Array(0, 1)), _n Array(Array(0, 0), Array(0, -1), Array(-1, 0), Array(-1, 1)), _n Array(Array(0, 0), Array(0, 1), Array(-1, 0), Array(-1, -1)), _n Array(Array(0, 0), Array(0, 1), Array(0, -1), Array(-1, 0)))ndirs = Array(Array(0, 1), Array(1, 0), Array(0, -1))nnEnd Subn
根據官方說法,俄羅斯方塊有7個類型的方塊,分別用字母I、J、L、O、S、Z、T表示;他們的顏色我隨便對照了某個俄羅斯方塊,從VBA自帶的顏色索引中找了相似的;他們的坐標,指的是以旋轉中心為原點的相對坐標。我們的坐標是根據旋轉中心來確定的,而旋轉中心,又是根據俄羅斯的旋轉系統來確定的。
俄羅斯方塊的基本元素——方塊,是由4個正方形小方塊組成。除了O型方塊,其他方塊都能旋轉。俄羅斯方塊旋轉系統的標準很多,以下內容來自俄羅斯方塊吧:
這個標準是很多的。
官網上規定的Guideline是Super Rotation System
即超級選轉系統的中心如下:(以上圖片來自於Harddrop)Arika Rotation System本旋轉系統被Arika的俄羅斯方塊大師系列(TGM)所用。
這裡為了方便實現,我把兩種規則綜合了下,用了一種比較容易實現的規則。這裡允許嘲笑我三秒鐘~我就是這麼的懶惰!
由於需要使方塊旋轉,我們在創建一個方塊對象的時候必須要知道的變數有四個:方塊旋轉中心的橫、縱坐標,方塊的類型,方塊的當前旋轉狀態。所以,在手動初始化時,我們需要傳入4個變數,如下:
Public Sub Init(ByVal x As Integer, ByVal y As Integer, ByVal typeName As String, ByVal status As Integer)nnFor i = 0 To 6n If typeName = typeArr(i) Then Me.typeIndex = i: Exit FornNext innMe.x = xnMe.y = ynMe.typeName = typeNamenMe.status = statusnnMe.color = Me.colorArr(typeIndex)nCall Me.Shape_Rotate(Me.status)nnEnd Subn
我們首先根據類型的名稱,從類型數組中匹配出對應的索引值(這裡為什麼不直接傳入索引數字,我們之後再說)。將4個變數賦值後,由顏色數組讀取出相應的顏色。因為我們的坐標數組中存放的坐標,都是方塊初始狀態坐標,所以需要再通過一個方塊旋轉方法,使方塊旋轉到當前的狀態。
2、方塊旋轉
旋轉的方法如下:
Public Sub Shape_Rotate(ByVal toStatus As Integer)nnDim tempArrntempArr = Me.shapeArr(Me.typeIndex)nnFor j = 1 To toStatusn For i = 0 To 3n point_x = tempArr(i)(0)n point_y = tempArr(i)(1)n tempArr(i)(0) = CInt(point_x * Math.Cos(PI / 2) - point_y * Math.Sin(PI / 2))n tempArr(i)(1) = CInt(point_x * Math.Sin(PI / 2) + point_y * Math.Cos(PI / 2))n Next inNext jnnMe.rotatedShape = tempArrnnEnd Subn
這裡對方塊狀態(status變數)的規定做一下說明,變數從0到3代表了4種不同的旋轉狀態,每一種都是前一狀態逆時針旋轉90度的結果。所以我們針對當前需要到達的狀態toStatus做一個從1開始的循環,如果當前狀態是0則循環不執行。
每次循環都把單個方塊的橫、縱坐標拿出來,放到變數point_x、point_y中,通過數學方法計算出旋轉後的坐標值,再將這個值放回去。我們這裡用了一個臨時數組tempArr來進行計算,以免干擾原始數據。計算時候的臨時數組再賦值給造就準備好的旋轉後坐標數組rotatedShape。
3、方塊輸出
變成當前狀態後,我們需要把方塊畫到遊戲界面中。Block_Draw的實現代碼如下:
Public Sub Block_Draw(Optional ByVal s As Integer = 0)nnDim colorIndex%, linencolorIndex = Me.colornline = xlContinuousnIf s = 1 Then colorIndex% = 2: line = xlLineStyleNonennSheet1.Cells(Me.x + Me.rotatedShape(0)(0), Me.y + Me.rotatedShape(0)(1)).Font.colorIndex = colorIndex%nFor i = 0 To 3n Sheet1.Cells(Me.x + Me.rotatedShape(i)(0), Me.y + Me.rotatedShape(i)(1)).Interior.colorIndex = colorIndex%n Sheet1.Cells(Me.x + Me.rotatedShape(i)(0), Me.y + Me.rotatedShape(i)(1)).Borders.LineStyle = linenNext innSheet1.Range("A1:J20").BorderAround Weight:=xlMediumnSheet1.Range("A1:J20").Borders(xlEdgeTop).LineStyle = xlNonenEnd Subn
這裡有個可選參數s,用來區分我們是來畫這個方塊,還是把這個方塊擦掉。因為方塊變化(包括移動和旋轉)的畫面,都是通過擦除原有方塊再畫一個新方塊來實現的。而所謂擦除方塊,就是把方塊塗上白色去掉邊框,和畫方塊時的塗色加邊框動作本質相同。
我們規定方塊的顏色來自Me.color,邊框線默認為xlContinuous,如果s參數傳入1則把顏色修改為白色,邊框修改為無(xlLineStyleNone)。然後通過循環,給每一個小方塊確定顏色和邊框。這裡有一個細節,在循環前將旋轉中心的字體顏色改成方塊顏色,這是為什麼?最後畫的是整個頁面的大邊框,以及去掉頂上的邊框線,為什麼要去掉?可以先思考下,後面會說明。
4、移動與判定
方塊的基本方法已經介紹完了,接下來是方塊的2個主要動作,移動和旋轉。方塊一共有3個移動方向右、下、左,我之前做的很多小遊戲都用到了這個方向的處理。移動的代碼很簡單:
Public Sub Block_Move(ByVal dir As Integer)nnMe.Block_Draw (1)nMe.x = Me.x + Me.dirs(dir)(0)nMe.y = Me.y + Me.dirs(dir)(1)nMe.Block_DrawnnEnd Subn
想讀取一個dir變數(direction的縮寫,原諒我的懶惰),我們規定0、1、2分別代表右、下、左,根據這個規定定義數組dirs的內容。然後按照上面說的處理方式,先把原來的方塊擦除,再讓坐標改變,然後畫出新的方塊。
移動沒什麼技術含量,可移動的判定才是較難的內容。接下來注意了,我要變形了!
這部分我們先來分析規則,俄羅斯方塊的碰撞判定是:方塊的任意部分與其他方塊或邊界接觸。碰撞後即不可再向該方向移動,如果產生向下的碰撞則方塊停止下沉。所以,要對每一個小方塊進行移動判定。我們只需要看方塊移動後的位置是否超越邊界或已存在方塊(顏色)即可,但是問題來了!方塊自身要被排除在外。
也就是說,我們要找出移動之後與移動前不重合的方塊,判斷這個方塊是否超越邊界或者已存在顏色,如果是則為不可移動。
Public Function Block_Move_Possible(ByVal dir As Integer) As BooleannBlock_Move_Possible = TruennDim count%nnFor i = 0 To 3n If Me.x + Me.rotatedShape(i)(0) + Me.dirs(dir)(0) > 20 Or Me.y + Me.rotatedShape(i)(1) + Me.dirs(dir)(1) < 1 Or Me.y + Me.rotatedShape(i)(1) + Me.dirs(dir)(1) > 10 Thenn Block_Move_Possible = Falsen Exit Functionn Elsen count = 0n For j = 0 To 3n If Me.rotatedShape(i)(0) + Me.dirs(dir)(0) <> Me.rotatedShape(j)(0) Or Me.rotatedShape(i)(1) + Me.dirs(dir)(1) <> Me.rotatedShape(j)(1) Then count = count + 1n Next jn If count = 4 Thenn If Sheet1.Cells(Me.x + Me.rotatedShape(i)(0) + Me.dirs(dir)(0), Me.y + Me.rotatedShape(i)(1) + Me.dirs(dir)(1)).Interior.colorIndex <> 2 Then Block_Move_Possible = False: Exit Functionn End Ifn End IfnNext innEnd Functionn
5、旋轉與判定
旋轉和移動的性質差不多,在初中數學有三大變化:抽煙、喝酒、燙頭,不對!劃掉重來,平移、對稱、旋轉,都是初等幾何變換。旋轉判定也和移動判定相似,把平移計算變為旋轉計算即可,代碼如下:
Public Sub Block_Rotate()nnCall Shape_Rotate(Me.status)nMe.Block_Draw (1)ntoStatus = (Me.status + 1) Mod 4nCall Shape_Rotate(toStatus)nMe.Block_DrawnMe.status = toStatusnnEnd SubnnPublic Function Block_Rotate_Possible() As BooleannBlock_Rotate_Possible = TruennIf Me.typeName = "O" Then Block_Rotate_Possible = False: Exit FunctionnnbeforRotate = Me.rotatedShapentoStatus = (Me.status + 1) Mod 4nCall Shape_Rotate(toStatus)nnFor i = 0 To 3n If Me.x + Me.rotatedShape(i)(0) > 20 Or Me.x + Me.rotatedShape(i)(0) < 1 Or Me.y + Me.rotatedShape(i)(1) < 1 Or Me.y + Me.rotatedShape(i)(1) > 10 Thenn Block_Rotate_Possible = Falsen Exit Functionn Elsen count = 0n For j = 0 To 3n If Me.rotatedShape(i)(0) <> beforRotate(j)(0) Or Me.rotatedShape(i)(1) <> beforRotate(j)(1) Then count = count + 1n Next jn If count = 4 Thenn If Sheet1.Cells(Me.x + Me.rotatedShape(i)(0), Me.y + Me.rotatedShape(i)(1)).Interior.colorIndex <> 2 Then Block_Rotate_Possible = False: Exit Functionn End Ifn End IfnNext innEnd Functionn
Block對象創建完畢,接下來就是對程序的主邏輯實現。
三、主程序
1、遊戲開始
遊戲流程是這樣設置的:
- 遊戲開始初始化主界面;
- 隨機放一個方塊在主界面,
- 隨機放一個方塊在提示區;
- 判斷遊戲是否可以繼續,是則進入自動下沉,否則結束;
- 下沉結束,將提示區的方塊放到主界面,重複進行3
代碼如下:
Public Sub Game_Start()nnCall UI_InitnnDim typeName$ntypeArr = Array("I", "J", "L", "O", "S", "Z", "T")nRandomize (Timer)ntypeName = typeArr(Int(7 * Rnd))nnDon 隨機方塊放入主界面n Sheet1.Cells(2, 5) = typeName & "0"n Dim mainBlock As New Blockn Call mainBlock.Init(2, 5, typeName, 0)n mainBlock.Block_Drawnn 隨機方塊放入提示區n typeName = typeArr(Int(7 * Rnd))n Dim hintBlock As New Blockn Call hintBlock.Init(19, 13, typeName, 0)n hintBlock.Block_Drawn n If Game_Over Then Exit Don If mainBlock.Block_Move_Possible(1) = False Then Exit Don n Call Downn Call hintBlock.Block_Draw(1)n Sheet1.Range("L17:O20").BorderAround Weight:=xlThinn nLoop While (True)nnCall Over_InitnCall List_UpdatennEnd Subn
隨機方塊放入主界面,我們看到是在旋轉中心單元格寫如typeName和0,比如新生成一個S塊就是S0。這就是之前提到的,為什麼要用字母而不用索引來代替方塊類型。一是沒有字母更加直觀,二是兩個數字寫入第一個0會被省略,如果以文本形式寫入單元格會有討厭的綠色小角標。同樣解釋了,為什麼旋轉中心單元格的字體顏色要和背景色一致,這是用來隱藏內容的!
這裡的UI_Init和Over_Init方法之前都已經做過了,Game_Over是對遊戲是否結束的判定,List_Update是對排行榜的更新。遊戲結束的判定,其實就是剛放入的方塊是否可以向下移動的判定,粗糙一點的話可以直接用mainBlock.Block_Move_Possible (1) = False來代替。如果要詳細一點,還要做是否可以放入的判定,顯示上處理起來就更麻煩了。這裡姑且先簡化處理吧!
排行榜更新代碼如下:
Private Sub List_Update()nnDim score%, listArrnscore = Sheet1.Range("L6")nlistArr = Sheet1.Range("M12:M14")nnIf score > listArr(3, 1) Then listArr(3, 1) = scorenFor i = 3 To 2 Step -1n If listArr(i, 1) > listArr(i - 1, 1) Thenn temp = listArr(i - 1, 1)n listArr(i - 1, 1) = listArr(i, 1)n listArr(i, 1) = tempn End IfnNext innSheet1.Range("M12:M14") = listArrnnEnd Subn
排行榜更新的邏輯是,當前分數大於排行榜最小值時就代替第三名記入榜內,然後按大小排序。這裡的排序可以用Excel自帶的排序功能實現,因為數量很少,又是對數組的處理,所以自己寫了個冒泡。懶得查Excel排序的VBA代碼了……
2、方塊下沉
方塊下沉就是執行一段延時的循環,延遲的時間又速度等級決定。這一段的流程是這樣的:
- 獲取當前速度等級;
- 獲取當前方塊位置、狀態;
- 判斷方塊是否可以下沉,是則繼續,否則跳出循環執行7;
- 方塊自動下沉一格;
- 開啟鍵盤事件,進行延時處理,關閉鍵盤事件;
- 重複執行2;
- 清空方塊狀態,執行消行方法;
代碼如下:
Private Sub Down()n獲取當前速度等級nlevel = Sheet1.Range("L9")nnDon 獲取當前方塊位置狀態n Dim coordinaten coordinate = Block_Centern If coordinate(0) = 20 Then Exit Don n Dim x%, y%, typeName$, status%n x = coordinate(0)n y = coordinate(1)n typeName = coordinate(2)n status = coordinate(3)n n Application.ScreenUpdating = Falsen Dim b As New Blockn Call b.Init(x, y, typeName, status)n n 判斷方塊是否可以下沉n If b.Block_Move_Possible(1) = False Then Exit Don 方塊下沉一格n b.Block_Move (1)n Sheet1.Cells(x, y) = ""n Sheet1.Cells(x + 1, y) = typeName & statusn Application.ScreenUpdating = Truen n 進入延時處理n Application.EnableEvents = Truen Sleep (1000 / level)n Application.EnableEvents = FalsenLoop While (True)nnApplication.ScreenUpdating = TruenApplication.EnableEvents = Falsen清空方塊狀態nSheet1.Cells(x, y) = ""nCall Line_ClearnnEnd Subn
這裡獲取當前方塊位置的方法,實現如下:
Private Function Block_Center() As Variantnnarr = Sheet1.UsedRangenFor i = 1 To 20n For j = 1 To 10n If IsEmpty(arr(i, j)) = False Then GoTo Exit_Forn Next jnNext innExit_For:nDim typeName$, status%ntypeName = Left(arr(i, j), 1)nstatus = CInt(Right(arr(i, j), 1))nBlock_Center = Array(i, j, typeName, status)nnEnd Functionn
簡而言之,就是對遊戲主界面區域進行遍歷,找到第一個非空單元格,把行列序號和內容通過數組的方式返回。
這裡的延時方法Sleep也是自己寫的,要用的API是GetTickCount,這是我覺得一個比較高效的延時方法。先在頭部引用API:
#If VBA7 And Win64 Thenn Private Declare PtrSafe Function GetTickCount Lib "kernel32" () As Longn#Elsen Private Declare Function GetTickCount Lib "kernel32" () As Longn#End Ifn
然後創建方法:
Private Sub Sleep(numa As Double)n延時方法nDim num1 As DoublenDim num2 As DoublenDim numb As Doublennnumb = 0nnum1 = GetTickCountnnDo While numa - numb > 0n num2 = GetTickCountn numb = num2 - num1n DoEventsnLoopnnEnd Subn
最後的Line_Clear清行方法,需要實現清空可消除的行,計入行數、分數,判斷等級等多個內容。代碼如下:
Private Sub Line_Clear()nApplication.ScreenUpdating = FalsennDim rowCount%nFor i = 20 To 1 Step -1n For j = 1 To 10n If Sheet1.Cells(i, j).Interior.colorIndex = 2 Then Exit Forn Next jn If j > 10 Thenn Sheet1.Range(Cells(i, 1), Cells(i, 10)).Interior.colorIndex = 2n Sheet1.Range(Cells(1, 1), Cells(i - 1, 10)).Copyn Sheet1.Range(Cells(2, 1), Cells(i, 10)).PasteSpecialn Sheet1.Range(Cells(1, 1), Cells(1, 10)).Interior.colorIndex = 2n rowCount = rowCount + 1n i = i + 1n End IfnNext innIf rowCount > 0 Thenn Sheet1.Range("K21").Selectn n Dim score%, lines%, level%n score = Sheet1.Range("L3")n lines = Sheet1.Range("L6")n level = Sheet1.Range("L9")n n Select Case rowCountn Case 1n score = score + 1n Case 2n score = score + 2n Case 3n score = score + 5n Case 4n score = score + 10n End Selectn lines = lines + rowCountn If lines >= level * 30 Then level = level + 1n n Sheet1.Range("L3") = scoren Sheet1.Range("L6") = linesn Sheet1.Range("L9") = levelnEnd IfnnApplication.ScreenUpdating = TruenEnd Subn
實現原理很簡單,從下往上遍歷遊戲區域,如果行內單元格出現白色則跳出,整行都沒有白色則進行消除。消除之後還需要將該行以上的所有遊戲區域都下沉,簡單的複製粘貼就可以了。這就是之前提到的,為什麼要將頂部的框線去掉,因為複製粘貼的時候會出現……這裡利用了Excel的首行頂部框線本來就是不顯示的,算是小小的偷個懶吧……
3、鍵盤動作
鍵盤動作還是老樣子,我基本複製的以前的代碼,以下代碼放在Sheet1的代碼頁中:
#If VBA7 And Win64 Thenn Private Declare PtrSafe Function GetKeyboardState Lib "user32" (pbKeyState As Byte) As Longn#Elsen Private Declare Function GetKeyboardState Lib "user32" (pbKeyState As Byte) As Longn#End IfnPrivate Sub Worksheet_SelectionChange(ByVal Target As Range)nApplication.EnableEvents = FalsennDim keycode(0 To 255) As BytenGetKeyboardState keycode(0)nnDim status%nIf keycode(38) > 127 Then Call Block_Rotate 上nIf keycode(39) > 127 Then Call Block_Move(0) 右nIf keycode(40) > 127 Then Call Block_Move(1) 下nIf keycode(37) > 127 Then Call Block_Move(2) 左nnSheet1.Range("K21").SelectnnApplication.EnableEvents = TruenEnd Subn
這裡涉及到的Block_Rotate和Block_Move都不是類模塊中的類方法,而是在Tetris模塊中的公共方法。
Public Sub Block_Move(ByVal dir As Integer)nApplication.ScreenUpdating = FalsennnDim coordinate, dirsncoordinate = Block_Centerndirs = Array(Array(0, 1), Array(1, 0), Array(0, -1))nnDim x%, y%, typeName$, status%nx = coordinate(0)ny = coordinate(1)ntypeName = coordinate(2)nstatus = coordinate(3)nnDim b As New BlocknCall b.Init(x, y, typeName, status)nIf b.Block_Move_Possible(dir) = False Then Exit SubnCall b.Block_Move(dir)nnSheet1.Cells(x, y) = ""nSheet1.Cells(x + dirs(dir)(0), y + dirs(dir)(1)) = typeName & statusnnApplication.ScreenUpdating = TruenEnd SubnnPublic Sub Block_Rotate()nnDim coordinatencoordinate = Block_CenternnDim x%, y%, typeName$, status%nx = coordinate(0)ny = coordinate(1)ntypeName = coordinate(2)nstatus = coordinate(3)nnDim b As New BlocknCall b.Init(x, y, typeName, status)nIf b.Block_Rotate_Possible = False Then Exit SubnCall b.Block_Rotatennstatus = (status + 1) Mod 4nSheet1.Cells(x, y) = typeName & statusnnEnd Subn
其實還是用類方法實現移動,只是需要在前後加一些規則,讀取/修改方塊的位置、狀態。
以上,VBA實現俄羅斯方塊的代碼就完成了。文章已經很長了,完成代碼就不放了吧~
文件下載:微雲文件-俄羅斯方塊-黃晨製作
推薦閱讀:
※百萬次實驗告訴你,堅持到底不一定勝利!
※Excel+VBA製作小遊戲:翻卡牌
※九九乘法表?三行就夠了!
※vba自動生成PPT報告?
※Excel VBA進階怎麼學,感覺市面上的書都是入門型的?
TAG:VBA | Excel编程 | 俄罗斯方块Tetris |