Code game xếp hình bằng javascript

Hẳn rằng ai trong chúng ta cũng đều biết và đã từng chơi thử trò chơi xếp hình [tetris] rồi đúng không, nhưng không phải trò "xếp hình" như bạn đang nghĩ đâu nhé. Tetris là một game hết sức đơn giản được làm bởi những người bạn Liên Xô của chúng ta từ những năm 80, đi cùng không thể thiếu là bản nhạc của Hồng Quân huyền thoại Korobeiniki.

Tuy đơn giản và lâu đời như vậy, Tetris có sức hút và tính gây nghiện rất lớn [tác giả gốc của tựa game còn nói rằng khi đang phát triển game, anh mải chơi game này đến mức quên cả việc hoàn thành nốt các đoạn code!]. Hội chứng Tetris Effect nổi tiếng cũng dùng để chỉ trạng thái người chơi một tựa game [như Tetris] quá nhiều, đến mức nhìn đâu cũng thấy mấy hình khối rơi xuống, kể cả trong giấc mơ.

Hôm nay, mình sẽ cùng các bạn làm một tựa game Tetris từ A-Z hoàn chỉnh, không bug, đủ chức năng nhất. Trong bài này, mình xin lựa chọn ngôn ngữ Javascript để tiện lợi cho việc demo ngay trên trình duyệt. Tuy nhiên, sau khi đọc xong bài này, bạn hoàn toàn có thể đủ khả năng để làm nó với bất cứ ngôn ngữ hướng đối tượng nào khác.

Sơ lược về Tetris

Có lẽ bạn cũng đã biết hay cũng chơi game Tetris rất nhiều lần rồi. Nhưng để cho chắc ăn, mình xin phép được nhắc lại chút ít về nguyên tắc tựa game này:

Game Board

Còn hay được gọi là "playfield", hay "matrix",... Đại loại đây chính là phần bố cục ô lưới, và là nơi chính để chơi game của bạn. Ở màn hình khi chơi game, bạn sẽ thấy game board rộng cỡ 20 hàng x 10 cột. Tuy nhiên, bạn có biết thực tế ở những game Tetris, board game có chiều cao thật từ 22 lên đến 40 ô [tức 40 hàng x 10 cột], và các ô trên cao từ thứ 20 từ dưới lên trở đi thực chất bị ẩn khỏi màn hình? Trong bài này, mình sẽ sử dụng board game rộng 23 x 10, với 3 hàng trên cùng bị ẩn khỏi giao diện game.

Các bạn hãy thử đoán xem tại sao mình lại để dư 3 hàng trên cùng đó nhé.

Tetromino

Là những khối hình thù quái dị từ trên trời rơi xuống. Có 7 loại tất cả: khối chữ L, J, O, T, S, Z, và I. Mỗi loại khối có màu sắc tương ứng khác nhau.

Các khối này đều có thể bị xoay [theo chiều kim đồng hồ] cũng như di chuyển [sang trái hoặc phải]. Tuy nhiên khối sẽ không thể xoay hay di chuyển được nếu gặp va chạm [với phần cạnh hay với các khối đã hạ cánh].

Game tick

Cứ sau cùng một khoảng thời gian ngắn [thường được đặt từ 0,5 đến 1 giây], khối Tetromino hiện tại sẽ rơi xuống thêm một ô. Sau khi rơi xuống tận cùng [chạm mặt đất hay chạm vào các khối đã hạ cánh khác], khối hiện tại sẽ bị gắn lại, và một khối mới khác sẽ rơi xuống.

Ăn điểm

Khi một hàng được "hoàn thành", hay được lấp đầy bởi các khối, bạn sẽ được ăn điểm. Với càng nhiều hàng được hoàn thành một lúc, bạn càng được nhiều điểm, tối đa là 4 hàng với khối chữ I.

Các hàng đã được hoàn thành sau đó sẽ được xóa khỏi bảng, cùng với khiến các khối ô ở phía trên nó thấp xuống dưới.

Game over

Trò chơi kết thúc khi lượng khối đã rơi xuống chồng chất lên đến mức chạm vào cạnh trên cùng của board, và khối mới không thể rơi xuống được nữa.

Sơ qua như phía trên cũng là kha khá chi tiết rồi, giờ bắt tay vào làm thôi.

Tiến hành

Khởi tạo dự án

Tạo các thư mục và file như thế này:

