본문 바로가기

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

11. [스칼라 테트리스] 07. 메인 프레임, 테트리스 패널

메인 프레임


스윙 애플리케이션에서 전체 화면의 모양을 담당한다. 프레임은 내부에 다른 컨테이너를 담을 수 있는 최상위 컨테이너 중 하나로, 윈도우나 메뉴를 가지는 일반적인 데스크탑 애플리케이션에 적합하다.

* 최상위 컨테이너란 다른 컨테이너에 포함되지 않고도 화면에 출력되어 독립적으로 존재할 수 있는 컨테이너이다.


메인 프레임은 가장 상단에 메뉴바가 존재하며 내부 컨테이너로 패널을 담는다.


메뉴는 위의 그림처럼 새 게임, 일시정지, 도움말 보기 그리고 게임 종료 4가지의 아이템이 존재한다.


위 창은 메뉴바에서 Help 아이템을 클릭했을 시 띄워지는 다이얼로그 창이다. 스윙에서 다이얼로그는 프레임과 마찬가지로 최상위 컨테이너 중 하나이며, 메뉴가 없는 일반적인 대화 상자 형식의 간단한 애플리케이션에 사용된다.


구현


1
2
3
4
5
6
7
8
9
10
object Scaltris extends SimpleSwingApplication {
  val boardPanel = new TetrisPanel
  
  def top = new MainFrame {
    title = "Scaltris-net"
    
    contents = boardPanel
    }
  }
}
cs

스윙 GUI를 사용하기 위해 SimpleSwingApplication을 상속하는 객체를 만든다. 이후 패널과 프레임을 생성한 뒤 패널을 프레임의 내부 컨테이너로 지정한다.


1
2
3
4
5
6
7
8
9
menuBar = new MenuBar {
  contents += new Menu("Game") {
    contents += new MenuItem(Action("New game") { boardPanel.controller.newGame })
    contents += new MenuItem(Action("Pause") { boardPanel.controller.togglePause })
    contents += new MenuItem(Action("Help") { showHelp })
    contents += new Separator
    contents += new MenuItem(Action("Exit") { sys.exit(0) })
  }
}
cs

가장 먼저 상단의 메뉴바를 구현해야 한다. 메뉴바를 생성하고 메뉴를 생성한 뒤 RichWindow의 menuBar 함수를 호출한다. 위의 코드는 menuBar_=(new MenuBar { ... }) 와 같다.


그럼 이제 각각 메뉴 아이템에 대해 살펴보자.

New Game 아이템은 테트리스 패널의 newGame을 호출하여 새로운 게임을 시작한다. 

Pause 아이템은 테트리스 패널의 togglePause를 호출하여 일시정지/재시작을 번갈아 수행한다.

Help는 앞에서 설명한 도움말 다이얼로그창을 보여주는 showHelp 함수를 호출한다.

Exit는 sys.exit(0)을 호출하여 애플리케이션을 종료한다


1
2
3
4
5
6
7
8
9
10
11
12
13
val helpText = """Left: Move left
                 |Right: Move right
                 |Up: Rotate
                 |Down: Move down
                 |Space: Drop all the way down
                 |P: Toggle pause
                 |N: New game""".stripMargin
                 
def showHelp: Unit = {
  boardPanel.controller.pauseGame
  Dialog.showMessage(boardPanel, helpText, "Scaltris-net help", Dialog.Message.Plain)
  boardPanel.controller.resumeGame
}
cs

showHelp 함수는 도움말 다이얼로그를 오픈하기 위한 함수이다. 게임을 잠시 일시정지 시킨 뒤 패널에 다이얼로그 창을 띄운다. 다이얼로그 창이 종료되면 게임을 재시작한다.


* /"""...""".stripMargin 은 무슨 구문일까?

