728x90
저번 글 1: https://stickode.tistory.com/822
저번 글 2: https://stickode.tistory.com/835
저번 글 3: https://stickode.tistory.com/852
저번 글 4: https://stickode.tistory.com/866
안녕하세요. 저번 시간에 이어서, 오늘은 Game 코드에 대한 설명을 업로드하겠습니다. 주석으로 설명을 달아두었습니다.
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
/**
* 테트리스 게임. 이 클래스는 게임의 모든 이벤트를 제어하고
* 모든 게임 로직을 처리합니다. 게임은 이 클래스에서 제공하는
* 그래픽 게임 컴포넌트와 사용자 상호작용을 통해 시작됩니다.
*/
public class Game extends Object {
public static final int STATE_GETREADY = 1;
public static final int STATE_PLAYING = 2;
public static final int STATE_PAUSED = 3;
public static final int STATE_GAMEOVER = 4;
/**
* 리스너를 등록하고 이벤트를 전송할 수 있는 PropertyChangeSupport 객체입니다.
*/
private final PropertyChangeSupport PCS = new PropertyChangeSupport(this);
/**
* 메인 사각형 보드입니다. 이 보드는 게임에 사용됩니다.
*/
private final SquareBoard board;
/**
* 미리보기 사각형 보드입니다. 이 보드는 도형의 미리보기를 표시하는 데 사용됩니다.
*/
private final SquareBoard previewBoard = new SquareBoard(5, 5);
/**
* 메인 보드와 미리보기 보드에서 사용되는 도형입니다.
* 게임 실행 중에 새로운 객체를 생성하지 않도록 도형을 재사용합니다.
* 미리보기 도형과 현재 도형이 같은 객체를 참조하는 경우 특별한 주의가 필요합니다.
*/
private Figure[] figures = {
new Figure(Figure.SQUARE_FIGURE),
new Figure(Figure.LINE_FIGURE),
new Figure(Figure.S_FIGURE),
new Figure(Figure.Z_FIGURE),
new Figure(Figure.RIGHT_ANGLE_FIGURE),
new Figure(Figure.LEFT_ANGLE_FIGURE),
new Figure(Figure.TRIANGLE_FIGURE)
};
/**
* 게임을 실행하는 스레드입니다. 이 변수가 null로 설정되면 게임 스레드가 종료됩니다.
*/
private final GameThread thread;
/**
* 게임 레벨입니다. 사각형 보드에서 20줄이 제거될 때마다 레벨이 증가합니다.
*/
private int level = 1;
/**
* 현재 점수입니다. 메인 보드에 놓을 수 있는 도형마다 점수가 증가합니다.
*/
private int score = 0;
/**
* 현재 도형입니다.
*/
private Figure figure = null;
/**
* 다음 도형입니다.
*/
private Figure nextFigure = null;
/**
* 다음 도형의 회전입니다.
*/
private int nextRotation = 0;
/**
* 도형 미리보기 플래그입니다. 이 플래그가 설정되면 도형이 도형 미리보기 보드에 표시
됩니다.
*/
private boolean preview = true;
/**
* 이동 잠금 플래그입니다. 이 플래그가 설정되면 현재 도형은 이동할 수 없습니다.
* 이 플래그는 도형이 아래로 이동할 때 설정되고, 새로운 도형이 표시될 때 재설정됩니다.
*/
private boolean moveLock = false;
/**
*
*/
private int state;
/**
* 새로운 테트리스 게임을 생성합니다. 사각형 보드의 기본 크기는 10x20입니다.
*/
public Game() {
this(10, 20);
}
/**
* 새로운 테트리스 게임을 생성합니다. 사각형 보드의 크기를 지정할 수 있습니다.
*
* @param width 사각형 보드의 너비 (위치 단위)
* @param height 사각형 보드의 높이 (위치 단위)
*/
public Game(int width, int height) {
board = new SquareBoard(width, height);
thread = new GameThread();
handleGetReady();
board.getComponent().setFocusable(true);
board.getComponent().addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
handleKeyEvent(e);
}
});
}
/**
* 이 게임에 PropertyChangeListener를 추가합니다.
* <p>
* 발생할 수 있는 이벤트 목록:
* <p>
* 이름: "state"
* 값: 새로운 현재 상태 (int) - STATE_OVER, STATE_PLAYING, STATE_PAUSED 중 하나
* 발생 시점: 상태가 변경될 때 발생합니다.
* <p>
* 이름: "level"
* 값: 현재 레벨 (int)
* 발생 시점: 플레이어가 다음 레벨로 이동할 때 발생합니다.
* <p>
* 이름: "score"
* 값: 현재 점수 (int)
* 발생 시점: 플레이어가 점수를 증가시킬 때 발생합니다.
* <p>
* 이름: "lines"
* 값: 제거된 줄 수 (int)
* 발생 시점: 플레이어가 한 줄 이상을 제거할 때 발생합니다.
*
* @param l 알림을 받을 PropertyChangeListener
*/
public void addPropertyChangeListener(PropertyChangeListener l) {
PCS.addPropertyChangeListener(l);
}
/**
* 이 PropertyChangeListener를 제거합니다.
*
* @param l 제거할 PropertyChangeListener 객체
*/
public void removePropertyChangeListener(PropertyChangeListener l) {
PCS.removePropertyChangeListener(l);
}
/**
* 현재 '상태'를 가져옵니다.
* 다음 중 하나일 수 있습니다: STATE_GETREADY, STATE_PLAYING, STATE_PAUSED, STATE_GAMEOVER.
*
* @return 현재 상태
*/
public int getState() {
return state;
}
/**
*
현재 레벨을 가져옵니다.
*
* @return 현재 레벨
*/
public int getLevel() {
return level;
}
/**
* 현재 점수를 가져옵니다.
*
* @return 현재 점수
**/
public int getScore() {
return score;
}
/**
* 게임이 시작된 이후 제거된 줄 수를 가져옵니다.
*
* @return 제거된 줄 수
*/
public int getRemovedLines() {
return board.getRemovedLines();
}
/**
* 보드의 Java AWT 컴포넌트를 가져옵니다.
* @return 보드의 GUI 컴포넌트
*/
public Component getSquareBoardComponent() {
return board.getComponent();
}
/**
* 미리보기 보드의 Java AWT 컴포넌트를 가져옵니다. (5x5)
* @return 미리보기 보드의 GUI 컴포넌트
*/
public Component getPreviewBoardComponent() {
return previewBoard.getComponent();
}
/**
* 상태가 STATE_GAMEOVER인 경우 준비 상태로 초기화합니다.
* 그렇지 않으면 아무 작업도 수행하지 않습니다.
**/
public void init() {
if (state == STATE_GAMEOVER) {
handleGetReady();
}
}
/**
* 게임을 시작합니다. (현재 상태에 관계없이)
**/
public void start() {
handleStart();
}
/**
* 게임을 일시정지합니다. 상태가 STATE_PLAYING인 경우에만 작동하며,
* 그렇지 않으면 아무 작업도 수행하지 않습니다.
**/
public void pause() {
if (state == STATE_PLAYING) {
handlePause();
}
}
/**
* 게임을 재개합니다. 상태가 STATE_PAUSED인 경우에만 작동하며,
* 그렇지 않으면 아무 작업도 수행하지 않습니다.
**/
public void resume() {
if (state == STATE_PAUSED) {
handleResume();
}
}
/**
* 게임을 종료합니다. (현재 상태에 관계없이)
**/
public void terminate() {
handleGameOver();
}
/**
* 게임 시작 이벤트를 처리합니다. 메인 보드와 미리보기 보드를 모두 초기화하고,
* 다른 모든 게임 매개변수를 재설정합니다. 마지막으로 게임 스레드를 시작합니다.
*/
private void handleStart() {
// 점수와 도형 재설정
level = 1;
score = 0;
figure = null;
nextFigure = randomFigure();
nextFigure.rotateRandom();
nextRotation = nextFigure.getRotation();
// 컴포넌트 재설정
state = STATE_PLAYING;
board.setMessage(null);
board.clear();
previewBoard.clear();
handleLevelModification();
handleScoreModification();
PCS.firePropertyChange("state", -1, STATE_PLAYING);
// 게임 스레드 시작
thread.reset();
}
/**
* 게임 오버 이벤트를 처리합니다. 이는 게임 스레드를 중지하고,
* 모든 도형을 재설정하고 게임 오버 메시지를 출력합니다.
*/
private void handleGameOver() {
// 게임 스레드 중지
thread.setPaused(true);
// 도형 재설정
if (figure != null) {
figure.detach();
}
figure = null;
if (nextFigure != null) {
nextFigure.detach();
}
nextFigure = null;
// 컴포넌트 처리
state = STATE_GAMEOVER;
board.setMessage("Game Over");
PCS.firePropertyChange("state", -1, STATE_GAMEOVER);
}
/**
* getReady 이벤트를 처리합니다.
* 게임 보드에 '준비 상태' 메시지를 출력합니다.
*/
private void handleGetReady() {
board.setMessage("Get Ready");
board.clear();
previewBoard.clear();
state = STATE_GETREADY;
PCS.firePropertyChange("state", -1, STATE_GETREADY);
}
/**
* 일시정지 이벤트를 처리합니다. 이는 게임 스레드를 일시정지하고
* 게임 보드에 일시정지 메시지를 출력합니다.
*/
private void handlePause() {
thread.setPaused(true);
state = STATE_PAUSED;
board.setMessage("Paused");
PCS.firePropertyChange("state", -1, STATE_PAUSED);
}
/**
* 게임 재개 이벤트를 처리합니다. 이는 게임 스레드를 재개하고
* 게임 보드에서 모든 메시지를 제거합니다.
*/
private void handleResume() {
state = STATE_PLAYING;
board.setMessage(null);
thread.setPaused(false);
PCS.firePropertyChange("state", -1, STATE_PLAYING);
}
/**
* 레벨 수정 이벤트를 처리합니다. 이는 레벨 라벨을 수정하고
* 스레드 속도를 조정합니다.
*/
private void handleLevelModification() {
PCS.firePropertyChange("level", -1, level);
thread.adjustSpeed();
}
/**
* 점수 수정 이벤트를 처리합니다. 이는 점수 라벨을 수정합니다.
*/
private void handleScoreModification() {
PCS.firePropertyChange("score", -1, score);
}
/**
* 도형 시작 이벤트를 처리합니다. 다음 도형을 현재 도형 위치로 이동하고
* 동시에 새로운 미리보기 도형을 생성합니다. 도형을 게임 보드에
* 추가할 수 없는 경우 게임 오버 이벤트가 시작됩니다.
*/
private void handleFigureStart() {
int rotation;
// 다음 도형을 현재 도형으로 이동
figure = nextFigure;
moveLock = false;
rotation = nextRotation;
nextFigure = randomFigure();
nextFigure.rotateRandom();
nextRotation = nextFigure.getRotation();
// 도형 미리보기 처리
if (preview) {
previewBoard.clear();
nextFigure.attach(previewBoard, true);
nextFigure.detach();
}
// 도형을 게임 보드에 추가
figure.setRotation(rotation);
if (!figure.attach(board, false)) {
previewBoard.clear();
figure.attach(previewBoard, true);
figure.detach();
handleGameOver();
}
}
/**
* 도형 착지 이벤트를 처리합니다. 도형이 완전히 보이는지 확인하고,
* 그렇지 않으면 게임 오버 이벤트가 시작됩니다. 이후 모든
* 가득 찬 줄을 제거합니다. 가득 찬 줄을 제거할 수 없는 경우
* 도형 시작 이벤트가 직접 시작됩니다.
*/
private void handleFigureLanded() {
// 도형 확인 및 분리
if (figure.isAllVisible()) {
score += 10;
handleScoreModification();
} else {
handleGameOver();
return;
}
figure.detach();
figure = null;
// 가득 찬 줄 확인 및 새로운 도형 생성
if (board.hasFullLines()) {
board.removeFullLines();
PCS.firePropertyChange("lines", -1, board.getRemovedLines());
if (level < 9 && board.getRemovedLines() / 20 > level) {
level = board.getRemovedLines() / 20;
handleLevelModification();
}
} else {
handleFigureStart();
}
}
/**
* 타이머 이벤트를 처리합니다. 일반적으로 도형을 아래로 한 칸 이동시킵니다.
* 도형이 착지하거나 준비되지 않은 경우 다른 이벤트가 시작됩니다.
* 이 메서드는 다른 비동기 이벤트(키보드 및 마우스)와의 경쟁 상황을
* 피하기 위해 동기화되었습니다.
*/
private synchronized void handleTimer() {
if (figure == null) {
handleFigureStart();
} else if (figure.hasLanded()) {
handleFigureLanded();
} else {
figure.moveDown();
}
}
/**
* 버튼 누름 이벤트를 처리합니다. 게임의 상태에 따라 다른 이벤트가 시작됩니다.
* 버튼 의미론은 게임이 변경됨에 따라 변경됩니다. 이 메서드는
* 동기화되어 다른 비동기 이벤트(타이머 및 키보드)와의 경쟁 상황을
* 피하기 위해 동기화되었습니다.
*/
private synchronized void handlePauseOnOff() {
if (nextFigure == null) {
handleStart();
} else if (thread.isPaused()) {
handleResume();
} else {
handlePause();
}
}
/**
* 키보드 이벤트를 처리합니다. 눌린 키에 따라 다른 동작을 수행합니다
.
* 일부 경우 다른 이벤트가 시작될 수 있습니다. 이 메서드는
* 동기화되어 다른 비동기 이벤트(타이머 및 마우스)와의 경쟁 상황을
* 피하기 위해 동기화되었습니다.
*
* @param e 키 이벤트
*/
private synchronized void handleKeyEvent(KeyEvent e) {
// 시작 처리 (아무 키나 시작 !!!)
if (state == STATE_GETREADY) {
handleStart();
return;
}
// 일시정지 및 재개
if (e.getKeyCode() == KeyEvent.VK_P) {
handlePauseOnOff();
return;
}
// 중지되었거나 일시정지 상태인 경우 진행하지 않음
if (figure == null || moveLock || thread.isPaused()) {
return;
}
// 나머지 키 이벤트 처리
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
figure.moveLeft();
break;
case KeyEvent.VK_RIGHT:
figure.moveRight();
break;
case KeyEvent.VK_DOWN:
figure.moveAllWayDown();
moveLock = true;
break;
case KeyEvent.VK_UP:
case KeyEvent.VK_SPACE:
if (e.isControlDown()) {
figure.rotateRandom();
} else if (e.isShiftDown()) {
figure.rotateClockwise();
} else {
figure.rotateCounterClockwise();
}
break;
case KeyEvent.VK_S:
if (level < 9) {
level++;
handleLevelModification();
}
break;
case KeyEvent.VK_N:
preview = !preview;
if (preview && figure != nextFigure) {
nextFigure.attach(previewBoard, true);
nextFigure.detach();
} else {
previewBoard.clear();
}
break;
}
}
/**
* 무작위 도형을 반환합니다. 도형은 figures 배열에서 가져오며
* 초기화되지 않습니다.
*
* @return 무작위 도형
*/
private Figure randomFigure() {
return figures[(int) (Math.random() * figures.length)];
}
/**
* 게임 시간 스레드입니다. 이 스레드는 타이머 이벤트를 적절하게
* 발생시켜 현재 도형을 내리는 역할을 합니다. 이 스레드는 게임
* 도중에 재사용될 수 있지만 게임이 실행되지 않을 때는 일시정지
* 상태로 설정되어야 합니다.
*/
private class GameThread extends Thread {
/**
* 게임 일시정지 플래그입니다. 이 플래그는 게임이 일시정지되어
* 있는 동안 true로 설정됩니다.
*/
private boolean paused = true;
/**
* 각 자동 이동 전에 잠들 시간(밀리초)입니다.
* 이 숫자는 게임 진행에 따라 낮아집니다.
*/
private int sleepTime = 500;
/**
* 기
본값으로 새로운 게임 스레드를 생성합니다.
*/
public GameThread() {
}
/**
* 게임 스레드를 재설정합니다. 속도를 조정하고
* 게임 스레드를 이전에 시작되지 않았다면 시작합니다.
*/
public void reset() {
adjustSpeed();
setPaused(false);
if (!isAlive()) {
this.start();
}
}
/**
* 스레드가 일시정지 상태인지 확인합니다.
*
* @return 스레드가 일시정지 상태인 경우 true를 반환하고,
* 그렇지 않으면 false를 반환합니다.
*/
public boolean isPaused() {
return paused;
}
/**
* 스레드 일시정지 플래그를 설정합니다.
*
* @param paused 새로운 일시정지 플래그 값
*/
public void setPaused(boolean paused) {
this.paused = paused;
}
/**
* 현재 레벨에 따라 게임 속도를 조정합니다. 속도는 레벨에 따라
* 큰 단계로 초기에 빠르게 작동하고, 레벨이 증가함에 따라
* 작은 단계로 줄어듭니다. 레벨 10 이상은 추가적인 영향을 주지 않습니다.
*/
public void adjustSpeed() {
sleepTime = 4500 / (level + 5) - 250;
if (sleepTime < 50) {
sleepTime = 50;
}
}
/**
* 게임을 실행합니다.
*/
public void run() {
while (thread == this) {
// 타이머 이벤트 실행
handleTimer();
// 일정 시간 동안 슬립
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ignore) {
// 아무 작업도 수행하지 않음
}
// 일시정지 상태인 경우 슬립
while (paused && thread == this) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignore) {
// 아무 작업도 수행하지 않음
}
}
}
}
}
}
'Java' 카테고리의 다른 글
[JAVA] Swing을 이용한 스케치 기능 구현 (0) | 2023.09.12 |
---|---|
[Java] awt 테트리스 모듈화 - SquareBoard (0) | 2023.08.16 |
[Java] Thread와 Swing으로 디지털 시계 만들기 (0) | 2023.07.16 |
[Java] awt 테트리스 모듈화 - Configuration (0) | 2023.07.02 |
[Java] awt 테트리스 - 실전편 (0) | 2023.06.17 |