tetris-html/
├── index.html
└── main.js

Khởi tạo file index.html:

doctype html>

  
    
    Tetris Game
    
  
  
    
    
  

Từ giờ các mã javascript sẽ được viết chủ yếu ở main.js.

Bước 1: Xây dựng giao diện cơ bản cho game

Với tựa game đơn giản như Tetris, ta có thể lựa chọn dựng giao diện chơi bằng HTML và CSS [tức dựa vào DOM], hoặc dùng HTML5 Canvas. Trong bài này, mình sẽ thử xây dựng game qua HTML5 Canvas.

Mục tiêu của chúng ta sẽ là xây dựng một giao diện trông như thế này:

Khóa học siêu tốc

Vẽ hình vuông/hình chữ nhật lên canvas rất đơn giản. Vẽ một hình vuông nhỏ lên canvas như sau:

/* Lấy context của phần tử canvas */
const canvas = document.getElementById['canvas']
const ctx = canvas.getContext['2d']

/* Vẽ hình vuông */
ctx.fillStyle = 'black'
ctx.fillRect[10, 20, 50, 50]

Bạn sẽ được hình vuông đã tô màu đen, nằm ở tọa độ x = 10, y = 20 và cạnh dài 50px.

Một ví dụ khác, ta thử vẽ một hình chữ nhật có viền màu đỏ:

ctx.strokeStyle = 'rgb[255, 0, 0]'
ctx.rect[10, 20, 50, 100]
ctx.stroke[]

Bạn sẽ có ngay hình chữ nhật không tô màu nhưng có viền đỏ, đặt ở tọa độ x = 70, y = 20, rộng 50px và cao 100px.

Viết chữ lên canvas cũng rất đơn giản. Tương tự 2 ví dụ trên, chữ này được viết với màu đen, kích thước 14px, đặt ở tọa độ x = 10, y = 140:

ctx.fillStyle = 'black'
ctx.font = '14px';
ctx.fillText['HAPPY NEW YEAR', 10, 140]

Demo như dưới đây, nhấn vào tab Result nhé.

Áp dụng vào bài

Trước hết, để game hoạt động được, ta cần có một vài biến để lưu lại trạng thái của game theo thời gian. Ví dụ như những cái sau:

  • boardWidth, boardHeight là chiều rộng, chiều cao của game board.
  • currentTetromino giữ đối tượng khối Tetromino hiện tại
  • currentBoard giúp lưu trạng thái hiện tại của game board. currentBoard là một mảng 2 chiều lưu các phần tử số nguyên, với phần tử toàn là số 0 [tức chưa có khối nào được đặt lên]. Tuy javascript không có khái niệm mảng 2 chiều, ta có thể mô phỏng nó bằng cách định nghĩa mảng con bên trong mảng.
  • landedBoard cũng tương tự currentBoard. Nhưng thay vì lưu cả thông tin về khối đang rơi và các khối đã "hạ cánh" như currentBoard, landedBoard chỉ lưu thông tin về các khối đã "hạ cánh". Có thể nói currentBoard=landedBoard+currentTetromino.
  • score để lưu điểm số hiện tại.

Chúng ta sẽ đi sâu nhiều hơn về các thuộc tính này ở phần sau của bài.

Mình sẽ dùng class syntax của ES6 để định nghĩa một class Game:

class Game {
    constructor[] {
        this.score = 0
        this.boardWidth = 10
        this.boardHeight = 23
        this.currentBoard = new Array[this.boardHeight].fill[0].map[[] => new Array[this.boardWidth].fill[0]]
        this.landedBoard = new Array[this.boardHeight].fill[0].map[[] => new Array[this.boardWidth].fill[0]]
        this.currentTetromino = null /* TODO */
    }
    
    /* TODO */
}

Ở phần , ta bổ sung thêm thẻ , nơi mà chúng ta sẽ vẽ ra đồ họa game:


Lấy context của phần tử đó:

class Game {
  constructor[] {
    /* ... */
    this.canvas = document.getElementById['tetris-canvas']
    this.ctx = this.canvas.getContext['2d']
  }

Thêm phương thức draw[] vào class Game. Ở phương thức này, ta cần làm 3 việc:

  • Vẽ khung bao bên ngoài, là hình chữ nhật chỉ có màu viền.
  • Vẽ từng ô vuông block nhỏ theo dạng lưới 20x10, tô [fill] màu trắng nhạt cho nó. Ta cần 2 vòng for lồng nhau để làm điều này.
  • Thêm một vài text lên nữa.
class Game {
  /* ... */
    