우선 """(쌍따옴표 3개)는 여러 라인에 스트링을 표현하기 위한 구문이다. 여기에 stripMargin을 사용하면 |앞의 공백을 스트링에 포함하지 않게 할 수 있다. 자바스크립트에서 문자열을 여러 줄에 정의할 때, 각 라인의 끝에 \를 사용하는 것과 비슷한 용도라고 보면 될 것 같다.



테트리스 패널


패널이란 프레임과 같은 컨테이너로, 다른 컴포넌트를 포함할 수 있는 GUI 컴포넌트이다. 최상위 컨테이너가 아니므로 프레임과 같은 최상위 컨테이너에 포함되어야 한다.


* 최상위 컨테이너와 달리 일반 컨테이너는 다른 컨테이너에 포함되어야 한다. 다른 계층 구조로 컴포넌트라는 GUI 객체가 있는데, 컴포넌트는 컨테이너와 달리 다른 GUI 요소를 포함할 수 없으며 컨테이너에 포함되어야 한다.


구현


프레임으로 메뉴바를 구현하고 패널을 포함하였으므로 이제 그 내부인 패널을 구현해야한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class TetrisPanel extends Panel {
  val controller = new BoardController(this)
  controller.listenTo(this.keys)
  
  /**
   * The extra width is for the next tetromino and scores
   */
  preferredSize = new Dimension(Block.BlockSize * (Board.Width + 5),
                                Block.BlockSize * Board.Height)
  
  override def paintComponent(g: Graphics2D) {
    super.paintComponent(g)
  
    g.setPaint(Color.black)
    g.fillRect(00, Board.Width * Block.BlockSize, Board.Height * Block.BlockSize)
  }
}
cs

패널 클래스는 java.awt.Panel을 상속한다. 

이벤트를 위한 보드 컨트롤러를 생성한 뒤, listenTo 메소드를 호출하여 패널에서 키입력을 감지한다.


1
preferredSize_=(x: Dimension) = peer setPreferredSize x
cs

prefferedSize 함수에 Demension을 전달하여 패널의 크기를 설정한다.


패널을 생성한 뒤, 이벤트 감지를 설정하고 크기를 지정하였으므로 이제 패널의 컴포넌트를 붙여나간다.


가장 먼저 paintComponent를 구현하여 컴포넌트가 화면에 그려지는 방법을 정의한다.

파라미터로 넘어온 그래픽 객체에 배경을 black으로 설정하고 fillRect를 호출하여 패널의 일정 부분을 검정색으로 채운다.


고스트 블록

고스트 블록은 현재 블록을 바닥에 위치시킨 블록이다. 현재 블록의 모양과 x 좌표를 그대로 갖는다. 원래는 블록을 투명하게 표시해야 하지만, 아직 블록의 이미지를 적용하지 않았으므로 우선은 그대로 표시한다.


1
2
3
4
5
6
7
8
val ghostTetromino = controller.droppedTetromino
 
ghostTetromino.getBlockPositions.foreach {
  case (x, y) =>
    val img = Block.getBlockImage(ghostTetromino.block)
    
    g.drawImage(img, null, x * Block.BlockSize, y * Block.BlockSize)
}
cs

컨트롤러로부터 현재 블록이 바닥에 떨어진 모습의 블록을 구한 뒤 블록의 좌표값과 크기를 이용하여 그래픽 객체에 표시한다.


현재 블록

사용자의 키 입력과 게임이 진행됨에 따라 모양과 좌표값이 실시간으로 변경되는 현재 블록이다.

현재 블록은 다른 블록과 달리 보드의 2차원 배열을 기반으로 그려져야한다.


1
2
3
4
5
6
7
8
9
10
controller.board.withTetromino(controller.currentTetromino).board.zipWithIndex.foreach {
  case (row, y) => row.zipWithIndex.foreach {
    case (block, x) => {
      if (block != Block.EMPTY) {
        val img = Block.getBlockImage(block)
        g.drawImage(img, null, x * Block.BlockSize, y * Block.BlockSize)
      }
    }
  }
}
cs

보드의 withTetromino를 호출하면서 컨트롤러로부터 얻은 현재 블록을 전달한다. 현재 보드를 복사하여 해당 보드의 2차원 배열에 현재 블록을 채운 뒤 복사된 보드가 반환된다. 이제 보드의 2차원 배열을 이용하여 패널에 표시해야한다.

첫 번째 foreach 문은 2차원 배열 board의 zipWithIndex와 함께 호출하여 row의 인덱스와 x배열을 얻는다.

두 번째 foreach 문은 1차원 배열 x배열의 zipWithIndex와 함께 호출하여 block의 인덱스와 block을 얻는다.

이후 두 번째 foreach 문에서 row 인덱스와 block 인덱스를 이용하여 그래픽 객체에 표시한다.


다음에 생성될 블록과 현재 점수

화면 우측에는 다음에 생성될 블록과 현재 점수를 표시한다.


1
2
3
4
5
6
7
8
9
10
g.drawString("Next tetromino:", (Board.Width + 1* Block.BlockSize, Block.BlockSize / 2)
val nextTetromino = controller.nextTetromino
nextTetromino.getBlockPositions.foreach {
  case (x, y) => {
     val img = Block.getBlockImage(nextTetromino.block)
     g.drawImage(img, null, (Board.Width / 2 + 2 + x) * Block.BlockSize, (y + 1* Block.BlockSize)
  }
}
 
g.drawString("Score: %d".format(controller.score), (Board.Width + 1* Block.BlockSize, Block.BlockSize * 6)
cs

미리 텍스트를 표시한 뒤, 컨트롤러로부터 다음 블록을 가져온다.

그 블록의 위치값을 이용하여 그래픽 객체에 미리 표시한 텍스트의 밑 부분에 블록을 그린다.


마지막으로, 다음 블록이 표시된 위치의 밑 부분에 컨트롤러의 점수를 표시한다.


정리


지금까지 myrjola의 scaltris 를 직접 분석하고 타이핑하여 테트리스 싱글 애플리케이션을 구현했다.

이제 소켓을 이용하여 멀티가 가능하게끔 확장해야 한다. 다음 포스팅에서 이를 위해 필요한 요구사항과 기술을 분석해보자.