俄羅斯方塊

   
 
 
  
  小時候,天天放學回到家,急衝衝寫完家庭做業,而後我就搬出遊戲機,開始打遊戲。對於 80 後而言,「 遊戲機 」 這三個字是一個不言自明的符號,那就是經典的任天堂紅白機,還有一盤盤能夠表明幸福指數的遊戲卡。當年誰家的小朋友擁有的遊戲卡多,誰就是當時的紅人,尤爲是黑卡,簡直是高端的象徵。
 
 
  得益於父母開明,打遊戲這事我沒受過限制,又得益於我有個偉大的表哥,因此個人幸福指數也歷來都是在高位震盪。經典的《魂鬥羅》《坦克大戰》《綠色兵團》《赤色要塞》《熱血系列》等,冷門的《懲罰者》《聖鬥士》《電梯》等等,包括後來玩的文卡《三國志》,雖不敢說天下游戲玩了個遍,但也是見過世面的資深玩家了。
  父親雖然不限制我,卻常會嘮叨「做業作完了就多看看課外書,少玩遊戲。打遊戲浪費時間。」 常常對着我正在玩的《熱血足球》說:「你要踢球就下樓去踢,操做幾個娃娃人在電視上踢有什麼意思?並且你這都是些什麼亂七八糟的,還有星星?!」(玩過熱血足球的應該都知道此刻屏幕上「小辮子」正在放大招)。
  可是,父親有時候也會和我一塊兒玩,很投入地玩,並且永遠只玩一個遊戲——《俄羅斯方塊》。
 
 
  無數個週末下午,我和父親就是在對戰俄羅斯方塊中度過的。
  這款遊戲,對於我而言,有着很深入的印象,它承載着童年,也承載着父子之情。
  
 
  《俄羅斯方塊》(Tetris) 誕生於 1984 年 6 月 6 日。當時在前蘇聯科學院電算中心工做的科學家 阿列克謝-帕基特諾夫 利用空閒時間設計和編寫了這個落下型益智遊戲的始祖。按理說,這款遊戲其實應該叫作 「前蘇聯方塊」。
  這款冷戰時期的產物,成了第一個進入美國的蘇聯遊戲。後來還被評爲「最偉大的100個遊戲」中的第1名、Gome Boy 史上最受歡迎的遊戲、有史以來最暢銷的電子遊戲,榮譽太多了。
 
  當年初學 C# 時,以爲用 Winform 作點小玩意很快樂,曾想本身作一個俄羅斯方塊。但一想到「方塊旋轉」,「碰撞檢測」,「GDI+ 動畫」等等具體問題,就一頭霧水,感受難度太大,因而就放棄了。
  一放就是好多年。
  前不久,突然有一天,也說不上來什麼緣由,又想到了俄羅斯方塊,又萌生了本身動手寫一個的念頭。可能這就是所謂的童年的影響所帶來的執念,不論過多久,總有個牽掛在內心。
  念頭來得容易,但問題依然存在,仍是得想辦法解決旋轉、碰撞、動畫等實際問題。
  鑑於個人早年經歷,用 C# 來作這事的話,首先就想到 GDI+ ,可我之前學 GDI+ 時就沒有學好,這麼多年過去了,更不會了。我也不從事遊戲開發領域,對於動畫,尤爲是處理圖形變換、碰撞檢測這樣的概念,缺少相關知識儲備。
  再一次感到一籌莫展。
  因而我遊離了。
  倒也沒遊多遠,在 Wikipedia 上查閱「俄羅斯方塊」這個詞條,想了解一下游戲的背景。意外地發現了一個有趣的知識:俄羅斯方塊的英文名叫作 Tetris, 這個名稱來自於希臘數字表示 4 的前綴 "tetra" 和網球 「tennis」 ——兩個詞組合而成。之因此用數字 4 的緣由是遊戲中的方塊元素都是由4個方格組成,而網球則是做者最喜歡的運動。
  雖然玩了無數遍,但看到這條信息時我才意識到,原來遊戲中的那些方塊均可以分解成4個小方格組成。
  而這個意外的收穫給了我一個啓發:既然每一個方塊能夠方格化,那麼擴展一下,整個遊戲的構成也能夠網格化。
  瞬間,事情出現了起色。
    
  俄羅斯方塊遊戲的視覺呈現能夠抽象爲兩部分:背景空間方塊
  將背景空間當作一張表格,方塊能夠當作是表格中單元格的組合
  因而,遊戲中任意一個時刻的靜態畫面,能夠抽象成:
  一張表格中,部分單元格有方塊,部分單元格無方塊,且每個單元格只可能處於兩種狀態中的一種。
  而遊戲的過程就是將全部時刻的靜態畫面按時間順序「一幀幀」呈現。 也就是說,全部的視覺效果均可以映射爲屢次更新一張表格中的不一樣單元格的狀態。    
  如此一來,以前困擾個人——
  「圖形旋轉」就演化成了修改單元格的值。
  「碰撞檢測」就只須要判斷單元格的行列位置。
  至於「GDI+繪圖」,就變成了給單元格着色。    
 
  時間上,將動態過程切割成離散的靜態畫面;空間上,將連續畫面切割成離散的網格。 解決連續動態的問題頗費周章,但處理一張表格就簡單多了。所謂思路決定出路,大概就是這樣吧。
 
  眼前的障礙消除了,這事感受能夠作下去了。
  動手以前,先作計劃。完成這個任務,大體可分解成如下環節:
  
  1.   設定網格區域
  2.   方塊的生成
  3.   方塊的移動
  4.   方塊的旋轉
  5.   方塊的消除
  6.   GameOver的判斷
  7.   計分
 
 
  One thing at a time. —— Mark Watney
 
  在開始以前,約定一下思路 —— 遊戲界面由表格提供,表格內的單元格分爲3種狀態:
 
  •   空白,值爲0
  •   下落中,值爲1
  •   已落定,值爲2
  對於下落中的方塊,顯示爲方塊原本的顏色;對於已落定的方塊,顯示爲當前遊戲級別對應的顏色(當年紅白機上的版本就是這麼設定的)。
  因此,遊戲畫面的網格化抽象效果以下圖所示:
    
 
  參考代碼:
  狀態常量設定  
 
 
  表格渲染  
 
    
  有了上面這樣一個思路,下面就一步步實現具體的遊戲邏輯。
 
  首先,解決基礎設施的問題——網格
 
  C#是少有的支持純粹二維數組的語言,但說到表格,沒有哪一個對象比 DataTable 更方便了,尤爲是配合 UI 組件 DataGridView。
  我記得紅白機上的俄羅斯方塊,遊戲的規格是寬10列,高25行(小時候數過,凡是小時候記過的事,好像就不會忘)。因此,最基礎的設施就是一個包含 10 列, 25 行的DataTable. 以後全部的事情都圍繞這個DataTable 來操做了。
 
  定義一個產生 DataTable,同時將內部單元格的值初始化爲 0 的方法:
 
 
 
  調用該方法產生一個 DataTable 變量,命名爲 matrix,這就是整個遊戲的基底
 
 
 
  其中,Game.MATRIX_HEIGHT 和 Game.MATRIX_WIDTH 是常量,分別爲 25 和 20,表明着表格的尺寸。
 
  如今,咱們有了一個寬25列,高25行,內容所有都是數字 "0" 的表格了。
  接下來是核心部件——方塊
    
  方塊的生成
 
  遊戲中須要不停地產生新的方塊,每塊一開始都會出如今頂部,這就須要有一個隨機生成方法。
  但在處理這個邏輯以前,首先要明確方塊的構成。
  前面提到,每一個方塊由 4 個方格組成。因此,能夠經過一個包含 4 個元素的單元格數組來表示一個方塊,每一個單元格包含所在行列的位置索引,經過 4 組行列座標能夠肯定一個方塊在網格中的所佔區域。額外的,每一個方塊有本身的顏色,至關於每一個單元格數組中的元素擁有統一的背景色屬性。
  俄羅斯方塊一共有 7 種,一般根據其形狀,分別稱做: I, J, L, S, Z, O, T. 
  利用OOP的思想,咱們能夠先定義一個抽象類 Tetromino(Tetromino這個詞的意思是「四連正方形」)。該抽象類包含 2 個只讀的抽象屬性:背景色、初始座標,1個抽象方法:旋轉。其中,初始座標這個抽象屬性定義爲 Cell 類型的數組。Cell 是一個自定義類,包含 2 個 int 類型的屬性,分別存儲行、列索引。
 
  類關係圖以下:
 
    
  Tetromino 定義以下:  
 
 
  Cell 定義以下:
 
 
  而後,分別定義 7 個具體形狀的類,每一個均繼承Tetromino,並覆寫其抽象成員。
    
  以方塊 " I " 爲例:  
 
 
  其它 6 種方塊類的定義與此相似,略。
 
  接下來,就是實現隨機生成方塊的方法了。很顯然,須要用到隨機數(Random),能夠這麼來作:
  定義一個靜態字符串數組,內容就是7個字母 [ "I", "J", "L", "O", "S", "T", "Z" ],而後設定 Random 的範圍剛好就是 0~6 這七個正整數,對應數組的索引範圍。每次隨機生成一個數就等於指定了一個字母,最後,實例化該字母對應的方塊類就能夠了。
 
  方塊字母名稱數組的定義:  
 
 
  生成方法: 
 
 
  方塊造出來以後,下一步就是顯示在遊戲界面中了。這裏所謂的顯示,指的是將方塊初始位置對應的單元格修改成「下落「狀態。
  修改單元格的數值的實現方法以下:     
 
    
  至此,方塊的生成邏輯就處理完了。
 
  方塊的移動
 
  移動總共有 3 個方向:左、右、下。這裏暗含了一個判斷邏輯——方塊是否可以移動到目標位置,也就是前文中提到的「碰撞檢測」的做用效果。
  在表格化思惟的基礎上,所謂「碰撞檢測」其實就是判斷目標位置的單元格內是否已經有方塊了,或者是否超出遊戲界面的邊界了。具體爲:
 
  •   判斷單元格內是否有方塊 = 判斷單元格的數值是否等於表示有方塊的狀態值。
  •        判斷是否超出邊界 = 判斷單元格的座標是否在表格的行、列範圍內。
  針對方塊的移動操做,必定是做用於下落狀態的方塊。因此在執行動做以前,須要先獲取當前正在下落中的方塊的位置信息。根據前文的約定,處於下落狀態的單元格的數值爲 CellStatus.FALL, 經過這個約定,能夠獲取下落中的方塊位置。
    
 
    
  拿到了下落中方塊的單元格位置信息,就能夠操做向左、向右、向下移動了。
  先來看向左移動的狀況。
  所謂向左移動,實現效果上至關於將 4 個下落中的單元格的列索引減1,行索引不變。其成立的前提條件是:左移以後的 4 個單元格的值均不爲 CellStatus.BLOCK, 同時每一個列索引大於等於 0.(由於向左移動是朝第 0 列的方向動)
  當上面兩個條件都知足時,就能夠把方塊向左平移 1 個單位列寬了。具體操做爲:先將當前方塊的單元格數值清零(CellStatus.GAP),而後把目標單元格的值設置爲 CellStatus.FALL .
  代碼大概長這樣:
 
 
  右移的邏輯和左移是類似的,區別在於:
 
  •   右移的具體實現是將單元格的列索引加1。
  •   右移是朝表格的最大列索引方向移動,因此越界的判斷標準是和最大列的列索引比較。
  其他的邏輯是一致的,略。
 
  再來看方塊的下移。
  相較於左右移動,下移的操做是將下落方塊的單元格的行索引加1,而列索引保持不變。「碰撞檢測」的邏輯其實和左右移也相似,惟一的區別就是判斷越界時,比較的是行索引。
  不過,相較於左右移動,下移操做含有附加邏輯:每當完成一次下移動做以後,須要馬上判斷該方塊是否「觸底」?並由此決定是否將下落方塊轉換成固定方塊,所謂「觸底」,指的是已經到達遊戲界面的底部或者是觸及到了下方固定方塊。一言蔽之,就是要判斷該方塊是否還能繼續下移。一旦方塊「觸底」,則結束了該方塊的下落週期了,須要將其轉換成固定方塊,並同時通知遊戲產生新方塊。
  整個下移的代碼差很少是這樣的:
 
    
  到這裏,方塊的移動就處理完了。
    
  方塊的旋轉
 
  旋轉是遊戲裏的重頭戲,俄羅斯方塊之因此風靡全球,全靠這個 feature.
  而這也是曾經讓我感受無從下手的地方,受限於鄙人愚鈍的大腦,我徹底沒心情去推算圖形旋轉時的數學公式,雖然我以爲這裏應該有一個三角函數上的解,或是其它什麼數學上的玩意,能夠一步搞定。(BTW,我數學很差全賴本身,和體育老師沒有關係)
  既然正面硬剛沒把握,只好想別的辦法從側面繞過了。
  根據遊戲規則,方塊每次旋轉爲 90 度,在紅白機上的操做好像是 A 鍵爲順時針轉,B 鍵爲逆時針轉(這個不重要)。重點是,每一個方塊最多隻有 4 種旋轉形態
  更進一步的,對於 I 形、Z 形、S 型方塊而言,由於形狀自己具備對稱性,因此這 3 個方塊的旋轉形態只有 2 種。事實上,在紅白機的版本中,當按下旋轉鍵時,這 3 個方塊的旋轉並非一直朝一個方向旋轉的,而是順時針、逆時針交替進行,即只有兩種形態變化。
  再進一步,形方塊不存在旋轉變化,由於它就是一坨。
  因此,真正會有 4 種旋轉形態的是 L 形、J 形和 T 形,這 3 種方塊。
  也就是說,7 種方塊總共包含:  3 * 4 + 3 * 2 + 0 = 18 (種) 旋轉情形。那麼,徹底能夠用窮舉法,把 18 種狀況逐個實現,就完整地解決了整個遊戲的方塊旋轉問題了。
 
  從變化多的下手。( L 形,J 形,T 形),先說 T 形方塊。
  談到旋轉,天然要有一個軸心的概念,每一個方塊都存在一個旋轉中心點。T 形方塊的旋轉點是就是橫縱交叉的那個單元格。其 4 種旋轉形態以下圖所示:
 
  若是將軸點單元格的行、列座標表示爲 [ x, y ], 則 圖1 中的方塊座標能夠表示爲 { [ x, y-1 ] , [ x, y ] , [ x, y+1 ] , [ x+1, y ] }.
  4 種旋轉過程能夠演化成以下計算規則:
  1 -> 2  : 將方塊的座標調整成 { [ x-1, y ], [ x, y-1 ], [ x, y ], [ x+1, y ] }.
  2 -> 3 : 將方塊的座標調整成 { [ x-1, y ], [ x, y-1 ], [ x, y ], [ x, y+1 ] }.
  3 -> 4 : 將方塊的座標調整成 { [ x-1, y ], [ x, y ], [ x, y+1 ], [ x+1, y ] }.
  4 -> 1 : 將方塊的座標調整成 { [ x, y-1 ], [ x, y ], [ x, y+1 ], [ x+1, y ] }.
    
  旋轉邏輯就這麼簡單粗暴地搞定了。
  且慢!這遊戲裏,任何動做都不能忘了「碰撞檢測」。上面的方法只提供瞭如何旋轉,但還缺一個判斷可否旋轉的邏輯。
  和移動相似,旋轉的可行性判斷也包含目標單元格是否可用,這個不言自明。
  額外的,爲保證遊戲效果更符合物理規律,還要包含一個每一個單元格在其旋轉路徑中是否有阻擋的判斷?
  如圖所示:
 
    
 
 
  上圖中的陰影區域,都屬於旋轉路徑,不能有障礙物存在,不然,方塊在物理上是轉不動的
  和分析旋轉動做的方法相似,判斷旋轉可行性也是以軸心單元格爲基礎,經過相對位置逐個判斷單元格是否合法。
  代碼實現以下,聲明變量:   
 
 
  1 => 2 :  
 
 
  以上代碼爲 T 形方塊從形態 1 旋轉到形態 2 的具體實現,其他 3 種旋轉狀況與此同理,代碼從略。
 
  至此,T 形方塊的旋轉方法就完結了。
  L 形、J 形方塊的作法和 T 形是一個道理,無非軸心格還有路徑格的位置不一樣罷了,沒有本質區別,故略。
 
  接下來看只有兩種變化的方塊( I 形、S 形、Z 形),就以最受歡迎的 I 形方塊爲例。
  I 形方塊的軸心在第二塊格子,也就是說它旋轉起來是非對稱的。其旋轉時的單元格變化關係以下:
 
 
 
  一樣以軸心單元格爲基礎,設橫、縱座標爲 [ x, y ] ,則它的旋轉過程可量化成:
  1 -> 2 : 將方塊的座標調整成 { [ x-1, y ], [ x, y ], [ x+1, y ], [ x+2, y ] }.
  2 -> 1 : 將方塊的座標調整成 { [ x, y-1 ], [ x, y ], [ x, y+1 ], [ x, y+2 ] }.
    
  I 形方塊的旋轉路徑合法性檢測以下:
    
 
    
  嚴格來講,上面兩幅圖中的右下角單元格也應該歸入「碰撞檢測」的範圍內,但我故意去掉了對這個單元格的限制,由於這樣能夠提高遊戲流暢度。
 
  代碼實現以下:   
 
 
  Okay, I 形方塊的旋轉搞定了。
  另外兩個同類的 S 形、Z 形,實現原理同樣,略。
 
  如今還剩最後一個 O 形方塊待處理,那就處理掉吧。
 
     Talk is cheap, show me your code. -- Linus Torvalds
 
  廢話少說,直接上碼:
 
 
 
  O形方塊,Over.
    
  方塊的消除
 
  這但是續命的操做,得好好實現。
  其實這一塊的邏輯,相對來講是比較簡單的。
  從視覺上來看,當有一行或幾行被消除了,則上方全部的方塊就集體落下一行或幾行,但落下過程並不會改變方塊自己的相對位置。因此,能夠將這一過程當作是:被消除的行,先變成空行,而後將該空行從當前位置抽出,並插入到表格的頂部,表格中原先的行自動往下順移。如此就實現了方塊的消除和下落補位的效果了。
  主要代碼實現大約長這樣: 
 
    
  Game Over 的判斷
 
  雖然遊戲界面的高度有 25 行,但根據紅白機上的玩法來講,實際遊戲空間的高度在 20 行,當落下任意一個方塊,其高度達到第21行時,就GG了。
  根據這個規則,判斷遊戲結束的標準也就是判斷第 21 行是否有固定方塊了(這裏描述時是從底往上數,實際代碼實現時是從頂往下數)。
  代碼以下圖所示:  
 
 
  計分
 
  分數其實不是遊戲的必須,前文已經介紹的部分,已經能夠組裝起一個可玩的遊戲了。可是,若是俄羅斯方塊沒有分數的話,那就像賭博不帶彩,瞬間沒有存在價值了。因此,分數其實又是遊戲的必須。我和老爸就曾爲了爭個最高分數,鬥到老媽發飆「大家倆再不來吃飯我把菜都倒垃圾桶!」
 
  按照紅白機上的玩法,俄羅斯方塊的計分規則分爲 3 項:
 
  •   得分
  •   消除行數
  •   等級
  一次消除一行,得100分
  一次消除兩行,得400分
  一次消除三行,得900分
  一次消除四行,得2500分
 
  每次發生消除時,得分和消除行數累加。
  每累計消除 30 行,遊戲等級增長 1 級。 
  等級越高,方塊下落速度越快,至於到底提速多少,我也搞不清楚。在我本身作的這個版本里,我設定爲增速10%.
 
  部分代碼:
  
 
 
  其中,分數值作成了常量:
    
 
 
  遊戲等級升級及分數顯示:
 
 
 
  至此,遊戲中全部的業務邏輯都已被實現,最後就剩把這些業務邏輯按照遊戲的玩法組合起來了。
  首先,當遊戲開始前,大約須要作如下事情:
 
  1.   生成遊戲界面表格
  2.   生成預覽表格(根據紅白機的玩法,遊戲過程應該包含提示下一個方塊的預覽功能)
  3.   準備一個定時器
  4.   啓動遊戲
  代碼參考:
    
    
 
  在啓動遊戲階段,大約須要作如下事情:
 
  1.   計數清零
  2.   生成新方塊
  3.   生成下一個方塊
  4.   界面重繪
  5.   啓動定時任務
  代碼參考:
    
 
    
  定時任務中包含了方塊的下落操做。而在方塊完成一次下落以後,須要判斷方塊是否能夠繼續下落?
    若是方塊能夠繼續下落,則繼續;
    若是已經觸底了,則須要對遊戲界面的狀態進行更新,緊接着判斷遊戲是否結束?
      若是遊戲結束,則結束當前遊戲;
      若是遊戲未結束,則進行計分操做並開始新的方塊;
 
  代碼參考:
 
  定時任務
    
 
   
  下落後處理
    
 
 
  而在方塊下落的過程當中,須要監聽鍵盤事件,完成相應的事件處理:
 
  •   上:執行旋轉
  •   下:方塊向下移動一行
  •   左:方塊向左移動一列
  •   右:方塊向右移動一列
  •   空格:方塊快速下落(直接一落到底)
  •   F2: 從新開始
  •   F8: 暫停
  代碼參考:
 
 
 
  That's all .
 
  到此,全部的事情都作完了,該玩一把了。
 
 
 
 
  Bug Report
    
  在我之前的一篇文章裏,我說過 " No bug, no code. ",我再補充一句:「沒有例外。」
 
  遊戲能玩,但有兩處遺憾:
  1.  當平移或者旋轉方塊時,會中斷方塊的自動下落過程。而在紅白機上方塊會持續下落。
  2.  根據我設計的旋轉邏輯,當I形方塊貼着遊戲界面的邊框時,沒法旋轉,由於旋轉路徑檢測會不經過。可是在紅白機上的,一樣的狀況,能夠旋轉,其產生的效果有點像方塊在邊框上打滑了一下而後被擠開。
  雖然明知存在瑕疵,但在我一鼓作氣寫完整份代碼後,已無意修復。
  畢竟能玩了,畢竟,我又不是處女座
  
 
  寫在最後
 
  據說俄羅斯方塊是遊戲開發領域的 " hello world ",這麼說來,我也算入門遊戲設計了?呵呵~
  決定寫這個程序是在一個週末的午後,動手前估計着週末兩天應該都得搭進去了。但當我完成核心代碼並玩上第一把的時候,天還沒黑,差很少用了也就一個下午的工夫,精確點說,大約 4 個小時左右。說實在的,我本身都很意外。若是我參加一個面試,要求在 4 個小時的時間內作一個俄羅斯方塊,我確定直接 「謝謝,再見」 了。不由回想當年初學 C# 時,就想作這個遊戲,卻淺嘗輒止,竟擱置了這麼久才實現,慚愧。諷刺的是,現在我寫下的這份代碼,徹底基於 .NET 2.0 的框架,C# 2.0 的語法,徹底沒有超出當年我初學 C# 時的知識體系。
    
  反思之餘,就用一句我很喜歡的英語格言做爲結束吧:
 
  WHEN THE GOING GETS TOUGH ,  THE TOUGH GET GOING .
 
 
 
  附 
  完整代碼(GitHub地址): https://github.com/sherrywasp/tetris.git
相關文章
相關標籤/搜索