  draw[blockSize = 24, padding = 4] {
    /* Vẽ khung của board */
    this.ctx.clearRect[0, 0, this.canvas.width, this.canvas.height];
    this.ctx.lineWidth = 2
    this.ctx.rect[padding, padding, blockSize*this.boardWidth+padding*[this.boardWidth+1], blockSize*[this.boardHeight-3]+padding*[this.boardHeight-3+1]]
    this.ctx.stroke[]

    /* Lặp qua các phần tử của mảng board và vẽ các block tại đúng vị trí */
    for [let i = 3; i  {
  const game = new Game[]
  game.draw[]
}]

Xong rồi, bạn đã [tạm] hoàn thành tạo canvas cho giao diện game. Kết quả của bước 1 này như ở JSFiddle dưới đây:

Bước 2: Định nghĩa class cho các hình khối [Tetromino]

Ý tưởng để biểu diễn dữ liệu

Như ở phần trước, chúng ta đã sử dụng thuộc tính currentBoard là một mảng 2 chiều để diễn tả game board của chúng ta. Các phần tử của mảng này sẽ là các số nguyên, biểu diễn cho những ô nhỏ [block] tương ứng trong board:

  • Số 0: Chưa có block nào ở đây
  • Số 1: Block thuộc một khối chữ L
  • Số 2: Block thuộc một khối chữ J
  • Số 3: Block thuộc một khối chữ O
  • Số 4: Block thuộc một khối chữ T
  • Số 5: Block thuộc một khối chữ S
  • Số 6: Block thuộc một khối chữ Z
  • Số 7: Block thuộc một khối chữ I

Để diễn tả hình dạng các khối Tetromino, mình cũng sẽ sử dụng các mảng 2 chiều, ví dụ với khối chữ L nằm dọc:

/* Khối chữ L nằm dọc */
[
  [1, 0],
  [1, 0],
  [1, 1]
]

Ấy nhưng trong Tetris, người chơi có thể xoay Tetromino đang rơi xuống theo nhiều góc khác nhau [theo chiều kim đồng hồ]: 0, 90, 180 và 270 độ. Nếu muốn, bạn có thể làm một phương thức để "xoay" mảng trên và trả về mảng đã xoay bằng chút thuật toán. Tuy nhiên, để cho đơn giản, ở đây chúng ta sẽ định nghĩa mọi chiều xoay của các Tetromino luôn. Ví dụ với Tetromino chữ L như sau:

[[[1, 0],
  [1, 0],
  [1, 1]],

 [[1, 1, 1],
  [1, 0, 0]],

 [[1, 1],
  [0, 1],
  [0, 1]],

 [[0, 0, 1],
  [1, 1, 1]]]

Tạo class cho các Tetromino

Giờ chúng ta sẽ bắt tay vào viết code nào. Mình sẽ tạo một abstract class tên Tetromino, và cho các loại Tetromino kế thừa nó. Các Tetromino đều có chung những thuộc tính và phương thức:

  • row, col: vị trí đặt theo hàng và cột.
  • angle: chiều xoay hiện tại, là các số 0, 1, 2, 3 [tương ứng với các góc 0, 90, 180, 270 độ].
  • width, height: chiều dài và chiều rộng.
  • move[]: di chuyển trái phải.
  • fall[]: di chuyển xuống.
  • rotate[]: xoay
  • ...

Tóm lại, ta có các class định nghĩa các Tetromino như sau:

class Tetromino {
  constructor[row, col, angle = 0] {
    if [this.constructor === Tetromino] {
      throw new Error["This is an abstract class."]
    }
    this.row = row
    this.col = col
    this.angle = angle
  }

  get shape[] {
    return this.constructor.shapes[this.angle]
  }

  get width[] {
    return this.shape[0].length
  }

  get height[] {
    return this.shape.length
  }

  fall[] {
    this.row += 1
  }

  rotate[] {
    if [this.angle  {
      this.progress[]
      this.updateCurrentBoard[]
      this.draw[]
    }, 800];
  }
  
  progress[] {
    /* TODO */
    this.currentTetromino.fall[]
  }
  
  updateCurrentBoard[] {
    for [let i = 0; i 

Chủ Đề