小時候,天天放學回到家,急衝衝寫完家庭做業,而後我就搬出遊戲機,開始打遊戲。對於 80 後而言,「 遊戲機 」 這三個字是一個不言自明的符號,那就是經典的任天堂紅白機,還有一盤盤能夠表明幸福指數的遊戲卡。當年誰家的小朋友擁有的遊戲卡多,誰就是當時的紅人,尤爲是黑卡,簡直是高端的象徵。
得益於父母開明,打遊戲這事我沒受過限制,又得益於我有個偉大的表哥,因此個人幸福指數也歷來都是在高位震盪。經典的《魂鬥羅》《坦克大戰》《綠色兵團》《赤色要塞》《熱血系列》等,冷門的《懲罰者》《聖鬥士》《電梯》等等,包括後來玩的文卡《三國志》,雖不敢說天下游戲玩了個遍,但也是見過世面的資深玩家了。
父親雖然不限制我,卻常會嘮叨「做業作完了就多看看課外書,少玩遊戲。打遊戲浪費時間。」 常常對着我正在玩的《熱血足球》說:「你要踢球就下樓去踢,操做幾個娃娃人在電視上踢有什麼意思?並且你這都是些什麼亂七八糟的,還有星星?!」(玩過熱血足球的應該都知道此刻屏幕上「小辮子」正在放大招)。
可是,父親有時候也會和我一塊兒玩,很投入地玩,並且永遠只玩一個遊戲——《俄羅斯方塊》。
無數個週末下午,我和父親就是在對戰俄羅斯方塊中度過的。
這款遊戲,對於我而言,有着很深入的印象,它承載着童年,也承載着父子之情。
《俄羅斯方塊》(Tetris) 誕生於 1984 年 6 月 6 日。當時在前蘇聯科學院電算中心工做的科學家 阿列克謝-帕基特諾夫 利用空閒時間設計和編寫了這個落下型益智遊戲的始祖。按理說,這款遊戲其實應該叫作 「前蘇聯方塊」。
這款冷戰時期的產物,成了第一個進入美國的蘇聯遊戲。後來還被評爲「最偉大的100個遊戲」中的第1名、Gome Boy 史上最受歡迎的遊戲、有史以來最暢銷的電子遊戲,榮譽太多了。
當年初學 C# 時,以爲用 Winform 作點小玩意很快樂,曾想本身作一個俄羅斯方塊。但一想到「方塊旋轉」,「碰撞檢測」,「GDI+ 動畫」等等具體問題,就一頭霧水,感受難度太大,因而就放棄了。
一放就是好多年。
前不久,突然有一天,也說不上來什麼緣由,又想到了俄羅斯方塊,又萌生了本身動手寫一個的念頭。可能這就是所謂的童年的影響所帶來的執念,不論過多久,總有個牽掛在內心。
念頭來得容易,但問題依然存在,仍是得想辦法解決旋轉、碰撞、動畫等實際問題。
鑑於個人早年經歷,用 C# 來作這事的話,首先就想到 GDI+ ,可我之前學 GDI+ 時就沒有學好,這麼多年過去了,更不會了。我也不從事遊戲開發領域,對於動畫,尤爲是處理圖形變換、碰撞檢測這樣的概念,缺少相關知識儲備。
再一次感到一籌莫展。
因而我遊離了。
倒也沒遊多遠,在 Wikipedia 上查閱「俄羅斯方塊」這個詞條,想了解一下游戲的背景。意外地發現了一個有趣的知識:俄羅斯方塊的英文名叫作 Tetris, 這個名稱來自於希臘數字表示 4 的前綴 "tetra" 和網球 「tennis」 ——兩個詞組合而成。之因此用數字 4 的緣由是遊戲中的方塊元素都是由4個方格組成,而網球則是做者最喜歡的運動。
雖然玩了無數遍,但看到這條信息時我才意識到,原來遊戲中的那些方塊均可以分解成4個小方格組成。
而這個意外的收穫給了我一個啓發:既然每一個方塊能夠方格化,那麼擴展一下,整個遊戲的構成也能夠網格化。
瞬間,事情出現了起色。
俄羅斯方塊遊戲的視覺呈現能夠抽象爲兩部分:背景空間和方塊。
將背景空間當作一張表格,方塊能夠當作是表格中單元格的組合。
因而,遊戲中任意一個時刻的靜態畫面,能夠抽象成:
一張表格中,部分單元格有方塊,部分單元格無方塊,且每個單元格只可能處於兩種狀態中的一種。
而遊戲的過程就是將全部時刻的靜態畫面按時間順序「一幀幀」呈現。 也就是說,全部的視覺效果均可以映射爲屢次更新一張表格中的不一樣單元格的狀態。
如此一來,以前困擾個人——
「圖形旋轉」就演化成了修改單元格的值。
「碰撞檢測」就只須要判斷單元格的行列位置。
至於「GDI+繪圖」,就變成了給單元格着色。
時間上,將動態過程切割成離散的靜態畫面;空間上,將連續畫面切割成離散的網格。 解決連續動態的問題頗費周章,但處理一張表格就簡單多了。所謂思路決定出路,大概就是這樣吧。
眼前的障礙消除了,這事感受能夠作下去了。
動手以前,先作計劃。完成這個任務,大體可分解成如下環節:
- 設定網格區域
- 方塊的生成
- 方塊的移動
- 方塊的旋轉
- 方塊的消除
- GameOver的判斷
- 計分
One thing at a time. —— Mark Watney
在開始以前,約定一下思路 —— 遊戲界面由表格提供,表格內的單元格分爲3種狀態:
對於下落中的方塊,顯示爲方塊原本的顏色;對於已落定的方塊,顯示爲當前遊戲級別對應的顏色(當年紅白機上的版本就是這麼設定的)。
因此,遊戲畫面的網格化抽象效果以下圖所示:
參考代碼:
狀態常量設定
表格渲染
有了上面這樣一個思路,下面就一步步實現具體的遊戲邏輯。
首先,解決基礎設施的問題——網格。
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 個方塊的旋轉並非一直朝一個方向旋轉的,而是順時針、逆時針交替進行,即只有兩種形態變化。
再進一步,O 形方塊不存在旋轉變化,由於它就是一坨。
因此,真正會有 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 ] }.
旋轉邏輯就這麼簡單粗暴地搞定了。
且慢!這遊戲裏,任何動做都不能忘了「碰撞檢測」。上面的方法只提供瞭如何旋轉,但還缺一個判斷可否旋轉的邏輯。
和移動相似,旋轉的可行性判斷也包含目標單元格是否可用,這個不言自明。
額外的,爲保證遊戲效果更符合物理規律,還要包含一個每一個單元格在其旋轉路徑中是否有阻擋的判斷?