본문 바로가기

OOP, FP/스칼라로 만드는 테트리스

04. [스칼라 테트리스] GUI, 스윙 컴포넌트 이벤트 분석 -2-

이번에는 이벤트를 감지하여 블록의 움직임, Row 클리어, 화면 갱신, 게임 오버 등의 동작을 발생시키는 클래스들을 분석할 차례이다.

  1. Board 라는 클래스에서 2차원 배열로 Block을 담는다.

    1. 1차원은 Board의 y, 2차원은 Board의 x를 표현한다.

    2. Board 클래스는 이름이 같은 짝 객체(Companion Object를 갖는다)

      1. 보통 팩토리를 만들 때 짝 객체를 사용한다.


1

2

3

4

5

6

class Bar(foo: String)

object Bar {

 def apply(foo: String) = new Bar(foo)

}

// Bar()를 이용하여 'new' 키워드를 사용하지 않고 새 객체를 만들 수 있다.

cs


      1. width, height를 속성으로 갖는다.

      2. EmptyBoardRow 속성은 보드의 width만큼 Block.EMPTY를 채운 변수이다.

      3. emptyBoard 메소드는 height만큼 EmptyBoardRow를 채운 2차원 배열을 반환한다.

        1. 2차원 배열을 1.에서 설명한 Block을 담은 배열이다.

    1. Board 클래스는 기본 생성자와 Block을 담는 2차원 배열을 받는 생성자가 있다.

      1. 기본 생성자는 1.a.iv의 emptyBoard를 2차원 배열로 전달하여 다른 생성자를 호출한다.

    2. withTetromino(tetromino: Tetromino): Board 메소드

      1. 인자로 받은 테트로미노의 블록을 그린다.

      2. 1.a에서 설명한 대로 position._2를 y값으로, position._1를 x값으로 사용한다.

      3. 블록을 그린 뒤, 해당 Board를 반환한다.

    3. overlap(tetromino: Tetromino): Boolean 메소드

      1. 인자로 받은 테트로미노의 블록 positions을 기반으로 Array(y)(x)에 위치에 있는 블록이 Block.EMPTY인지 비교하여, 하나라도 EMPTY가 아니면 false를 반환한다.

      2. 즉, 인자로 받은 테트로미노 블록의 위치에 이미 어떤 블록이 존재하는지 검사하는 메소드이다.

    4. isLegal(tetromino: Tetromino): Boolean 메소드

      1. 인자로 받은 테트로미노 블록의 position를 기반으로 x, y값이 보드의 width, height 범위에 포함되는지 검사한 뒤, overlap을 호출하여 해당 위치에 블록이 존재하는지 검사하여, 해당 블록의 positions 중 하나라도 올바른 위치에 있지 않거나, 하나라도 EMPTY이면 false를 반환한다.

      2. 즉, 인자로 받은 테트로미노 블록이 올바른 위치에 있는지와 overlap되어 있는지를 검사하는 메소드이다.

    5. clearFullRows: Int 메소드

      1. clearedBoard 변수

        1. 1.a에서 Board를 생성할 때 인자로 받은 2차원 배열의 중 Block.EMPTY를 포함하는 요소만 필터링 하여 저장한다.

        2. Board의 2차원 배열에서 한 칸이라도 비어있지 않은 요소는 제거하여 저장한다.

        3. 즉, 블록으로 꽉 차서 지워야되는 row를 제외시킨다.

      2. clearedRows 변수

        1. Board 짝 객체의 Height 속성에서 clearedRows를 뺀 값을 저장한다.

        2. 전체 높이에서 빈 행을 포함한 요소들의 크기를 빼면 화면에서 지워야하는 row의 갯수가 된다.

        3. 즉, 몇 개의 row를 클리어하는 지 계산한 값을 저장한다.

      3. 1. a에서 설명한 2차원 배열을 채운다.

        1. Board.EmptyBoardRow를 clearedRows 값만큼 채운다.

      4. 2차원 배열을 채운 뒤, clearedBoard를 해당 배열에 추가한다.

      5. 즉, 현재 화면(2차원 배열)에서 클리어 되지 않는 2차원 배열과, 클리어 될 rows의 개수를 구하여 꽉 찬 rows를 클리어하여 갱신시키는 메소드이다.

    6. clone 메소드

        1. Object 의 clone을 오버라이드 시킨 메소드.

        2. 인자로 받은 Board와 똑같은 요소를 가진 새로운 Board를 만들어서 반환한다.

  1. BoardController 라는 클래스에서 블록의 동작과 게임 이벤트를 제어한다.

    1. Board 클래스의 인스턴스를 속성으로 갖는다.

    2. Tetromino 클래스의 인스턴스 두 개를 속성으로 갖는다.

      1. 현재 블록을 의미하는 currentTetromino

      2. 다음 생성될 블록을 의미하는 nextTetromino

    3. 현재 점수를 속성으로 갖는다.

      1. 점수가 갱신 되는 시점은 보드의 row가 클리어 되는 시점이다.

    4. Timer 클래스의 인스턴스를 속성으로 갖는다.

      1. Timer 클래스는 interval로 제시된 시간마다 인자로 받은 이벤트를 실행한다.

    5. 게임 진행 중, 게임오버 등의 상태를 Boolean 속성으로 관리한다.

    6. 게임루프 속성

      1. Timer 클래스의 이벤트로 사용하기 위해 ActionListener의 익명 클래스를 생성한다.

        1. ActionListener의 actionPerformed를 구현하여 이벤트를 정의한다.

      2. currentTetromino.withMoveDown를 이용하여 새로운 테트로미노를 생성한다.

      3. board.isLegal()을 이용하여 새로운 테트로미노의 규칙성을 검사한다.

        1. isLagl() 을 통과할 경우 currentTetromino에 새로운 테트로미노를 대입한다.

        2. 새로운 블록의 positions에 이미 다른 블록이 있을 경우

          1. placeTetromino()를 호출한다.

            1. 화면과 점수를 갱신한다.

            2. 현재 블록에 다음 블록을 대입한 뒤, 다음 블록에 새로운 테트로미노를 생성한다.

          2. board.isLegal()을 이용하여 currentTetromino를 검사한다.

            1. isLegal을 통과하지 못할 경우 게임오버액션을 보여준 뒤 게임을 멈춘다.

    7. getTickInterval(score: Int): Int 메소드

      1. 이벤트가 발생되는 인터벌을 score점수에 기반하여 반환한다.

      2. 이로 인해 스코어가 올라갈 수록 속도가 진행속도가 빨라진다.

    8. def setScore(newScore: Int): Unit 메소드

      1. score를 새로 설정한다.

      2. Timer의 interval도 새로 설정한다.

    9. def tryMove(tetromino: Tetromino): Unit 메소드

      1. 게임이 진행 중 일 때, board.isLegal()을 통과해야만 인자로 받은 테트로미노를 움직인다.

    10. droppedTetromino: Tetromino 메소드

      1. board.isLegal()을 통과할 경우 board의 제일 바닥부분으로 currentTetromino를 이동한 테트로미노를 반환한다.

    11. dropTetromino: Unit 메소드

      1. 게임이 진행 중일 때, droppedTetromino()를 호출하여 블록을 바닥으로 이동 시킨다.

      2. placeTetromino()를 호출하여 board의 점수와 블록을 갱신한다.

    12. placeTetromino: Unit 메소드

      1. board의 점수와 현재블록, 다음블록을 갱신하는 메소드

      2. setScore()에 현재 점수와 board.clearFullRows의 결과값을 전달하여 점수를 재설정한다.

      3. 현재 테트로미노에 다음에 생성될 테트로미노를 대입한다.

      4. 다음에 생성될 테트로미노에는 새로운 테트로미노를 생성한다.

    13. pauseGame: Unit 메소드

      1. gameRunning을 false로 변환한 뒤 Timer.stop으로 잠시 게임을 중단시킨다.

    14. resumeGame: Unit 메소드

      1. 게임오버가 아닌 경우, gameRunning을 true로 변한한 뒤 Timer.start로 게임의 일시정지를 해제한다.

    15. newGame: Unit 메소드

      1. placeTetromino()를 호출하여 board의 점수와 블록을 갱신한다.

      2. 2.a 의 board를 new Board로 새로 생성한다.

        1. 현재 board는 전체 rows가 block.EMPTY로 채워진다.

      3. setSocre(0)을 호출하여 점수를 0으로 갱신한다.

      4. gameOver의 상태를 false로 변환한 뒤 resumeGame()을 호출한다.

        1. gameOver가 아니므로, 게임이 다시 시작된다.

    16. togglePause 메소드

      1. paseGame과 resumeGame을 분기하는 메소드

      2. 게임이 진행 중일 경우 pauseGame을 호출하여 게임을 중지한다.

      3. 게임이 진행 중이 아닐 경우 resumeGame을 호출하여 게임의 일시정지를 해제한다.

    17. repaint 메소드

      1. tryMove()와, 게임루프에서 사용되며 패널의 repaint를 호출하여 panel을 갱신한다.

    18. 이벤트 설정

      1. Reactor 라는 Trait을 이용하여 이벤트를 설정할 수 있다.

        1. reactions 의 += 메소드를 이용하여 이벤트를 추가한다.

        2. listenTo(ps: Publisher*) 를 이용하여 어떤 컴포넌트의 이벤트를 감지할지 결정한다.

      2. case KeyPressed(_, Key.Down, _, _) => tryMove(currentTetromino.withMoveDown)

        1. 위와 같이 reacions에 방향키 아래 버튼을 눌렀을 경우 무슨 액션을 취할지 지정한다.

      3. 현재 게임에는 상, 하, 좌, 우 방향키와 스페이스, p(pause), n(newGame)의 키 이벤트가 설정되어 있다.


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

