Membuat Game Tetris dengan Java

Membuat Game Tetris dengan Java
Game Tetris merupakan salah satu dari beberapa game computer yang terpopular yang pernah ciptakan. Game orisinalnya sih didisain dan diprogram oleh seorang programmer Russia yang bernama Alexey Pajitnov pada tahun 1985. Sejak saat itu, Tetris tersedia hampir pada seluruh platform komputer dengan berbagai variasinya.
Tetris merupakan sebutan bagi permainan yang ada block puzzle runtuh. Pada game ini, saya mempunyai 7 bentuk berbeda yang bernama tetrominoes. S-shape, Z-shape, T-shape, L-shape, Line-shape, Mirrored L-shape and a Square-shape. Masing-masing dari bentuk-bentuk ini terbuat dari 4 balok. Bentuk-bentuk tersebut berjatuhan ke papan game (board). Cara permainan tertris ini adalah dengan cara memindahkan dan memutarkan bentuk-bentuk tersebut, sehingga mereka bias masuk dengan benar. Jika kita benar mengatur shape2 tersebut baris demi baris, garis tersebut akan hancur dan kita mendapatkan skor. Kita memainkan game tetris sampai habis spes tetrisnya.
Tetris 1

Gambar : Tetrominoes
Pengembangan Program
Kita tidak membutuhkan gambar untuk permainan tetris kita, kita menggambar tetrominoes menggunakan Swing drawing API. Di belakang semua game komputer, terdapat sebuah model matematis. Begitu pula di dalam tetris ini.
Beberapa ide di balik game tetris.
• Kita menggunakan sebuah class Timer untuk membuat sebuah lingkaran game.
• Menggambar Tetrominoes
• Bentuk-bentuk tersebut berjalan melalui kotak perkotak (bukan pixel by pixel)
• Secara matematis sebuah papan merupakan daftar angka-angka yang sederhana.
Saya telah sedikit menyederhanakan game tersebut, sehingga menjadi lebih mudah untuk difahami. Game tersebut berjalan dengan seketika, setelah diluncurkan / dijalankan. Kita bisa menghentikan sejenak (pause) game dengan cara memencet atau menekan tombol P di keyboard. Tombol spasi akan menurunkan tetris seketika ke bawah. Tombol D akan menurukan tetrotinoes sebaris ke bawah. (Itu bias digunakan untuk mempercepat (speed up) jatuhnya sedikit) Game berjalan pada kecepatan yang constant, tidak ada akselerasi yang diterapkan. Scorenya didapatkan dari jumlah baris, yang telah kita hilangkan.

Tetris.java

package tetris;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class Tetris extends JFrame {

    JLabel statusbar;

    public Tetris() {

        statusbar = new JLabel(" 0");
        add(statusbar, BorderLayout.SOUTH);
        Board board = new Board(this);
        add(board);
        board.start();

        setSize(200, 400);
        setTitle("Tetris");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
   }

   public JLabel getStatusBar() {
       return statusbar;
   }

    public static void main(String[] args) {

        Tetris game = new Tetris();
        game.setLocationRelativeTo(null);
        game.setVisible(true);

    }
}

Di dalam file Tetris.java, saya telah men-set up game tersebut. Kami membuat sebuah board (papan permainan) dalam game tersebut. Kami juga membuat statusbar.
board.start();
Method start() mengindikasikan untuk memulai game Tetris. Dengan seketika, setelah window muncul dalam layar.

Shape.java
===========

package tetris;

import java.util.Random;
import java.lang.Math;

public class Shape {

enum Tetrominoes { NoShape, ZShape, SShape, LineShape,
 TShape, SquareShape, LShape, MirroredLShape };

private Tetrominoes pieceShape;
 private int coords[][];
 private int[][][] coordsTable;

public Shape() {

coords = new int[4][2];
 setShape(Tetrominoes.NoShape);

}

public void setShape(Tetrominoes shape) {

coordsTable = new int[][][] {
 { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
 { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } },
 { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } },
 { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } },
 { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } },
 { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } },
 { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } },
 { { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }
 };

for (int i = 0; i < 4 ; i++) {
 for (int j = 0; j < 2; ++j) {
 coords[i][j] = coordsTable[shape.ordinal()][i][j];
 }
 }
 pieceShape = shape;

}

private void setX(int index, int x) { coords[index][0] = x; }
 private void setY(int index, int y) { coords[index][1] = y; }
 public int x(int index) { return coords[index][0]; }
 public int y(int index) { return coords[index][1]; }
 public Tetrominoes getShape() { return pieceShape; }

