Chắc hẳn tuổi thơ chúc ta ai cũng biết đến trò chơi “Tetris” hay thường gọi là game ‘xếp hình’. Nhưng nó được tạo ra thế nào thì những người không làm về lập trình thì chắc sẽ không biết được còn nhưng người có kiến thức về lập trình thì tại sao chúng ta không thử tự code và chơi trên chính game mình code ra nhỉ? Hôm nay, tôi sẽ đưa bạn đến với hành trình implement trò chơi cổ điển Tetris. Tôi sẽ đề cập đến các khái niệm như đồ họa, vòng lặp trò chơi và phát hiện va chạm. Cuối cùng, chúng ta sẽ có một trò chơi hoạt động đầy đủ với điểm và cấp độ.
Code của trò chơi phiên bản đầy đủ đã có trên GitHub. Bạn hãy lên và nguyên cứu thêm nhé.
Chắc hẵn chúng ta ai cũng biết chơi trò này rồi nên hãy cùng đến với phần implement nó luôn nhé
Bạn cần có kiến thức gì
Thực ra cũng chẳng có gì to tát cả, mọi thứ đều rất là cơ bản
- Một chút kiến thức về typescript
- Biết một chút về Angular(chỉ một xíu xiu thôi, học 2 3 tiếng chắc cũng đủ =)) )
- Một chút kiến thức về Canvas(cũng chỉ là vẽ hình cơ bản luôn bạn có thể xem ở đây)
Cài đặt môi trường
Với angular thì tất nhiên đầu tiên chúng ta sẽ cài Angular CLI rồi
1 2 | npm install -g @angular/cli |
Với CLI được cài đặt, chúng ta có thể tạo một project mới với ng new:
1 2 | ng new ng-tetris — minimal — defaults |
Playing board bao gồm 10 cột và 20 hàng. Các giá trị này có lẽ chúng ta sẽ phải sử dụng thường xuyên để chạy vòng lặp trong các logic khi implement, vì vậy tôi sẽ đưa chúng vào các giá trị constants ở constants.ts
1 2 3 4 | <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token constant">COLS</span> <span class="token operator">=</span> <span class="token number">10</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token constant">ROWS</span> <span class="token operator">=</span> <span class="token number">20</span><span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token constant">BLOCK_SIZE</span> <span class="token operator">=</span> <span class="token number">30</span><span class="token punctuation">;</span> |
Trước tiên, chúng ta cần giới thiệu một phần tử <canvas>
, mà chúng ta có thể thực hiện trong component template. Chúng tôi cũng sẽ sử dụng một reference variable vào phần tử để có thể tham chiếu đến nó component class. Đây là template hoàn chỉnh, tôi sẽ implement một số logic để làm cho những điều hay ho xảy ra:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"grid"</span><span class="token operator">></span> <span class="token operator"><</span>canvas #board <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"game-board"</span><span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>canvas<span class="token operator">></span> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"right-column"</span><span class="token operator">></span> <span class="token operator"><</span>div<span class="token operator">></span> <span class="token operator"><</span>h1<span class="token operator">></span><span class="token constant">TETRIS</span><span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span> <span class="token operator"><</span>p<span class="token operator">></span>Score<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> points <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span> <span class="token operator"><</span>p<span class="token operator">></span>Lines<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> lines <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span> <span class="token operator"><</span>p<span class="token operator">></span>Level<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">{</span> level <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator"><</span><span class="token operator">/</span>p<span class="token operator">></span> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> <span class="token operator"><</span><span class="token function">button</span> <span class="token punctuation">(</span>click<span class="token punctuation">)</span><span class="token operator">=</span><span class="token string">"play()"</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"play-button"</span><span class="token operator">></span>Play<span class="token operator"><</span><span class="token operator">/</span>button<span class="token operator">></span> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> |
Đây là board.component.ts
và đây là những logic khởi đầu, component này là component chính để thực hiện các logic, hãy xem nhé:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <span class="token keyword">import</span> <span class="token punctuation">{</span> Component<span class="token punctuation">,</span> ViewChild<span class="token punctuation">,</span> ElementRef<span class="token punctuation">,</span> OnInit <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@angular/core'</span><span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> <span class="token constant">COLS</span><span class="token punctuation">,</span> <span class="token constant">BLOCK_SIZE</span><span class="token punctuation">,</span> <span class="token constant">ROWS</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./constants'</span><span class="token punctuation">;</span> @<span class="token function">Component</span><span class="token punctuation">(</span><span class="token punctuation">{</span> selector<span class="token operator">:</span> <span class="token string">'game-board'</span><span class="token punctuation">,</span> templateUrl<span class="token operator">:</span> <span class="token string">'board.component.html'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">BoardComponent</span> <span class="token keyword">implements</span> <span class="token class-name">OnInit</span> <span class="token punctuation">{</span> <span class="token comment">// Get reference to the canvas.</span> @<span class="token function">ViewChild</span><span class="token punctuation">(</span><span class="token string">'board'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token keyword">static</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> canvas<span class="token operator">:</span> ElementRef<span class="token operator"><</span>HTMLCanvasElement<span class="token operator">></span><span class="token punctuation">;</span> ctx<span class="token operator">:</span> CanvasRenderingContext2D<span class="token punctuation">;</span> points<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> lines<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> level<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> <span class="token function">ngOnInit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">initBoard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">initBoard</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Get the 2D context that we draw on.</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>canvas<span class="token punctuation">.</span>nativeElement<span class="token punctuation">.</span><span class="token function">getContext</span><span class="token punctuation">(</span><span class="token string">'2d'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Calculate size of canvas from constants.</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span>canvas<span class="token punctuation">.</span>width <span class="token operator">=</span> <span class="token constant">COLS</span> <span class="token operator">*</span> <span class="token constant">BLOCK_SIZE</span><span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span>canvas<span class="token punctuation">.</span>height <span class="token operator">=</span> <span class="token constant">ROWS</span> <span class="token operator">*</span> <span class="token constant">BLOCK_SIZE</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">play</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Trong app.component.ts
hãy thêm game-board
được khai báo ở trên nhé:
1 2 | <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>game-board</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>game-board</span><span class="token punctuation">></span></span> |
Styling
Đây là một trò chơi của thập kỉ 80 có thể thiết kể sẽ cổ cổ, Press Start 2P là một phông chữ bitmap dựa trên thiết kế phông chữ từ trò chơi arcade Namco những năm 1980. Chúng ta có thể thêm nó theo hai bước:
1 2 3 4 | <span class="token comment"><!-- index.html --></span> <link href=”https://fonts.googleapis.com/css?family=Press+Start+2P" rel=”stylesheet” /> /* styles.css */ |
1 2 3 4 | <span class="token selector">*</span> <span class="token punctuation">{</span> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">'Press Start 2P'</span><span class="token punctuation">,</span> cursive<span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Chúng ta đã tạo style cho vùng chứa trò chơi của mình và và chuẩn bị code logic nha.
The Board
Bảng trong Tetris bao gồm các ô, ô đó có được hiển thị hay không. Suy nghĩ đầu tiên của tôi là đại diện cho một ô có giá trị boolean tuy nhiên bạn đã nghĩ đến với màu sắc của ô sẽ hiển thị thế nào chưa? nó có nhiều giá trị mà phải không? Chúng ta có thể biểu diễn một ô trống với 0 và các màu sẽ được gán số.
Khái niệm tiếp theo là đại diện cho các hàng và cột của trò chơi. Ngay từ đầu chúng ta đã có thể hình dung ngay được nó thể sử mảng hai chiều (2D) rồi phải không, khái niện này nhập môn lập trình có lẽ ai cũng phải làm các bài luyện tập liên quan đến khái niệm này, và đây là lúc nó phát huy tác dụng.
Let go, bắt đầu code logic nào! Đầu tiền hãy tạo một hàm trả về một bảng trống với tất cả các ô được đặt thành 0.
1 2 3 4 | <span class="token function">getEmptyBoard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token builtin">Array</span><span class="token punctuation">.</span><span class="token keyword">from</span><span class="token punctuation">(</span><span class="token punctuation">{</span> length<span class="token operator">:</span> <span class="token constant">ROWS</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">Array</span><span class="token punctuation">(</span><span class="token constant">COLS</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">fill</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Khi bắt đầu trò chơi, thì tất nhiên là mình phải sử dụng hàm phía trên rồi, chúng ta khởi động bằng nút play, vậy đặt tên là play luôn nhé
1 2 3 4 5 | <span class="token function">play</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>board <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>boardService<span class="token punctuation">.</span><span class="token function">getEmptyBoard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">table</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>board<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Bằng cách sử dụng console.table, chúng ta thấy biểu diễn của mảng(bảng điều khiển chính của game)
Các tọa độ X và Y đại diện cho các ô của bảng. Bây giờ chúng ta đã có bảng, chúng ta hãy xem xét các khối hình(các mảnh).
Tetrominos
Một mảnh trong Tetris là một khối khối có thể xoay theo một trục ở 1 trạng thái. Chúng thường được gọi là tetrominos và có bảy kiểu và mỗi kiểu có một màu sắc khác nhau. Các khối này cũng không xa lạ gì với chúng ta, có lẽ nó lấy theo hình dạng của các chữ cái I, J, L, O, S, T và Z
Tôi biểu diễn tetromino J dưới dạng ma trận trong đó số hai đại diện cho các ô màu tôi thêm hàng số không để có tâm xoay xung quanh:
1 2 3 4 | [2, 0, 0], [2, 2, 2], [0, 0, 0]; |
Với các giá trị còn lại bạn hãy tự suy ra nhé, hãy nhớ hãy định nghĩa sao có mỗi khối sẽ phải có tâm xoay để lúc xoay hình được thuận tiện cũng như sử dụng nó để đại diện cho vị trí của khối đó.
1 2 3 4 5 6 7 | <span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">IPiece</span> <span class="token punctuation">{</span> x<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> y<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> color<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> shape<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Tôi muốn Piece class biết vị trí của nó trên bảng, màu sắc và hình dạng của nó. Vì vậy, để có thể vẽ chính nó trên bảng, nó cần một tham chiếu đến canvas context.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">Piece</span> <span class="token keyword">implements</span> <span class="token class-name">IPiece</span> <span class="token punctuation">{</span> x<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> y<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span> color<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> shape<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">constructor</span><span class="token punctuation">(</span><span class="token parameter"><span class="token keyword">private</span> ctx<span class="token operator">:</span> CanvasRenderingContext2D</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">spawn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">spawn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>color <span class="token operator">=</span> <span class="token string">'blue'</span><span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>shape <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// Position where the shape spawns.</span> <span class="token keyword">this</span><span class="token punctuation">.</span>x <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>y <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Để vẽ tetromino trên bảng, chúng ta lặp qua tất cả các ô của mảnh. Nếu giá trị trong ô lớn hơn 0, thì ta tô màu cho mảnh đó.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token function">draw</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span>fillStyle <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>color<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>shape<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">row<span class="token punctuation">,</span> y</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> row<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">value<span class="token punctuation">,</span> x</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>value <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// this.x & this.y = position on the board</span> <span class="token comment">// x & y position are the positions of the shape</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span><span class="token function">fillRect</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>x <span class="token operator">+</span> x<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>y <span class="token operator">+</span> y<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Lúc này, bạn có thể nhận thấy rằng các cạnh của mảnh dính với nhau thành một nguyên nhận là vì nó rất nhỏ, vì vậy chúng ta cần phải “thổi to nó lên”, tăng tỷ lệ nó theo kích thước khối như ở constan này BLOCK_SIZE
:
1 2 | <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span><span class="token function">scale</span><span class="token punctuation">(</span><span class="token constant">BLOCK_SIZE</span><span class="token punctuation">,</span> <span class="token constant">BLOCK_SIZE</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Ở bảng chính chúng ta cùng thử thể tạo và vẽ nó khi chúng ta nhấn nút play:
1 2 3 4 5 6 | <span class="token function">play</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>board <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>boardService<span class="token punctuation">.</span><span class="token function">getEmptyBoard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>piece <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Piece</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>piece<span class="token punctuation">.</span><span class="token function">draw</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Khối tetromino J màu xanh xuất hiện!
Với những chữ khác bạn hãy tự tạo rồi vẽ thử lên xem sao nhé
Sau khi có các mảnh rồi, hãy cũng sử dụng các keyboard event để điều khiển chúng nha
Keyboard input
Bây giờ, hãy xem cách chúng ta di chuyển các mảnh trên bảng qua hàm này
1 2 3 4 5 | <span class="token function">move</span><span class="token punctuation">(</span><span class="token parameter">p<span class="token operator">:</span> IPiece</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>x <span class="token operator">=</span> p<span class="token punctuation">.</span>x<span class="token punctuation">;</span> <span class="token keyword">this</span><span class="token punctuation">.</span>y <span class="token operator">=</span> p<span class="token punctuation">.</span>y<span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Hàm này với tham số truyền vào là một dạng IPiece và gán tọa độ của IPiece(argument đó) vào Piece hiện tại. Mới đầu có vẻ sẽ hơi kì quoặc phải không, hãy cùng xem mục đích của nó là gì nhé.
Trước hết là chúng ta phải map các phím(key) định dùng với mã của nó đã, để khi có một phím được bấm chúng ta biết được phím đó có điều khiển khối của chúng ta hay không
1 2 3 4 5 6 | <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">KEY</span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token constant">LEFT</span> <span class="token operator">=</span> <span class="token number">37</span><span class="token punctuation">;</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token constant">RIGHT</span> <span class="token operator">=</span> <span class="token number">39</span><span class="token punctuation">;</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token constant">DOWN</span> <span class="token operator">=</span> <span class="token number">40</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Như ở trên tôi có hàm move với một kiểu IPiece, tôi chưa nói mục đích là gì đúng không. Mục đích của tôi rất đợn giản, đó là: tôi sẽ tạo ra một bản sao của nó, sau đó tôi gán tọa độ mới mà khối đó đến vào bản sao đó, và truyền vào hàm move
kia, như vậy tọa độ của khối đó đã được thay đổi.
Note: Trong JavaScript, chúng ta có thể sử dụng tính năng shallow copying
để sao chép các kiểu dữ liệu nguyên thủy như số và chuỗi. Tuy nhiên với một object thì cơ chế nó có sự khác biệt, chắc hẵn ai làm một thời gian sẽ hiểu. Vì vậy ES6 cung cấp hai cơ chế shallow copy: Object.assign () và spread operator.
Ta sử dụng luôn spread operator nhé:
1 2 3 4 5 6 | moves <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token constant">KEY</span><span class="token punctuation">.</span><span class="token constant">LEFT</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>p<span class="token operator">:</span> IPiece<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token parameter">IPiece</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token operator">...</span>p<span class="token punctuation">,</span> x<span class="token operator">:</span> p<span class="token punctuation">.</span>x <span class="token operator">-</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token constant">KEY</span><span class="token punctuation">.</span><span class="token constant">RIGHT</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>p<span class="token operator">:</span> IPiece<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token parameter">IPiece</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token operator">...</span>p<span class="token punctuation">,</span> x<span class="token operator">:</span> p<span class="token punctuation">.</span>x <span class="token operator">+</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token constant">KEY</span><span class="token punctuation">.</span><span class="token constant">UP</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>p<span class="token operator">:</span> IPiece<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token parameter">IPiece</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token operator">...</span>p<span class="token punctuation">,</span> y<span class="token operator">:</span> p<span class="token punctuation">.</span>y <span class="token operator">+</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> |
Chúng ta có thể sử dụng như thế này để có được trạng thái mới mà không làm thay đổi phần ban đầu. Điều này quan trọng vì không phải lúc nào chúng ta cũng muốn chuyển sang một vị trí mới.
1 2 | <span class="token keyword">const</span> p <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>moves<span class="token punctuation">[</span>event<span class="token punctuation">.</span>key<span class="token punctuation">]</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>piece<span class="token punctuation">)</span><span class="token punctuation">;</span> |
đến đây mọi người thấy cực kì thuyết phục rồi chứ =))
Để nghe các sự kiện bàn phím, chúng ta có thể sử dụng HostListener decorator trong component bảng.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @<span class="token function">HostListener</span><span class="token punctuation">(</span><span class="token string">'window:keydown'</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">'$event'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token function">keyEvent</span><span class="token punctuation">(</span><span class="token parameter">event<span class="token operator">:</span> KeyboardEvent</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>moves<span class="token punctuation">[</span>event<span class="token punctuation">.</span>keyCode<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// If the keyCode exists in our moves stop the event from bubbling.</span> event<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Get the next state of the piece.</span> <span class="token keyword">const</span> p <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>moves<span class="token punctuation">[</span>event<span class="token punctuation">.</span>key<span class="token punctuation">]</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>piece<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Move the piece</span> <span class="token keyword">this</span><span class="token punctuation">.</span>piece<span class="token punctuation">.</span><span class="token function">move</span><span class="token punctuation">(</span>p<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Clear the old position before drawing</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span><span class="token function">clearRect</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span>canvas<span class="token punctuation">.</span>width<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>ctx<span class="token punctuation">.</span>canvas<span class="token punctuation">.</span>height<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Draw the new position.</span> <span class="token keyword">this</span><span class="token punctuation">.</span>piece<span class="token punctuation">.</span><span class="token function">draw</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Bây giờ chúng ta đang nghe các sự kiện trên bàn phím và nếu chúng ta nhấn mũi tên trái, phải hoặc mũi tên xuống, thì chúng ta có thể thấy phần di chuyển.
Cũng ổn rồi đấy, tuy nhiên, những mảnh ma đi xuyên tường không phải là điều chúng ta muốn. Chúng ta tiếp tục cải tiến nó nhé
Phát hiện va chạm
Việc phát hiện va chạm này không chỉ giải quyết vấn để chỉ ở case bên trên chúng ta vừa mắc phải mà nó còn ở nhiều case đặc biệt nữa. Thông qua lúc chơi chúng ta cũng đã nghĩ ra một vài case rồi. Cụ thể đó là:
- Lên đỉnh =)) (cao quá đụng sàn)
- Di chuyển sáng hai bên và va vào tường
- Chạm vào một khối(mảnh) khác trên bảng
- Lúc xoay dọc, ngang hình đó chạm vào tường hoặc khối khác
Ở phần trước chúng ta đã xác định được vị trí với khi chúng ta điều khiển khối bằng bàn phím, điều này cũng tương tụ với khi khối tự rơi(vì tự rơi cũng như ta bấm nút ‘xuống’ vậy). Vậy đơn giản chúng ta sẽ kiểm tra vị trí mới(nếu đủ điều kiện) trước, nếu nó hợp lệ thì mới chuyển sang nó.
Để kiểm tra sự va chạm, tôi lặp qua tất cả các khoảng trống trong bảng mà tetromino sẽ có thể đến. Nếu nó không phải là giá trị 0, thì chắc chắn nó đã có khối khác hoặc là tường rồi. Arraut method phù hợp nhất cho điều này là every()
. Với nó, chúng ta có thể kiểm tra xem tất cả các phần tử trong mảng có pass qua các logic mà chúng ta cần hay không hay không. Chúng ta cần tính toán mọi ô trong khối(mảnh) xem nó có phải là vị trí hợp lệ hay không, hãy cùng xem đoạn code này nhé:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <span class="token function">valid</span><span class="token punctuation">(</span>p<span class="token operator">:</span> IPiece<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">boolean</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> p<span class="token punctuation">.</span>shape<span class="token punctuation">.</span><span class="token function">every</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">row<span class="token punctuation">,</span> dy</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> row<span class="token punctuation">.</span><span class="token function">every</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">value<span class="token punctuation">,</span> dx</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">let</span> x <span class="token operator">=</span> p<span class="token punctuation">.</span>x <span class="token operator">+</span> dx<span class="token punctuation">;</span> <span class="token keyword">let</span> y <span class="token operator">=</span> p<span class="token punctuation">.</span>y <span class="token operator">+</span> dy<span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token punctuation">(</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">isEmpty</span><span class="token punctuation">(</span>value<span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">insideWalls</span><span class="token punctuation">(</span>x<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">aboveFloor</span><span class="token punctuation">(</span>y<span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Bằng cách sử dụng phương pháp này trước khi di chuyển khối sang vị trí mới, chúng ta đảm bảo rằng khối không di chuyển đến bất kỳ nơi nào nó không thể đến:
1 2 3 4 | if (this.service.valid(p)) { this.piece.move(p); } |
Hàm này mình để trong service, còn để dâu bạn hãy tạo nhé. Hoặc làm them mình cũng được
Hãy cùng kiểm tra tại nhé
Rồi, có vẻ đã ổn rồi đấy, không còn hiện tượng đi xuyên tường nữa rồi. Bây giờ còn chúng ta sẽ code tiếp để khi cũng ta cho khối rơi khi điều khiển, nó có thể dừng lại được khi có vật cản
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">KEY</span> <span class="token punctuation">{</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token constant">SPACE</span> <span class="token operator">=</span> <span class="token number">32</span><span class="token punctuation">;</span> <span class="token comment">// ...</span> <span class="token punctuation">}</span> moves <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token constant">KEY</span><span class="token punctuation">.</span><span class="token constant">SPACE</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>p<span class="token operator">:</span> IPiece<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token parameter">IPiece</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token operator">...</span>p<span class="token punctuation">,</span> y<span class="token operator">:</span> p<span class="token punctuation">.</span>y <span class="token operator">+</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token comment">// ...</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>keyCode <span class="token operator">===</span> <span class="token constant">KEY</span><span class="token punctuation">.</span><span class="token constant">SPACE</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>service<span class="token punctuation">.</span><span class="token function">valid</span><span class="token punctuation">(</span>p<span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>board<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>piece<span class="token punctuation">.</span><span class="token function">move</span><span class="token punctuation">(</span>p<span class="token punctuation">)</span><span class="token punctuation">;</span> p <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>moves<span class="token punctuation">[</span><span class="token constant">KEY</span><span class="token punctuation">.</span><span class="token constant">DOWN</span><span class="token punctuation">]</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>piece<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Xử lý xoay
…
Phần này mình sẽ tiếp tục ở phần sau các bạn nhé bài này cũng hơi dài rồi
Phần kế tiếp mình sẽ tiếp tục với
- Xử lý xoay
- Cách để ramdom một khối
- Game loop
- Timer
- Ăn điểm
- …
Nói chung là sẽ hoàn thiện trò chơi nhé. Cảm ơn các bạn đã theo dõi
Tác giả
Bài biết được dịch từ : https://medium.com/angular-in-depth/game-development-tetris-in-angular-64ef96ce56f7 của tác giả Michael Karén
.
Hãy chơi thử game sau khi hoàn thiện ở đây và để lại feedback nhé