使用JavaScript實現一個俄羅斯方塊

清明假期期間,閒的無聊,就作了一個小遊戲玩玩,目前遊戲邏輯上暫未發現bug,只不過樣子稍微醜了一些-.-
項目地址:github.com/Jiasm/tetri…
在線Demo:blog.jiasm.org/tetris/?wid… (修改URL參數能夠調整難度)javascript

總體分紅三塊進行開發,使用面向對象式編程進行開發(其實我更喜歡用函數式編程,但苦於遊戲的一些狀態用對象來存儲會更直觀一些):java

  1. Game
    1. 負責生成新的方塊
    2. 負責方塊移動的處理
    3. 方塊觸底的判斷
    4. 移除知足清除條件的行
  2. Render
    1. 負責用Game的數據來渲染整個遊戲界面
  3. Controller
    1. 負責接受用戶輸入(上下左右各類操做)並處理
    2. 向用戶反饋當前遊戲的狀態

這樣分層帶來了一個好處,咱們遊戲的邏輯Game模塊並不依賴於當前程序運行的環境,而Render能夠是CanvasDOM,甚至是控制檯輸出。咱們要移植到其餘平臺,只須要修改Render便可。git

項目結構

忽略了一些與遊戲沒有直接關係的結構github

.
├── model
│   ├── Brick.js
│   ├── Game.js
│   └── index.js
├── utils
│   ├── buildEnum.js
│   ├── deepCopy.js
│   ├── getShape.js
│   ├── index.js
│   ├── lineIndex.js
│   ├── matrixString.js
│   └── rotateArray.js
├── enum
│   ├── gameType.js
│   ├── index.js
│   └── pointType.js
├── data
│   └── shapes.js
├── controller
│   └── index.js
└── view
    ├── RenderCanvas.js
    └── index.js
複製代碼

各目錄下的index.js是爲了方便同時引用多個文件,大體長這個樣子:shell

export { default as model1 } from './model1'
export { default as model2 } from './model2'
複製代碼

而後咱們就能夠在用到的地方寫:編程

import { model1, model2 } from './XXX'
複製代碼

model

這裏是遊戲的核心邏輯所在位置。canvas

像俄羅斯方塊這種的矩陣類遊戲,存儲數據最合適的方法就是一個二維數組了。 爲了更直觀一些,咱們選擇了遊戲的高度做爲第一層數組的長度:數組

matrix = new Array(height).fill(new Array(width))

// width: 2 height: 4
[
  [ 1, 1],
  [ 1, 1],
  [ 1, 1],
  [ 1, 1]
]
複製代碼

並且這樣選擇在一些邏輯處理上也會更方便一些:函數式編程

  1. 下移操做時,咱們只需改變元素的第一層下標
  2. 判斷是否觸底時,咱們只需將當前下標 + 1 判斷是否有元素便可

咱們對數組中的元素進行了定義:函數

  • 0: 空,表示當前座標爲空白
  • 1: 新的方塊,表示當前活動的方塊
  • 2: 老的方塊,已經觸底固定的方塊

接下來,咱們就遇到了一個問題,如何處理方塊的放置。 咱們知道,遊戲會不停的向棋盤中加載新的方塊。 若是咱們每次處理下移的時候,都將當前二維數組中對應的方塊元素移除,而後在塞入到新的位置,未免太過繁瑣了。

因此咱們在初始化數據時,初始化兩個二維數組。 當咱們加載一個新的方塊後,將方塊對應的元素塞入其中的一個二維數組。 而後等到咱們有進行其餘的操做時,好比左右移動,向下之類的。 咱們直接使用第二個二維數組覆蓋到當前的數組中去,而後再將更改下標後的方塊塞入數組。 這樣在數據上,咱們就完成了方塊的移動。

class Game {
  init () {
    // 初始化兩個矩陣
    this.matrix = [[], []]
    this.oldMatrix = [[], []]
  }
  move () {
    // 重置當前矩陣數據
    this.matrix = deepCopy(this.oldMatrix) // 解除引用
    // 加載方塊數據
    this.matrix[y][x1] = 1
    this.matrix[y][x2] = 1
  }
}
複製代碼

左右移動的處理

左右的移動不能像向下移動同樣,單純的下標+1。 咱們須要判斷當前的操做是否有效。 好比右側若是遇到了障礙物或者到達邊緣,咱們確定是不可以再進行移動的。