public void setRandomShape()
 {
 Random r = new Random();
 int x = Math.abs(r.nextInt()) % 7 + 1;
 Tetrominoes[] values = Tetrominoes.values();
 setShape(values[x]);
 }

public int minX()
 {
 int m = coords[0][0];
 for (int i=0; i < 4; i++) {
 m = Math.min(m, coords[i][0]);
 }
 return m;
 }

public int minY()
 {
 int m = coords[0][1];
 for (int i=0; i < 4; i++) {
 m = Math.min(m, coords[i][1]);
 }
 return m;
 }

public Shape rotateLeft()
 {
 if (pieceShape == Tetrominoes.SquareShape)
 return this;

Shape result = new Shape();
 result.pieceShape = pieceShape;

for (int i = 0; i < 4; ++i) {
 result.setX(i, y(i));
 result.setY(i, -x(i));
 }
 return result;
 }

public Shape rotateRight()
 {
 if (pieceShape == Tetrominoes.SquareShape)
 return this;

Shape result = new Shape();
 result.pieceShape = pieceShape;

for (int i = 0; i < 4; ++i) {
 result.setX(i, -y(i));
 result.setY(i, x(i));
 }
 return result;
 }
 }

Class Shape menyediakan informasi mengenai bagian tetris (tertrominoes).

enum Tetrominoes { NoShape, ZShape, SShape, LineShape,
TShape, SquareShape, LShape, MirroredLShape };

Tetrominoes enum bertugas membentuk ketujuh bentuk tetris. Plus bentuk kosong yang dinamakan di sini sebagai NoShape.

public Shape() {

coords = new int[4][2];
setShape(Tetrominoes.NoShape);

}

Ini adalah constructor dari class Shape. Array coords bertugas dalam menentukan coordinates sebnarnya dari sebuah potongan Tetris.

coordsTable = new int[][][] {
{ { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } },
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } },
{ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } },
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } },
{ { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } },
{ { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } },
{ { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }
};

Array coordsTable bertugas untuk menentukan semuka kemungkinan nilai kordinat dari potongan tetris kita. Ini adalah template dari nilai koorsinat yang biasa.

for (int i = 0; i &lt; 4 ; i++) {
for (int j = 0; j &lt; 2; ++j) {
coords[i][j] = coordsTable[shape.ordinal()][i][j];
}
}

Di sini kita menempatkan sebuah baris nilai koordinat dari coordsTable kepada coords array potongan tetris. Lihat kembali keguanaan dari metode ordinal(). Di dalam C++, sebuah tipe data enum biasanya berupa bilangan integer. Tidak seperti di C++, Java enums merupakan full class dan method ordinal() mengembalikan posisi sekarang dari tipe enum di dalam objek enum.
Gambar berikut mudah-mudahan sedikit bisa membantu untuk menjelaskan nilai koordinat. array coords menyimpan koordinat-koordinat potongan tetris. Sebagai contoh, nomor-nomor { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } , mewakili sebuah rotasi dari S-shape. Diagram berikut mengilustrasikan bentuk tersebut.
Koordinat
Gambar : Koordinat

public Shape rotateLeft()
{
if (pieceShape == Tetrominoes.SquareShape)
return this;

Shape result = new Shape();
result.pieceShape = pieceShape;

for (int i = 0; i &lt; 4; ++i) {
result.setX(i, y(i));
result.setY(i, -x(i));
}
return result;
}

Code ini merotasi potongan tetris ke sebelah kiri. Bentuk kotak tidak perlu dirotasi. Itulah alasannya mengapa kita hanya mengembalikan referensi ke objek sekarang saja. Dengan melihat gambar sebelumnya mudah-mudah bias membantu dalam memahami tentang rotasi tersebut.

Board.java
===============

package tetris;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

import tetris.Shape.Tetrominoes;

public class Board extends JPanel implements ActionListener {

final int BoardWidth = 10;
final int BoardHeight = 22;

Timer timer;
boolean isFallingFinished = false;
boolean isStarted = false;
boolean isPaused = false;
int numLinesRemoved = 0;
int curX = 0;
int curY = 0;
JLabel statusbar;
Shape curPiece;
Tetrominoes[] board;

public Board(Tetris parent) {

setFocusable(true);
curPiece = new Shape();
timer = new Timer(400, this);
timer.start();

statusbar = parent.getStatusBar();
board = new Tetrominoes[BoardWidth * BoardHeight];
addKeyListener(new TAdapter());
clearBoard();
}

public void actionPerformed(ActionEvent e) {
if (isFallingFinished) {
isFallingFinished = false;
newPiece();
} else {
oneLineDown();
}
}

int squareWidth() { return (int) getSize().getWidth() / BoardWidth; }
int squareHeight() { return (int) getSize().getHeight() / BoardHeight; }
Tetrominoes shapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }

