Hoài Cổ Với Game Tetris Lập Tình Bằng Java
Tetris là game huyền thoại mà các thế hệ 8x, 9x đều biết được chơi. Từ những năm 2000, Những máy chơi game đen trắng bùng nổ, xuất hiện khắp mọi nơi, hầu như trẻ em thời điểm đấy ai cũng master thể loại Game này.
Sau đây, mình sẽ hướng dẫn các bạn làm game Tetris bằng ngôn ngữ Java
.
Chuẩn bị
- Một file ảnh block 7 màu (nếu có). Bạn có thể tham khảo của mình tại đây
- Cài đặt
Visual Studio Code
tại đây - Cài đặt
Java 8
trên VSCode tại đây
Tiến hành
Mình sẽ tạo 2 folder gổm src
(chứa source code) và textures
(chứa ảnh)
Tạo một file ảnh gồm 7 khối màu
File ảnh kích thước là 210x30
nghĩa là ảnh gồm 7 khối, mỗi khối kích thước 30x30
. Mình phóng to hình cho các bạn dễ quan sát. Bạn có thể tải từ source code của mình file ảnh trên hoặc tự tạo cho mình một khối ảnh 7 màu như sau:
- Lấy mã hex hoặc mã RGB màu của 7 màu rainbow trên từ trang schemecolor
- Sử dụng thư viện
pillow
củaPython
vẽ các khối hình vuông cạnh nhau rồi lưu vào folder textures
from PIL import Image, ImageDraw
im = Image.new('RGB', (210, 30), (0, 0, 0))
draw = ImageDraw.Draw(im)
draw.rectangle((0, 0, 30, 30), fill=(246, 0, 0))
draw.rectangle((30, 0, 60, 30), fill=(255, 140, 0))
draw.rectangle((60, 0, 90, 30), fill=(255, 238, 0))
draw.rectangle((90, 0, 120, 30), fill=(77, 233, 76))
draw.rectangle((120, 0, 150, 30), fill=(55, 131, 255))
draw.rectangle((150, 0, 180, 30), fill=(72, 21, 170))
draw.rectangle((180, 0, 210, 30), fill=(255, 26, 206))
im.save('textures/tiles.png', quality=100)
Chia lớp
Mình chia đơn giản thành 3 phần:
- Lớp
Window
: Tạo cửa số chính, các thanh ngang trên, kích thước cửa số. Chạy cửa số chính, load Board game. - Lớp
Board
: Chia các block ảnh màu, lưu thành 7 shape như trong game Tetris, random shape, vẽ các đường bảng, … - Lớp
Shape
: Tập hợp các thuộc tính của Shape, render Shape, cập nhập Shape sau mỗi lần xuống, biến đối Shape, thao tác phím với Shape, …
Mình sẽ đi từng bước thực hiện game Tetris như sau:
Thiết lập Window Game
Window có kích thước 306x629
. Window có kích thước này là vì phải trừ đi vài pixcel 2 bên lề trái phải và trên dưới để ta được bên trong kích thước Board game sẽ là 300x600
. Nút Close được thêm vào, không thêm nút Resize.
Bây giờ tạo thêm class Board rồi thêm Board vào trong Window để load game.
import javax.swing.JFrame;
/**
* Window
*/
public class Window {
public static final int WIDTH = 306, HEIGHT = 629;
private JFrame window;
private Board board;
public Window(){
window = new JFrame("Tetris Game");
window.setSize(WIDTH, HEIGHT);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setResizable(false);
window.setLocationRelativeTo(null);
board = new Board();
window. add(board);
window.addKeyListener(board);
window.setVisible(true);
}
public static void main(String[] args) {
new Window();
}
}
Vẽ Board game
Với kích thước Board game là 300x600
, ta chia bề rộng thành 10
phần, bề ngang thành 20
phần. Kẻ các đường màu đen phân chia tạo thành Board.
private final int blockSize = 30;
private final int boardWidth = 10, boardHeight = 20;
public void paintComponent(Graphics g) {
super.paintComponent(g);
for (int i = 0; i < boardHeight; i++) {
g.drawLine(0, i * blockSize, boardWidth * blockSize, i * blockSize);
}
for (int j = 0; j < boardWidth; j++) {
g.drawLine(j * blockSize, 0, j * blockSize, boardHeight * blockSize);
}
}
Load ảnh, chia Block và tạo hình các Shape
Sau khi có Board game, ta sẽ load bức ảnh tiles.png chứa các Block
7 màu như trên. Tiến hành chia bức ảnh thành 7 block với 7 màu kích thước 30x30
.
try {
blocks = ImageIO.read(Board.class.getResource("tiles.png"));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Tiếp theo, ta tạo Shape lưu một số thông tin hiện tại như khối màu, vị trí. Sau đó tạo hàm render để vẽ các khối Block cùng màu lên Board game để tạo thành hình các Shape.
private BufferedImage block;
private int[][] coords;
private Board board;
public void render(Graphics g){
for (int row = 0; row < coords.length; row++) {
for (int col = 0; col < coords[row].length; col++) {
if(coords[row][col] != 0)
g.drawImage(block, col*board.getBlockSize() + x*board.getBlockSize(), row * board.getBlockSize() + y*board.getBlockSize(), null);
}
}
}
Các Shape gồm 7 tạo hình gồm hình chữ O
, I
, S
, Z
, L
, J
, T
. Các chia là chúng ta sử dụng một ma trận kích thước nhỏ, đánh dấu các ô số 1 nghĩa là chứa block, số 0 là không chứa block. Các ô số 1 xếp thành hình các Shape rồi ta render
cho Shape là được.
shapes[0] = new Shape(blocks.getSubimage(0, 0, blockSize, blockSize), new int[][] { { 1, 1, 1, 1 } // I shape
}, this, 1);
shapes[1] = new Shape(blocks.getSubimage(blockSize, 0, blockSize, blockSize),
new int[][] { { 1, 1, 0 }, { 0, 1, 1 } // Z shape
}, this, 2);
shapes[2] = new Shape(blocks.getSubimage(blockSize * 2, 0, blockSize, blockSize),
new int[][] { { 0, 1, 1 }, { 1, 1, 0 } // S shape
}, this, 3);
shapes[3] = new Shape(blocks.getSubimage(blockSize * 3, 0, blockSize, blockSize),
new int[][] { { 1, 1, 1 }, { 0, 0, 1 } // J shape
}, this, 4);
shapes[4] = new Shape(blocks.getSubimage(blockSize * 4, 0, blockSize, blockSize),
new int[][] { { 1, 1, 1 }, { 1, 0, 0 } // L shape
}, this, 5);
shapes[5] = new Shape(blocks.getSubimage(blockSize * 5, 0, blockSize, blockSize),
new int[][] { { 1, 1, 1 }, { 0, 1, 0 } // T shape
}, this, 6);
shapes[6] = new Shape(blocks.getSubimage(blockSize * 6, 0, blockSize, blockSize),
new int[][] { { 1, 1 }, { 1, 1 } // O shape
}, this, 7);
Tạo chức năng cho Shape
Sau khi có các Shape việc tiếp theo cần làm là tạo các Event Key tạo tác các Shape. Xử lý va chạm với border của Board.
Bình thường, Shape sẽ tự động di chuyển từ trên xuống dưới. Key thao tác gồm phím trái, phải để di chuyển Shape sang trái hay phải. Phím xuống dưới dùng để tăng tốc độ cho Shape. Bình thường thì deltaX = 0 còn deltaX = -1 hoặc 1 sẽ di chuyển shape sang trái hoặc phải. Ở board.java:
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
if(e.getKeyCode() == KeyEvent.VK_LEFT){
currentShape.setDeltaX(-1);
}
if(e.getKeyCode() == KeyEvent.VK_RIGHT){
currentShape.setDeltaX(1);
}
}
Khi gặp các border trái phải, ta sẽ hủy các Event Key. Còn gặp border dưới sẽ dừng lại.
public void update(){
time += System.currentTimeMillis() - lastTime;
lastTime = System.currentTimeMillis();
if(collision){
for (int row = 0; row < coords.length; row++) {
for (int col = 0; col < coords[row].length; col++) {
if(coords[row][col] != 0){
board.getBoard()[y + row][x + col] = color;
}
}
}
checkLine();
board.setNextShape();
}
if(!(x + deltaX + coords[0].length > 10) && !(x + deltaX < 0)){
for (int row = 0; row < coords.length; row++) {
for (int col = 0; col < coords[row].length; col++) {
if(coords[row][col] != 0){
if(board.getBoard()[y + row][x + deltaX + col] != 0){
moveX = false;
}
}
}
}
if(moveX)
x += deltaX;
}
if(!(y + 1 + coords.length > 20)){
for (int row = 0; row < coords.length; row++) {
for (int col = 0; col < coords[row].length; col++) {
if(coords[row][col] != 0){
if(board.getBoard()[y + row + 1][col + x] != 0){
collision = true;
}
}
}
}
if(time > currentSpeed){
y++;
time = 0;
}
} else {
collision = true;
}
deltaX = 0;
moveX = true;
}
Xoay Shape
Ta tạo thao tác phím lên dùng để xoay Shape. Như trên, mỗi Shape sẽ được định nghĩa là một ma trận gồm các phần từ 0 và 1. Do đó, Shape sẽ được xoay theo công thức: chuyển vị ma trận (Transpose) sau đó ta đảo ngược ma trận này (Reverse).
public void rotate(){
if(collision)
return;
int[][] rotateMatrix = null;
rotateMatrix = getTranspose(coords);
rotateMatrix = getReverseMatrix(rotateMatrix);
if(x + rotateMatrix[0].length > 10 || y + rotateMatrix.length > 20)
return;
for (int row = 0; row < rotateMatrix.length; row++) {
for (int col = 0; col < rotateMatrix[0].length; col++) {
if(board.getBoard()[y + col][x + col] != 0){
return;
}
}
}
coords = rotateMatrix;
}
private int[][] getTranspose(int[][] matrix){
int[][] newMatrix = new int[matrix[0].length][matrix.length];
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
newMatrix[j][i] = matrix[i][j];
}
}
return newMatrix;
}
private int[][] getReverseMatrix(int[][] matrix){
int middle = matrix.length/2;
for(int i = 0; i < middle; i++){
int[] m = matrix[i];
matrix[i] = matrix[matrix.length - i - 1];
matrix[matrix.length - i - 1] = m;
}
return matrix;
}
Tiếp theo ta tiếp tục xử lý va chạm đối với Shape sau khi xoay. Vì sau khi xoay số hàng và cột cũng thay đổi theo.
Tạo Shape mới và Xử lý va chạm giữa các Shape 
Sau khi ta đã có các thao tác đối với một Shape. Tiếp theo ta sẽ tạo ra một Shape mới sau khi Shape cũ va chạm với đáy hoặc va chạm với Shape khác. Mỗi Shape được tạo ra bằng hàm random và được tạo ra ngay giữa hàng trên cùng.
public void setNextShape(){
int index = (int)(Math.random()*shapes.length);
Shape newShape = new Shape(shapes[index].getBlock(), shapes[index].getCoords(), this, shapes[index].getColor());
currentShape = newShape;
for (int row = 0; row < currentShape.getCoords().length; row++) {
for (int col = 0; col < currentShape.getCoords()[row].length; col++) {
if(currentShape.getCoords()[row][col] != 0){
if(board[row][col + 3] != 0)
gameOver = true;
}
}
}
}
Các Shape va chạm với nhau ở trạng thái động (xoay Shape) thì sẽ xảy ra lỗi do đó, khi thấy trạng thái xoay mới của Shape va chạm với Shape cũ thì ta dừng chức năng xoay để đảm bảo Shape va chạm hợp lí.
public void update(){
time += System.currentTimeMillis() - lastTime;
lastTime = System.currentTimeMillis();
if(collision){
for (int row = 0; row < coords.length; row++) {
for (int col = 0; col < coords[row].length; col++) {
if(coords[row][col] != 0){
board.getBoard()[y + row][x + col] = color;
}
}
}
checkLine();
board.setNextShape();
}
if(!(x + deltaX + coords[0].length > 10) && !(x + deltaX < 0)){
for (int row = 0; row < coords.length; row++) {
for (int col = 0; col < coords[row].length; col++) {
if(coords[row][col] != 0){
if(board.getBoard()[y + row][x + deltaX + col] != 0){
moveX = false;
}
}
}
}
if(moveX)
x += deltaX;
}
if(!(y + 1 + coords.length > 20)){
for (int row = 0; row < coords.length; row++) {
for (int col = 0; col < coords[row].length; col++) {
if(coords[row][col] != 0){
if(board.getBoard()[y + row + 1][col + x] != 0){
collision = true;
}
}
}
}
if(time > currentSpeed){
y++;
time = 0;
}
} else {
collision = true;
}
deltaX = 0;
moveX = true;
}
Ghi điểm
Khi một hàng được điền tất cả các ô trống nó sẽ tự động mất đi và đấy các hàng ở trên xuống dưới. Bước này được gọi là bước ghi điểm.
private void checkLine(){
int height = board.getBoard().length - 1;
for (int i = height; i > 0; i--) {
int count = 0;
for (int j = 0; j < board.getBoard()[0].length; j++) {
if(board.getBoard()[i][j] != 0){
count++;
}
board.getBoard()[height][j] = board.getBoard()[i][j];
}
if(count < board.getBoard()[0].length)
height--;
}
}
Game Over 
Game kết thúc khi không thể tạo ra Shape mới. Tức là khi tạo ra một Shape mới nếu Shape mới có ô đè lên Shape cũ thì game dừng lại.
public void update(){
currentShape.update();
if(gameOver){
timer.stop();
}
}
public void setNextShape(){
int index = (int)(Math.random()*shapes.length);
Shape newShape = new Shape(shapes[index].getBlock(), shapes[index].getCoords(), this, shapes[index].getColor());
currentShape = newShape;
for (int row = 0; row < currentShape.getCoords().length; row++) {
for (int col = 0; col < currentShape.getCoords()[row].length; col++) {
if(currentShape.getCoords()[row][col] != 0){
if(board[row][col + 3] != 0)
gameOver = true;
}
}
}
}
Source code
Một game Tetris cơ bản đã xong. Mình để lại cho các bạn Full code để các bạn tham khảo nhé.
Phát triển
Những phần khó nhất của game Tetris đã hoàn thành. Từ bài viết chúng ta có thể cải tiến thêm một số chức năng như sau:
- Thêm
background
(Chỉ cần tải ảnh và thêm ảnh vào phía sau window) - Tăng độ khó cho game
level
(Ta có thể tăng độ khó cho game bằng 2 cách: tăng độ khó khi score đạt một mức nào đó hoặc tăng độ khó sau một khoảng thời gian nào đó) - Tính điểm số
score
(Ta có thể tính điểm bằng công thứctotal += score_line*level
) - Luyện nào
brain
. Một vài ý tưởng khoai chuối khác như chỉnh tốc độ speed thất thường cho Shape. Tạo 2 Tetris game cạnh nhau để chơi. Thêm chướng ngại vật,… - …
Cảm ơn các bạn đã đọc và quan tâm. Nếu có ý kiến gì thêm hãy để lại bình luận bên dưới.
Chào quyết thắng