Recently, I've seen an interesting post: http://www.iteye.com/topic/595321 , it's a java implementation of Tetris. While being a long time game player (My first game was the gold digger on an IBM XT back in 1988, subsequently I played Koei's Romance of 3 kingdoms, from II - XI, among others), this is the first time I am trying a game.
Though I like this post a lot, the code there suffers multi-threading problem. While I don't claim I am a good game developer or an expert on multi-threading, I try to fix this problem. At the same time, I don't want to compete what-you-can-do-in-100-lines game, I am trying to write an easy-to-understand version.
Most of the code and the gif files are from the original author, I modified some of them.
The first class is the tetris block, for more details, check the wiki page: http://en.wikipedia.org/wiki/Tetris .
package my.test1; import java.awt.Image; import javax.swing.ImageIcon; public class TetrisBlock { public int type; public int orientation; public int color = 1; public int x; public int y; private static int[][][][] blockmeshs = { { {{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},/* l */ {{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}}},/*-*/ { {{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},/* z */ {{0, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}}},/* z| */ { {{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},/* xz */ {{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}}},/* xz| */ { {{0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}}},/** []*/ { {{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}}, {{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}}, {{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}},/* f */ { {{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}, {{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}, {{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}}},/* xf */ { {{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}, {{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}, {{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}} /* t */ } }; private static Image[] images = { new ImageIcon("domaintest/pics/red.gif").getImage(), // this is just a place holder, not used. new ImageIcon("domaintest/pics/lightblue.gif").getImage(), new ImageIcon("domaintest/pics/pink.gif").getImage(), new ImageIcon("domaintest/pics/blue.gif").getImage(), new ImageIcon("domaintest/pics/orange.gif").getImage(), new ImageIcon("domaintest/pics/green.gif").getImage(), new ImageIcon("domaintest/pics/red.gif").getImage() // this is the real red image }; public static Image image(int color) { return images[color]; } public static int blocksize() { return 6; } // the length of the really used images. public boolean isOccupied(int row, int col) { return blockmeshs[type][orientation][row][col] != 0; } public void rotate() { orientation++; if (orientation >= blockmeshs[type].length) orientation = 0; } public void settle(TetrisBoard tetrisBoard) { int[][] b = blockmeshs[type][orientation]; for (int i=0; i<4; i++) { for (int j=0; j<4; j++) { int a = b[i][j]; if (a != 0) { tetrisBoard.settle(y+i, x+j, color); } } } for (int i=y+3; i>y; i--) { tetrisBoard.removeFilledRow(i); } } public boolean canMoveDown(TetrisBoard tetrisBoard) { int[][] b = blockmeshs[type][orientation]; int yy = y + 1; for (int i=0; i<4; i++) { for (int j=0; j<4; j++) { if (yy + i >= tetrisBoard.length() && b[i][j] != 0) return false; if (yy+i < tetrisBoard.length() && x+j < tetrisBoard.width() && tetrisBoard.isOccupied(yy+i, x+j) && b[i][j] != 0) return false; } } return true; } public boolean canMoveLeft(TetrisBoard tetrisBoard) { int[][] b = blockmeshs[type][orientation]; int xx = x - 1; for (int i=0; i<4; i++) { for (int j=0; j<4; j++) { if (xx + j <= -1 && b[i][j] != 0) return false; if (y+i < tetrisBoard.length() && xx+j < tetrisBoard.width() && tetrisBoard.isOccupied(y+i, xx+j) && b[i][j] != 0) return false; } } return true; } public boolean canMoveRight(TetrisBoard tetrisBoard) { int[][] b = blockmeshs[type][orientation]; int xx = x + 1; for (int i=0; i<4; i++) { for (int j=0; j<4; j++) { if (xx + j >= tetrisBoard.width() && b[i][j] != 0) return false; if (y+i < tetrisBoard.length() && xx+j < tetrisBoard.width() && tetrisBoard.isOccupied(y+i, xx+j) && b[i][j] != 0) return false; } } return true; } public boolean canRotate(TetrisBoard tetrisBoard) { int oo = orientation + 1; if (oo >= blockmeshs[type].length) oo = 0; int[][] b = blockmeshs[type][oo]; for (int i=0; i<4; i++) { for (int j=0; j<4; j++) { if (y+i < tetrisBoard.length() && x+j < tetrisBoard.width() && tetrisBoard.isOccupied(y+i, x+j) && b[i][j] != 0) return false; } } return true; } }
The tetris block is encoded in a 4X4 matrix (look at the 1's in the matrix). All rotations of each block form an array. All such arrays form a bigger array called blockmeshs. There should be 7 images for 7 different blocks, but I just use the six gifs from the original post.
Though this class has >200 line of code, half of them are static data, so I can live with it. This class can be unit-tested without Swing.
The TetrisBoard class is:
package my.test1; public class TetrisBoard { private int length = 21; private int width = 10; public int[][] board = new int[length][width]; public int width() { return width; } public int length() { return length;} public boolean isOccupied(int row, int col) { if (row < 0 || row >= length || col < 0 || col >= width) return false; return board[row][col] != 0; } public int colorOf(int row, int col) { return board[row][col]; } public void settle(int row, int col, int color) { if (row < length && col < width) board[row][col] = color; } public void removeFilledRow(int row) { if (row >= length) return; boolean notFilled = false; for (int j=0; j0; j--) System.arraycopy(board[j-1], 0, board[j], 0, board[0].length); } } }
These two classes are tightly coupled. I choose to put some methods in one rather than another, the main motivation is performance (multi dimension array access can be slow, though it doesn't matter much in this case).
The next class is the composition of the above two:
package my.test1; /** * Other features, such as scores, next shape, stop/restart, change speed */ import java.util.Random; public class Tetris { public TetrisBlock movingBlock = new TetrisBlock(); public TetrisBoard board = new TetrisBoard(); public boolean finished = false; private Random generator = new Random(); public synchronized void play() { if (movingBlock.canMoveDown(board)) movingBlock.y++; else { if (movingBlock.y == 0) { finished = true; } else { movingBlock.settle(board); newBlock(); } } } private void newBlock() { movingBlock = new TetrisBlock(); movingBlock.x = 3; movingBlock.y = 0; movingBlock.type = generator.nextInt(TetrisBlock.blocksize()+1); movingBlock.color = generator.nextInt(TetrisBlock.blocksize()) + 1; // should be movingBlock.orientation = 0; } }
The method play() is synchronized because the data can be modified by this class and user input (arrow keys).
Now we are done with the game logic, it's time to work on the GUI. The first class is the drawing class:
package my.test1; import java.awt.Dimension; import java.awt.Graphics; import javax.swing.JPanel; public class TetrisPanel extends JPanel { public Tetris tetris; public TetrisPanel(Tetris tetris) { super(); this.setFocusable(true); this.setPreferredSize(new Dimension(150, 315)); this.tetris = tetris; addKeyListener(new TetrisGuiListener(tetris, this)); } public void paintComponent(Graphics g) { super.paintComponent(g); g.fillRect(0, 0, 150, 315); for (int i=0; iWe just override the paintComponent() method with our logic. The second part of it is a little nonintuitive because we need a special logic to write the last block. My current design is to draw the partial image, but others could choose not to draw at all.
The listener class is as follows:
package my.test1; import java.awt.Component; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; public class TetrisGuiListener implements KeyListener { private Tetris tetris; private Component gui; public TetrisGuiListener(Tetris tetris, Component gui) { this.tetris = tetris; this.gui = gui; } public void keyPressed(KeyEvent e) { synchronized(tetris) { if (e.getKeyCode() == 65 || e.getKeyCode() == 37) { if (tetris.movingBlock.canMoveLeft(tetris.board)) tetris.movingBlock.x--; } else if (e.getKeyCode() == 68 || e.getKeyCode() == 39) { if (tetris.movingBlock.canMoveRight(tetris.board)) tetris.movingBlock.x++; } else if (e.getKeyCode() == 83 || e.getKeyCode() == 40) { if (tetris.movingBlock.canMoveDown(tetris.board)) tetris.movingBlock.y++; } else if (e.getKeyCode() == 87 || e.getKeyCode() == 38) { if (tetris.movingBlock.canRotate(tetris.board)) tetris.movingBlock.rotate(); } gui.repaint(); } } public void keyTyped(KeyEvent e) { } public void keyReleased(KeyEvent e) { } }Again, we need to synchronize the data since this class is running on the EDT (event dispatching thread).
The next class is a wrapper of Tetris class, using SwingWorker:
package my.test1; /** * http://download.oracle.com/javase/tutorial/uiswing/concurrency/index.html * http://java.sun.com/developer/technicalArticles/javase/swingworker/ * http://www.javaworld.com/javaworld/jw-08-2007/jw-08-swingthreading.html?page=1 * http://developerlife.com/tutorials/?p=15 * http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html * http://java.sun.com/products/jfc/tsc/articles/threads/threads2.html * http://java.sun.com/products/jfc/tsc/articles/threads/threads3.html * http://stackoverflow.com/questions/1505427/multithreading-with-java-swing-for-a-simple-2d-animation * http://www.javaranch.com/journal/200410/JavaDesigns-SwingMultithreading.html * http://kenai.com/projects/trident/pages/SimpleSwingExample * http://java.sun.com/products/jfc/tsc/articles/painting/ */ import java.awt.Component; import java.util.List; import javax.swing.SwingWorker; public class TetrisGame extends SwingWorker{ private Tetris tetris; private Component gui; public TetrisGame(Tetris tetris, Component gui) { this.tetris = tetris; this.gui = gui; } @Override protected Tetris doInBackground() throws Exception { try { while (!tetris.finished) // && !isCancelled()) { tetris.play(); publish(tetris); try { Thread.sleep(200); } catch (Exception ex) { ex.printStackTrace(); } } return tetris; } catch (Throwable t) { t.printStackTrace(); return null; } } // This is called fro EDT @Override protected void process(List tetrisList) { Tetris t = tetrisList.get(tetrisList.size()-1); synchronized(t) { gui.repaint(); } } } For more information on SwingWorker, check the links in the java doc section. This class, except the method marked, will run in a separate thread. The separate thread and EDT both modify the data in the Tetris class.
Finally, a window class to stitch everything together.
package my.test1; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import javax.swing.JFrame; import javax.swing.SwingUtilities; public class TetrisWindow extends JFrame { public TetrisWindow() { super("Tetris"); this.setDefaultCloseOperation(EXIT_ON_CLOSE); this.setPreferredSize(new Dimension(160, 355)); this.setResizable(false); Tetris tetris = new Tetris(); TetrisPanel tetrisPanel = new TetrisPanel(tetris); Container content = getContentPane(); content.setLayout(new FlowLayout()); content.add(tetrisPanel); this.pack(); TetrisGame game = new TetrisGame(tetris, tetrisPanel); game.execute(); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { new TetrisWindow().setVisible(true); } }); } }Remember, we need to fire off the window from EDT too, so we use SwingUtilities.
Here is a screenshot.
Other references on this topic:
http://www.cs.unc.edu/~plato/COMP14/Assignments/tetris/tetris.html
http://www.ibm.com/developerworks/java/library/j-tetris/
http://gametuto.com/tetris-tutorial-in-c-render-independent/
http://zetcode.com/tutorials/javaswingtutorial/thetetrisgame/