public void start()
{
if (isPaused)
return;

isStarted = true;
isFallingFinished = false;
numLinesRemoved = 0;
clearBoard();

newPiece();
timer.start();
}

private void pause()
{
if (!isStarted)
return;

isPaused = !isPaused;
if (isPaused) {
timer.stop();
statusbar.setText("paused");
} else {
timer.start();
statusbar.setText(String.valueOf(numLinesRemoved));
}
repaint();
}

public void paint(Graphics g)
{
super.paint(g);

Dimension size = getSize();
int boardTop = (int) size.getHeight() - BoardHeight * squareHeight();

for (int i = 0; i &lt; BoardHeight; ++i) {
for (int j = 0; j &lt; BoardWidth; ++j) {
Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
if (shape != Tetrominoes.NoShape)
drawSquare(g, 0 + j * squareWidth(),
boardTop + i * squareHeight(), shape);
}
}

if (curPiece.getShape() != Tetrominoes.NoShape) {
for (int i = 0; i &lt; 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); drawSquare(g, 0 + x * squareWidth(), boardTop + (BoardHeight - y - 1) * squareHeight(), curPiece.getShape()); } } } private void dropDown() { int newY = curY; while (newY &gt; 0) {
if (!tryMove(curPiece, curX, newY - 1))
break;
--newY;
}
pieceDropped();
}

private void oneLineDown()
{
if (!tryMove(curPiece, curX, curY - 1))
pieceDropped();
}

private void clearBoard()
{
for (int i = 0; i &lt; BoardHeight * BoardWidth; ++i)
board[i] = Tetrominoes.NoShape;
}

private void pieceDropped()
{
for (int i = 0; i &lt; 4; ++i) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
board[(y * BoardWidth) + x] = curPiece.getShape();
}

removeFullLines();

if (!isFallingFinished)
newPiece();
}

private void newPiece()
{
curPiece.setRandomShape();
curX = BoardWidth / 2 + 1;
curY = BoardHeight - 1 + curPiece.minY();

if (!tryMove(curPiece, curX, curY)) {
curPiece.setShape(Tetrominoes.NoShape);
timer.stop();
isStarted = false;
statusbar.setText("game over");
}
}

private boolean tryMove(Shape newPiece, int newX, int newY)
{
for (int i = 0; i &lt; 4; ++i) {
int x = newX + newPiece.x(i);
int y = newY - newPiece.y(i);
if (x &lt; 0 || x &gt;= BoardWidth || y &lt; 0 || y &gt;= BoardHeight)
return false;
if (shapeAt(x, y) != Tetrominoes.NoShape)
return false;
}

curPiece = newPiece;
curX = newX;
curY = newY;
repaint();
return true;
}

private void removeFullLines()
{
int numFullLines = 0;

for (int i = BoardHeight - 1; i &gt;= 0; --i) {
boolean lineIsFull = true;

for (int j = 0; j &lt; BoardWidth; ++j) {
if (shapeAt(j, i) == Tetrominoes.NoShape) {
lineIsFull = false;
break;
}
}

if (lineIsFull) {
++numFullLines;
for (int k = i; k &lt; BoardHeight - 1; ++k) {
for (int j = 0; j &lt; BoardWidth; ++j) board[(k * BoardWidth) + j] = shapeAt(j, k + 1); } } } if (numFullLines &gt; 0) {
numLinesRemoved += numFullLines;
statusbar.setText(String.valueOf(numLinesRemoved));
isFallingFinished = true;
curPiece.setShape(Tetrominoes.NoShape);
repaint();
}
}

private void drawSquare(Graphics g, int x, int y, Tetrominoes shape)
{
Color colors[] = { new Color(0, 0, 0), new Color(204, 102, 102),
new Color(102, 204, 102), new Color(102, 102, 204),
new Color(204, 204, 102), new Color(204, 102, 204),
new Color(102, 204, 204), new Color(218, 170, 0)
};

Color color = colors[shape.ordinal()];

g.setColor(color);
g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);

g.setColor(color.brighter());
g.drawLine(x, y + squareHeight() - 1, x, y);
g.drawLine(x, y, x + squareWidth() - 1, y);

