본문 바로가기
Java

[Java] awt 테트리스 모듈화 - Game

by teamnova 2023. 7. 26.

저번 글 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) {
                        // 아무 작업도 수행하지 않음
                    }
                }
            }
        }
    }
}