reactions += {

   case KeyPressed(_, Key.Down, _, _) => tryMove(currentTetromino.withMoveDown)

   

   case KeyPressed(_, Key.Space, _, _) => dropTetromino

   case KeyPressed(_, Key.Left, _, _) => { /*println(currentTetromino.getBlockPositions); println(currentTetromino.position);*/ tryMove(currentTetromino.withMoveLeft) }

   case KeyPressed(_, Key.Right, _, _) => tryMove(currentTetromino.withMoveRight)

   case KeyPressed(_, Key.Up, _, _) => tryMove(currentTetromino.withRotation)

   case KeyPressed(_, Key.P, _, _) => togglePause

   case KeyPressed(_, Key.N, _, _) => newGame

 }

Colored by Color Scripter

cs


  1. GameOverAnimation 라는 클래스를 이용하여 게임오버 시의 애니메이션을 정의한다.

    1. Timer 클래스의 인스턴스를 속성으로 갖는다.

      1. 게임오버루프

        1. Timer 클래스의 이벤트로 사용하기 위해 ActionListener의 익명 클래스를 생성한다.

          1. ActionListener의 actionPerformed를 구현하여 이벤트를 정의한다.

        2. BoardController의 gameRunning이 true일 경우, 즉 게임이 다시 시작된 경우 타이머를 정지하여 애니메이션을 중지한다.

        3. board의 row를 바닥부터 돌면서 순회한다.

          1. 각각의 row를 Block.nextBlock를 이용해 랜덤한 블록으로 채워간다.

          2. 이렇게 되면 화면이 전체 랜덤한 블록으로 꽉 차게되어, 종료 애니며이션을 표현할 수 잇다.

        4. BoardController의 repaint를 호출하여 패널을 다시 그린다.


4번의 포팅을 거쳐 2개의 데이터 구조, 5개의 GUI 클래스를 분석해보았다.

스칼라 코드에 대한 이해가 부족한 상태에서 시작해서 골치가 많이 아팠다.

코드에 메소드의 용도나 파라미터, 반환값에 대한 설명만 적혀있었어도 좀 더 수월했을텐데...

그래도 모르는 부분을 한줄 한줄 떼어서 테스트하면서 분석하다보니 진행은 가능했다.

많이 비효율적인 진행이였지만, 익숙하지 않은 스칼라만의 표현 방식이나 문법을 다시 공부할 수 있어서 괜찮았다.