// blend 爲活動磚塊的形狀描述 [[1, 1, 1], [0, 1, 0]] 相似這樣的結構
if (
  x >= width - brickWidth ||
  blend.some((row, rowIndex) => {
    let _pos = oldMatrix[y + rowIndex]
    return row && row[brickWidth - 1] && _pos && _pos[x + brickWidth]
  })
)
  return // 右側有障礙物,沒法移動
複製代碼

使用相似這樣的邏輯進行判斷,保證當前方塊向右移動後不會覆蓋以前的方塊。

快速向下的處理

我看有些遊戲實現的,貌似降低觸發只是加速降低而已(這種狀況只須要改變定時降低的速度便可)-.-這裏的實現是,直接觸底

因此就會遇到一個問題,當前磚塊最多能夠降低到什麼位置?

[1, 1, 1]
[0, 0, 0]
[0, 2, 0]
[2, 2, 2]
複製代碼

就像這樣的一個數據,0|2這兩列均可以向下移動兩列,可是這樣就會致使中間一列的重疊。 咱們必定要取出降低幅度最小的那個值。 因此咱們就要算出最後一行1的下標以及第一行2的下標,將這兩個下標進行相減,最小值即爲咱們當前方塊可降低的距離。

旋轉方塊的處理

旋轉方塊應該是遊戲中比較複雜的一塊邏輯了。 毫不是僅僅簡單的將方塊的二維數組由行改成列,在有些時候,咱們還須要判斷方塊是否能夠進行旋轉。

就像這樣的,中間的長條是不可以進行旋轉的。 因此咱們要先拿到旋轉後的數據,來與當前遊戲中的數據進行比較,檢驗是否會出現重疊的狀況,若是出現了,則表示不可以進行旋轉。

觸底檢測

每完成一個移動的動做後,咱們都須要進行方塊的觸底檢測。 也就是判斷當前方塊下,是否已經有元素佔位,若是有的話,則表示已經觸底了,當前元素就會被固定進矩陣數組中。 一樣的,咱們在判斷時,不須要將方塊全部的下標都檢查一遍,只須要檢查最底部一層的有效元素便可。

[1, 1],
[0, 1],
[0, 1],
複製代碼

像這樣的一個方塊,咱們僅須要判斷第一列的第二行&第二列的第四行是否有元素便可完成檢查。

移除行

當某一行被填滿元素後,咱們就要將它進行移除。 在觸底檢測觸發後,若是有方塊被固定進數組,此時咱們再進行移除行的操做。 由於若是沒有新的方塊進入,移除行的這步操做就不是必要的。 同時,得分的計數也應該在此處進行,咱們將移除的行數進行記錄,獲取到的行數即是得分了。

至此,全部有關矩陣數據的操做就結束了。 Game對象只去維護這麼一個二維數組,對象自己不包含任何遊戲相關的操做,只會在被調用時進行對應的處理。 而後生成新的二維數組。

utils

這裏放置了一些比較通用的方法,用來提升開發效率使用。 好比獲取方塊最底部一層的下標之類的工具函數。

enum

存放了一些狀態的枚舉,遊戲狀態以及方塊所對應的狀態,相似這樣的數據:

{
  empty: 0,
  newBrick: 1,
  oldBrick: 2
}
複製代碼

data

存放了遊戲中各類使用到的方塊信息。 正方形,梯形之類的方塊在二維數組中所對應的描述。

controller

就是上邊咱們所說的,用來與用戶交互的模塊,由Controller來獲取遊戲相關的信息,並調用Render進行渲染。 監聽鍵盤事件,在頁面中渲染一些控制按鈕。 以及定時觸發Game的下落方法。

view

遊戲界面的渲染部分,目前選定的是使用canvas,因此只寫了RenderCanvas。 在渲染的這部分,稍微作了一些優化處理,將活動中的方塊與固定的方塊進行分開渲染。 這樣在用戶操做上下左右移動時,並不會從新渲染整個遊戲佈局,而只是渲染活動方塊的canvas

小記

兩天多的時間進行開發,其中有半天時間在修復FlowType的Warning提示。。。 搞完了之後,以爲實現這個的主要難點就在於方塊旋轉&觸底的判斷這裏了。 可以清晰的管理遊戲對應的二維數組,這個遊戲開發起來就會很順暢。

界面還有待優化。

相關文章
相關標籤/搜索