g.setColor(color.darker());
g.drawLine(x + 1, y + squareHeight() - 1,
x + squareWidth() - 1, y + squareHeight() - 1);
g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
x + squareWidth() - 1, y + 1);

}

class TAdapter extends KeyAdapter {
public void keyPressed(KeyEvent e) {

if (!isStarted || curPiece.getShape() == Tetrominoes.NoShape) {
return;
}

int keycode = e.getKeyCode();

if (keycode == 'p' || keycode == 'P') {
pause();
return;
}

if (isPaused)
return;

switch (keycode) {
case KeyEvent.VK_LEFT:
tryMove(curPiece, curX - 1, curY);
break;
case KeyEvent.VK_RIGHT:
tryMove(curPiece, curX + 1, curY);
break;
case KeyEvent.VK_DOWN:
tryMove(curPiece.rotateRight(), curX, curY);
break;
case KeyEvent.VK_UP:
tryMove(curPiece.rotateLeft(), curX, curY);
break;
case KeyEvent.VK_SPACE:
dropDown();
break;
case 'd':
oneLineDown();
break;
case 'D':
oneLineDown();
break;
}

}
}
}

Akhirnya, kita sudah memiliki file Board.java. Di sinilah di mana logic game ditempatkan.

...
isFallingFinished = false;
isStarted = false;
isPaused = false;
numLinesRemoved = 0;
curX = 0;
curY = 0;
...

Kita menginitialisasi beberapa variable yang penting. Variable isFallingFinished mementukan, jika bentuk tetris telah jatuh dan kemudian kita perlu membuat bentuk yang baru. Variable numLinesRemoved menghitung jumlah baris yang telah kita buang (selesaikan) sejauh ini. Variable curX and curY menentukan posisi sebenarnya dari bentuk tetris yang telah jatuh.

setFocusable(true);

Kita harus memanggil secara eksplisit metod setFocusable(). Mulai dari sekarang, board mempunyai input dari keyboard.

timer = new Timer(400, this);
timer.start();

Objek Timer menyalakan satu event atau lebih setelah delay tertentu. Dalam kasus kita ini, timer memanggil method
actionPerformed() setiap 400 ms.

public void actionPerformed(ActionEvent e) {
if (isFallingFinished) {
isFallingFinished = false;
newPiece();
} else {
oneLineDown();
}
}

Method actionPerformed() akan mengecek jika telah jatuh. Jika begitu, bentuk yang baru diciptakan kembali. Jika tidak, potongan tetris yang jatuh tersebut berada pada baris selanjutnya.
Di dalam method paint(), kita menggambar semua objek dalam board. Metode painting tersebut mempunyai 2 langkah.

for (int i = 0; i &lt; BoardHeight; ++i) {
for (int j = 0; j &lt; BoardWidth; ++j) {
Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
if (shape != Tetrominoes.NoShape)
drawSquare(g, 0 + j * squareWidth(),
boardTop + i * squareHeight(), shape);
}
}

Pada langkah awal kita paint semua shape, atau sisa dari shape, yang telah didrop keu bawah board. Semua kotak tersimpan di array board. Kita mengaksesnya melalui method shapeAt().

if (curPiece.getShape() != Tetrominoes.NoShape) {
for (int i = 0; i &lt; 4; ++i) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
drawSquare(g, 0 + x * squareWidth(), boardTop + (BoardHeight - y - 1) * squareHeight(), curPiece.getShape());
}
}

Pada langkah yang kedua, kita paint potongan tetris yang sebenarnya.

private void dropDown() { int newY = curY; while (newY &gt; 0) {
if (!tryMove(curPiece, curX, newY - 1))
break;
--newY;
}
pieceDropped();
}

Jika kita menekan tombol spasi, potongan tetris akan jatuh ke bawah. Kita mencoba dengan mudah untuk menjatuhkan ke bawah hingga mencapai bagian bawah.

private void clearBoard()
{
for (int i = 0; i &lt; BoardHeight * BoardWidth; ++i)
board[i] = Tetrominoes.NoShape;
}

Method clearBoard() mengisi board dengan NoSpapes. Ini nanti akan kita gunakan untuk sebagai deteksi tambrakan / tubrukan.

private void pieceDropped()
{
for (int i = 0; i &lt; 4; ++i) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
board[(y * BoardWidth) + x] = curPiece.getShape();
}

removeFullLines();

if (!isFallingFinished)
newPiece();
}

