Java Tetris game - creating Tetris game in Java
Contents Previous Next Java Tetris last modified January 10, 2023 In this chapter, we create a Tetris game clone in Java Swing. Source code and images can be found at the author's Github Java-Tetris-Game repository. Tetris The Tetris game is one of the mos
zetcode.com
실행결과
Tetris
테트리스 게임은 지금까지 만들어진 가장 인기 있는 컴퓨터 게임 중 하나입니다. 오리지널 게임은 1985년 러시아의 프로그래머 알렉세이 파지트노프에 의해 디자인되고 프로그래밍되었습니다. 그 이후로 테트리스는 거의 모든 컴퓨터 플랫폼에서 다양하게 이용할 수 있습니다. 심지어 제 휴대폰에도 테트리스 게임의 수정된 버전이 있습니다.
테트리스는 폴링 블록 퍼즐 게임이라고 불립니다. 이 게임에서, 우리는 테트로미노라고 불리는 7가지 다른 모양을 가지고 있습니다. S형, Z형, T형, L형, 선형, 대칭 L형 및 사각형입니다. 이 모양들은 각각 네 개의 정사각형으로 형성됩니다. 모양들이 널빤지에서 떨어지고 있습니다. 테트리스 게임의 목적은 모양이 최대한 맞도록 움직이고 회전하는 것입니다. 만약 우리가 간신히 한 줄을 이룬다면, 그 줄은 파괴되고 우리는 득점합니다. 우리는 최고가 될 때까지 테트리스 게임을 합니다.
The development
tetrominoes는 Swing 페인팅 API를 사용하여 그려집니다. java.util.Timer를 사용합니다. 게임 주기를 만드는 타이머입니다. 모양은 픽셀 단위가 아닌 정사각형 단위로 이동합니다. 수학적으로 게임의 보드는 단순한 숫자 목록입니다.
게임은 시작된 후 바로 시작됩니다. 우리는 p 키를 눌러 게임을 일시 중지할 수 있습니다. 스페이스 키는 테트리스 조각을 바로 아래로 떨어뜨릴 것입니다. d 키는 조각을 한 줄 아래로 떨어뜨립니다. (낙하 속도를 조금 더 높이는 데 사용할 수 있습니다.) 게임은 일정한 속도로 진행되며 가속은 구현되지 않습니다. 점수는 우리가 제거한 줄 수입니다.
package com.zetcode;
import java.util.Random;
public class Shape {
protected enum Tetrominoe {
NoShape, ZShape, SShape, LineShape,
TShape, SquareShape, LShape, MirroredLShape
}
private Tetrominoe pieceShape;
private int[][] coords;
public Shape() {
coords = new int[4][2];
setShape(Tetrominoe.NoShape);
}
void setShape(Tetrominoe shape) {
int[][][] 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++) {
System.arraycopy(coordsTable[shape.ordinal()], 0, coords, 0, 4);
}
pieceShape = shape;
}
private void setX(int index, int x) {
coords[index][0] = x;
}
private void setY(int index, int y) {
coords[index][1] = y;
}
int x(int index) {
return coords[index][0];
}
int y(int index) {
return coords[index][1];
}
Tetrominoe getShape() {
return pieceShape;
}
void setRandomShape() {
var r = new Random();
int x = Math.abs(r.nextInt()) % 7 + 1;
Tetrominoe[] values = Tetrominoe.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;
}
int minY() {
int m = coords[0][1];
for (int i = 0; i < 4; i++) {
m = Math.min(m, coords[i][1]);
}
return m;
}
Shape rotateLeft() {
if (pieceShape == Tetrominoe.SquareShape) {
return this;
}
var 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;
}
Shape rotateRight() {
if (pieceShape == Tetrominoe.SquareShape) {
return this;
}
var 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;
}
}
Shape 클래스는 테트리스 조각에 대한 정보를 제공합니다.
protected enum Tetrominoe {
NoShape, ZShape, SShape, LineShape,
TShape, SquareShape, LShape, MirroredLShape
}
Tetrominoe 열거형에는 7개의 테트리스 모양 이름과 NoShape라는 빈 모양이 있습니다.
coords = new int[4][2];
setShape(Tetrominoe.NoShape);
좌표 배열은 테트리스 조각의 실제 좌표를 유지합니다.
int[][][] 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}}
};
coordsTable 배열은 테트리스 조각의 가능한 모든 좌표 값을 유지합니다. 이것은 모든 조각이 좌표 값을 취하는 템플릿입니다.
int[][][] 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++) {
System.arraycopy(coordsTable[shape.ordinal()], 0, coords, 0, 4);
}
coordsTable 좌표 값 행 하나를 테트리스 조각의 좌표 배열에 넣습니다. ordinal() 메서드의 사용법을 기록합니다. C++에서 열거형은 본질적으로 정수입니다. C++와 달리 Java 열거형은 전체 클래스이며 Ordinal() 메서드는 열거형 개체에서 열거형 유형의 현재 위치를 반환합니다.
+ 배열을 복사하는 System.arraycopy 사용법
System.arraycopy( Object src, int srcPos, Object dest, int destPos, int length )
- Object src: Ctrl+C 할 곳 (복사하려는 원본)
- int srcPos: Object src의 몇 번째 인덱스부터 복사할 것인지
- Object dest: Ctrl+V 할 곳 (붙여넣기하려는 대상)
- int destPos: Object dest의 몇 번째 인덱스부터 붙여넣기할 것인지
- int length: Object src에서 몇 개를 복사할 것인지
Ex)
int[] arr1 = {10, 20, 30, 40, 50};
int[] arr2 = {1, 2, 3, 4, 5};
System.arraycopy(arr1, 0, arr2, 1, 3); // arr1의 index 0에서부터 3개를, arr2의 index 1에서부터 copy
// System.arraycopy(arr1, 0, arr2, 1, 5); // 범위를 넘어서면 Error
for( int i = 0; i < arr2.length; i++ ) {
System.out.println(arr2[i]);
}
1
10
20
30
5
다음 이미지는 좌표 값을 조금 더 이해하는 데 도움이 됩니다. 좌표 배열은 테트리스 조각의 좌표를 저장합니다. 예를 들어, 숫자(-1, 1), (-1, 0), (0, 0) 및 (0, -1)은 회전된 S자형을 나타냅니다. 다음 다이어그램은 모양을 보여줍니다.
Shape rotateLeft() {
if (pieceShape == Tetrominoe.SquareShape) {
return this;
}
var 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;
}
이 코드는 조각을 왼쪽으로 회전시킵니다. 사각형은 회전할 필요가 없습니다. 그렇기 때문에 현재 개체에 대한 참조만 반환합니다. 이전 이미지를 보면 회전을 이해하는 데 도움이 될 것입니다.
package com.zetcode;
import com.zetcode.Shape.Tetrominoe;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public class Board extends JPanel {
private final int BOARD_WIDTH = 10;
private final int BOARD_HEIGHT = 22;
private final int PERIOD_INTERVAL = 300;
private Timer timer;
private boolean isFallingFinished = false;
private boolean isPaused = false;
private int numLinesRemoved = 0;
private int curX = 0;
private int curY = 0;
private JLabel statusbar;
private Shape curPiece;
private Tetrominoe[] board;
public Board(Tetris parent) {
initBoard(parent);
}
private void initBoard(Tetris parent) {
setFocusable(true);
statusbar = parent.getStatusBar();
addKeyListener(new TAdapter());
}
private int squareWidth() {
return (int) getSize().getWidth() / BOARD_WIDTH;
}
private int squareHeight() {
return (int) getSize().getHeight() / BOARD_HEIGHT;
}
private Tetrominoe shapeAt(int x, int y) {
return board[(y * BOARD_WIDTH) + x];
}
void start() {
curPiece = new Shape();
board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];
clearBoard();
newPiece();
timer = new Timer(PERIOD_INTERVAL, new GameCycle());
timer.start();
}
private void pause() {
isPaused = !isPaused;
if (isPaused) {
statusbar.setText("paused");
} else {
statusbar.setText(String.valueOf(numLinesRemoved));
}
repaint();
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
doDrawing(g);
}
private void doDrawing(Graphics g) {
var size = getSize();
int boardTop = (int) size.getHeight() - BOARD_HEIGHT * squareHeight();
for (int i = 0; i < BOARD_HEIGHT; i++) {
for (int j = 0; j < BOARD_WIDTH; j++) {
Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);
if (shape != Tetrominoe.NoShape) {
drawSquare(g, j * squareWidth(),
boardTop + i * squareHeight(), shape);
}
}
}
if (curPiece.getShape() != Tetrominoe.NoShape) {
for (int i = 0; i < 4; i++) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
drawSquare(g, x * squareWidth(),
boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
curPiece.getShape());
}
}
}
private void dropDown() {
int newY = curY;
while (newY > 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 < BOARD_HEIGHT * BOARD_WIDTH; i++) {
board[i] = Tetrominoe.NoShape;
}
}
private void pieceDropped() {
for (int i = 0; i < 4; i++) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
board[(y * BOARD_WIDTH) + x] = curPiece.getShape();
}
removeFullLines();
if (!isFallingFinished) {
newPiece();
}
}
private void newPiece() {
curPiece.setRandomShape();
curX = BOARD_WIDTH / 2 + 1;
curY = BOARD_HEIGHT - 1 + curPiece.minY();
if (!tryMove(curPiece, curX, curY)) {
curPiece.setShape(Tetrominoe.NoShape);
timer.stop();
var msg = String.format("Game over. Score: %d", numLinesRemoved);
statusbar.setText(msg);
}
}
private boolean tryMove(Shape newPiece, int newX, int newY) {
for (int i = 0; i < 4; i++) {
int x = newX + newPiece.x(i);
int y = newY - newPiece.y(i);
if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {
return false;
}
if (shapeAt(x, y) != Tetrominoe.NoShape) {
return false;
}
}
curPiece = newPiece;
curX = newX;
curY = newY;
repaint();
return true;
}
private void removeFullLines() {
int numFullLines = 0;
for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {
boolean lineIsFull = true;
for (int j = 0; j < BOARD_WIDTH; j++) {
if (shapeAt(j, i) == Tetrominoe.NoShape) {
lineIsFull = false;
break;
}
}
if (lineIsFull) {
numFullLines++;
for (int k = i; k < BOARD_HEIGHT - 1; k++) {
for (int j = 0; j < BOARD_WIDTH; j++) {
board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1);
}
}
}
}
if (numFullLines > 0) {
numLinesRemoved += numFullLines;
statusbar.setText(String.valueOf(numLinesRemoved));
isFallingFinished = true;
curPiece.setShape(Tetrominoe.NoShape);
}
}
private void drawSquare(Graphics g, int x, int y, Tetrominoe 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)
};
var 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);
}
private class GameCycle implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
doGameCycle();
}
}
private void doGameCycle() {
update();
repaint();
}
private void update() {
if (isPaused) {
return;
}
if (isFallingFinished) {
isFallingFinished = false;
newPiece();
} else {
oneLineDown();
}
}
class TAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
if (curPiece.getShape() == Tetrominoe.NoShape) {
return;
}
int keycode = e.getKeyCode();
// Java 12 switch expressions
switch (keycode) {
case KeyEvent.VK_P -> pause();
case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);
case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY);
case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY);
case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY);
case KeyEvent.VK_SPACE -> dropDown();
case KeyEvent.VK_D -> oneLineDown();
}
}
}
}
마지막으로, 우리는 Board.java 파일을 가지고 있습니다. 여기가 게임 로직이 위치한 곳입니다.
private final int BOARD_WIDTH = 10;
private final int BOARD_HEIGHT = 22;
private final int PERIOD_INTERVAL = 300;
4개의 상수가 있습니다. BOARD_WIDTH 및 BOARD_HEIGHT는 보드의 크기를 정의합니다. PERIOD_INTERVAL 상수는 게임의 속도를 정의합니다.
...
private boolean isFallingFinished = false;
private boolean isStarted = false;
private boolean isPaused = false;
private int numLinesRemoved = 0;
private int curX = 0;
private int curY = 0;
...
일부 중요 변수가 초기화됩니다. isFallingFinished는 테트리스 셰이프가 하강을 마쳤는지 여부를 결정한 다음 새 셰이프를 만들어야 합니다. isStarted는 게임이 시작되었는지 확인하는 데 사용됩니다. 마찬가지로 isPaused는 게임이 일시 중지되었는지 확인하는 데 사용됩니다. numLines Removed는 지금까지 제거한 라인 수를 계산합니다. curX 및 curY는 하강 테트리스 형상의 실제 위치를 결정합니다.
private int squareWidth() {
return (int) getSize().getWidth() / BOARD_WIDTH;
}
private int squareHeight() {
return (int) getSize().getHeight() / BOARD_HEIGHT;
}
이 선은 단일 테트로미노에 사각형의 너비와 높이를 결정합니다.
private Tetrominoe shapeAt(int x, int y) {
return board[(y * BOARD_WIDTH) + x];
}
우리는 주어진 좌표에서 모양을 결정합니다. 모양은 보드 배열에 저장됩니다.
void start() {
curPiece = new Shape();
board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];
...
우리는 새로운 현재 모양과 새로운 보드를 만듭니다.
clearBoard();
newPiece();
보드가 삭제되고 새 낙하물이 초기화됩니다.
timer = new Timer(PERIOD_INTERVAL, new GameCycle());
timer.start();
우리는 타이머를 만듭니다. 타이머는 PERIOD_INTERVAL 간격으로 실행되어 게임 사이클을 생성합니다.
private void pause() {
isPaused = !isPaused;
if (isPaused) {
statusbar.setText("paused");
} else {
statusbar.setText(String.valueOf(numLinesRemoved));
}
repaint();
}
pause() 메서드는 게임을 일시 중지하거나 재개합니다. 게임이 일시 중지되면 상태 표시줄에 일시 중지된 메시지가 표시됩니다.
doDrawing() 메서드 내에서는 보드에 있는 모든 객체를 그립니다. 그 그림은 두 단계로 되어 있습니다.
for (int i = 0; i < BOARD_HEIGHT; i++) {
for (int j = 0; j < BOARD_WIDTH; j++) {
Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);
if (shape != Tetrominoe.NoShape) {
drawSquare(g, j * squareWidth(),
boardTop + i * squareHeight(), shape);
}
}
}
첫 번째 단계에서 우리는 보드의 바닥으로 떨어진 모양의 모든 모양이나 잔해를 칠합니다. 모든 정사각형은 보드 배열에 기억됩니다. 우리는 shapeAt() 메서드를 사용하여 액세스합니다.
if (curPiece.getShape() != Tetrominoe.NoShape) {
for (int i = 0; i < 4; i++) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
drawSquare(g, x * squareWidth(),
boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
curPiece.getShape());
}
}
두 번째 단계에서는 실제 낙하물을 칠합니다.
private void dropDown() {
int newY = curY;
while (newY > 0) {
if (!tryMove(curPiece, curX, newY - 1)) {
break;
}
newY--;
}
pieceDropped();
}
스페이스 키를 누르면 부품이 아래로 떨어집니다. 우리는 단지 조각이 다른 떨어진 테트리스 조각의 바닥이나 꼭대기에 닿을 때까지 한 줄 아래로 떨어뜨리려고 합니다. 테트리스 피스가 하강을 마치면 pieceDropped()가 호출됩니다.
private void oneLineDown() {
if (!tryMove(curPiece, curX, curY - 1)) {
pieceDropped();
}
}
oneLineDown() 메서드는 하강 피스가 완전히 떨어질 때까지 한 줄 아래로 이동하려고 합니다.
private void clearBoard() {
for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {
board[i] = Tetrominoe.NoShape;
}
}
clearBoard() 메서드는 보드를 빈 테트로미노에 채웁니다. NoShape. 이 기능은 나중에 충돌 감지에 사용됩니다.
private void pieceDropped() {
for (int i = 0; i < 4; i++) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
board[(y * BOARD_WIDTH) + x] = curPiece.getShape();
}
removeFullLines();
if (!isFallingFinished) {
newPiece();
}
}
pieceDropped() 메서드는 하강 piece를 보드 배열에 넣습니다. 다시 한번, 보드는 모든 조각들의 정사각형과 떨어지는 것을 끝낸 조각들의 잔해를 잡습니다. 조각이 다 떨어졌을 때, 우리가 보드에서 몇 개의 선을 제거할 수 있는지 확인해야 할 때입니다. 이것은 removeFullLines() 메서드의 작업입니다. 그런 다음 우리는 새로운 작품을 창조합니다. 더 정확히 말하면, 새로운 작품을 창조하려고 합니다.
private void newPiece() {
curPiece.setRandomShape();
curX = BOARD_WIDTH / 2 + 1;
curY = BOARD_HEIGHT - 1 + curPiece.minY();
if (!tryMove(curPiece, curX, curY)) {
curPiece.setShape(Tetrominoe.NoShape);
timer.stop();
var msg = String.format("Game over. Score: %d", numLinesRemoved);
statusbar.setText(msg);
}
}
newPiece() 메서드는 새 테트리스 조각을 만듭니다. 그 작품은 새로운 무작위 모양을 얻습니다. 그런 다음 초기 curX 및 curY 값을 계산합니다. 만약 우리가 초기 위치로 이동할 수 없다면, 게임은 끝입니다. 우리는 최고입니다. 타이머가 중지되고 상태 표시줄에 점수가 포함된 게임 오버 문자열을 표시합니다.
private boolean tryMove(Shape newPiece, int newX, int newY) {
for (int i = 0; i < 4; i++) {
int x = newX + newPiece.x(i);
int y = newY - newPiece.y(i);
if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {
return false;
}
if (shapeAt(x, y) != Tetrominoe.NoShape) {
return false;
}
}
curPiece = newPiece;
curX = newX;
curY = newY;
repaint();
return true;
}
tryMove() 메서드는 테트리스 조각을 이동하려고 합니다. 메소드가 보드 경계에 도달했거나 이미 떨어진 테트리스 조각에 인접한 경우 false를 반환합니다.
private void removeFullLines() {
int numFullLines = 0;
for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {
boolean lineIsFull = true;
for (int j = 0; j < BOARD_WIDTH; j++) {
if (shapeAt(j, i) == Tetrominoe.NoShape) {
lineIsFull = false;
break;
}
}
if (lineIsFull) {
numFullLines++;
for (int k = i; k < BOARD_HEIGHT - 1; k++) {
for (int j = 0; j < BOARD_WIDTH; j++) {
board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1);
}
}
}
}
if (numFullLines > 0) {
numLinesRemoved += numFullLines;
statusbar.setText(String.valueOf(numLinesRemoved));
isFallingFinished = true;
curPiece.setShape(Tetrominoe.NoShape);
}
}
removeFullLines() 메서드 내에서 보드의 모든 행 중에 전체 행이 있는지 확인합니다. 하나 이상의 전체 줄이 있으면 해당 줄이 제거됩니다. 전체 줄을 찾은 후에 카운터를 늘립니다. 전체 행 위의 모든 행을 한 줄 아래로 이동합니다. 이렇게 하면 전 노선을 파괴할 수 있습니다. 우리의 테트리스 게임에서 우리는 순진한 중력을 사용합니다. 즉, 정사각형이 빈 간격 위에 떠 있을 수 있습니다.
private void drawSquare(Graphics g, int x, int y, Tetrominoe 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)
};
var 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);
}
모든 테트리스 조각에는 정사각형이 네 개 있습니다. 각 정사각형은 drawSquare() 방법으로 그려집니다. 테트리스 조각은 다른 색을 가지고 있습니다. 정사각형의 왼쪽과 위쪽은 더 밝은 색으로 그려집니다. 마찬가지로, 아래쪽과 오른쪽은 더 어두운 색으로 그려집니다. 이것은 3D 에지를 시뮬레이션하기 위한 것입니다.
private class GameCycle implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
doGameCycle();
}
}
게임은 게임 주기로 나뉩니다. 각 주기는 게임을 업데이트하고 보드를 다시 그립니다.
private void update() {
if (isPaused) {
return;
}
if (isFallingFinished) {
isFallingFinished = false;
newPiece();
} else {
oneLineDown();
}
}
update()는 게임의 한 단계를 나타냅니다. 하강 조각이 한 줄 아래로 내려가거나 이전 조각이 하강을 완료한 경우 새 조각이 만들어집니다.
private class TAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
...
게임은 커서 키로 제어됩니다. 키 어댑터에서 주요 이벤트를 확인합니다.
int keycode = e.getKeyCode();
우리는 getKeyCode() 메서드로 키 코드를 얻습니다.
// Java 12 switch expressions
switch (keycode) {
case KeyEvent.VK_P -> pause();
case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);
case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY);
case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY);
case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY);
case KeyEvent.VK_SPACE -> dropDown();
case KeyEvent.VK_D -> oneLineDown();
}
Java 12 스위치 식을 사용하여 주요 이벤트를 메서드에 바인딩합니다. 예를 들어, Space 키를 사용하여 떨어지는 테트리스 조각을 떨어뜨립니다.
package com.zetcode;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JLabel;
/*
Java Tetris game clone
Author: Jan Bodnar
Website: http://zetcode.com
*/
public class Tetris extends JFrame {
private JLabel statusbar;
public Tetris() {
initUI();
}
private void initUI() {
statusbar = new JLabel(" 0");
add(statusbar, BorderLayout.SOUTH);
var board = new Board(this);
add(board);
board.start();
setTitle("Tetris");
setSize(200, 400);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
}
JLabel getStatusBar() {
return statusbar;
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
var game = new Tetris();
game.setVisible(true);
});
}
}
Tetris.java 파일에서 우리는 게임을 설정했습니다. 우리는 게임을 하는 보드를 만듭니다. 상태 표시줄을 만듭니다.
statusbar = new JLabel(" 0");
add(statusbar, BorderLayout.SOUTH);
점수는 보드 하단에 있는 레이블에 표시됩니다.
var board = new Board(this);
add(board);
board.start();
보드가 생성되어 컨테이너에 추가됩니다. start() 메서드는 테트리스 게임을 시작합니다.
This was the Tetris game.