Method pieceDropped() menempatkan bagian yang jatuh tersebut ke array board. Sekali lagi, board bertugas memegang semua potongan kubus dan sisa dari potongan yang telah selesai mendarat di board. Ketika potongna tersebut telah selesai jatuh, saatnya mengecek, jika kita bisa membuang beberapa baris dari board tersebut. Ini adalah tugas dari method removeFullLines(). Kemudian kita membuat potongan yang baru. Lebih tepatnya lagi mah, kita berusaha membuat potongan yang baru.

private void newPiece()
{
curPiece.setRandomShape();
curX = BoardWidth / 2 + 1;
curY = BoardHeight - 1 + curPiece.minY();

if (!tryMove(curPiece, curX, curY)) {
curPiece.setShape(Tetrominoes.NoShape);
timer.stop();
isStarted = false;
statusbar.setText("game over");
}
}

Method newPiece() menciptakan potongan tetris yang baru. Potongan tersebut merupakan bentuk yang acak (random). Kemudian kita menghitung initial nilai curX and curY. Jika kita tidak bias bergerak ke posisi awalnya, permainan pun berakhir atau dalam istilah kita adalah “Game is over”. Timer pun berhenti. Kita menempatkan kalimat “Game over” pada statusbar.

private boolean tryMove(Shape newPiece, int newX, int newY)
{
for (int i = 0; i &lt; 4; ++i) {
int x = newX + newPiece.x(i);
int y = newY - newPiece.y(i);
if (x &lt; 0 || x &gt;= BoardWidth || y &lt; 0 || y &gt;= BoardHeight)
return false;
if (shapeAt(x, y) != Tetrominoes.NoShape)
return false;
}

curPiece = newPiece;
curX = newX;
curY = newY;
repaint();
return true;
}

Method tryMove() mencoba untuk memindahkan potongan tetris. Method tersebut mengembalikan nilai false, jika telah mencapai batas board atau berdekatan dengan batas yang akan jatuh.

for (int i = BoardHeight - 1; i &gt;= 0; --i) {
boolean lineIsFull = true;

for (int j = 0; j &lt; BoardWidth; ++j) {
if (shapeAt(j, i) == Tetrominoes.NoShape) {
lineIsFull = false;
break;
}
}

if (lineIsFull) {
++numFullLines;
for (int k = i; k &lt; BoardHeight - 1; ++k) {
for (int j = 0; j &lt; BoardWidth; ++j)
board[(k * BoardWidth) + j] = shapeAt(j, k + 1);
}
}
}

Di dalam method removeFullLines(), kita mengecek apakah masih ada baris yang penuh di Antara semua baris dalam board. Jika terdapat setidaknya satu baris saja, maka akan dibuang. Setelah menemukan baris penuh kita menambahkannya ke “Counter”. Kita pindahkan semua baris di atas yang penuh tersebut satu baris ke bawah. Pada saat ini kita menghancurkan seluruh barisnya. Sebagai catatan, bahwasanya dalam game Tetris kita, kita menggunakan “naive gravity”. Artinya, semua kotak / kubus mungkin saja mengambang di atas celah yang kosong.
Setiap potongan tetris mempunyai empat kotak. Masing-masing kotak / kubus tersebut digambar dengan metod drawSquare(). Potongan-potongan Tetris tersebut mempunyai warna yang berbeda.

g.setColor(color.brighter());
g.drawLine(x, y + squareHeight() - 1, x, y);
g.drawLine(x, y, x + squareWidth() - 1, y);

Bagian kiri dan bagian atas dari sebuah kotak digambar dengan warna yang cerah. Sama juga, bagian bawah dan kanan digambar dengan warna yang lebih gelap. Ini untuk mensimulasikan bentuk 3 Dimensi.
Kita mengendalikan permainan dengan sebuh keyboard. Mekanisme pengendalian diimplementasikan dengan KeyAdapter. Ini merupakan inner class yang meng-override method keyPressed().

case KeyEvent.VK_RIGHT:
tryMove(curPiece, curX + 1, curY);
break;

Jika kita menekan tombol panah kiri, maka kita mencoba untuk memindahkan potongan tetris tersebut ke pojok sebelah kiri.
Tetris
Gambar : Tetris
Inilah game Tetris.

Download Source code

Iklan

9 thoughts on “Membuat Game Tetris dengan Java

  1. Meiske

    Trima Kasih banyak.. sangat bermanfaat.. ijin download untuk tugas kuliah.. Trima Kasih.. Tuhan Memberkati.. 🙂

    Balas

Tinggalkan Balasan

Isikan data di bawah atau klik salah satu ikon untuk log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s