IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

자바 빈과 테트리스
목 차:
조각과 부분
TetrisPiece 클래스
TetrisBoard 클래스
TetrisGame 클래스
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
dW at JavaOne: 새천년의 게임
Robocode
Rock 'em, sock 'em Robocode!
US 원문 읽기
테트리스 게임의 구성 요소들을 자바 객체로 나누어 재사용 가능한 자바 게임 컴포넌트로 만드는 방법

Scott Clee
소프트웨어 엔지니어, IBM
2002년 3월

IBM 소프트웨어 엔지니어 (마음은 게이머)인 Scott Clee가 테트리스 게임 모델을 재사용 가능한 자바 빈 컴포넌트로 포장하는 간단한 방법을 소개한다. 일단 게임의 구성 요소들이 자바 객체들로 나누어지면 완전한 게임 모델 빈을 형성하도록 재조립될 수 있고, 실제로 어떤 테트리스 GUI에도 결합될 수 있다. forum에서 이 글에 대한 여러분의 생각을 저자와 다른 독자들과 공유하기 바란다.

한 친구가 자신은 새로운 프로그래밍 언어를 배울 때마다 그 언어를 사용해 테트리스 게임을 작성하는데 도전한다고 말한 적이 있다. 이러한 전통하에, 내가 처음 자바 언어를 사용한 프로그래밍 법을 배웠을 때 나도 같은 시도를 해 보기로 했다. 나의 첫번째 시도는 하나의 완전한 게임이긴 했지만 매우 간단하고 흉한 것이었다. 시간이 가고 내가 자바 설계와 개발에 더 많은 경험을 얻음에 따라 나는 GUI에서 게임 모델을 분리시켜 (Swing 컴포넌트와 유사하게) 테트리스 빈을 만들 수 있다는 사실을 알았다. 그래서 나는 그것을 해 보기 시작했다.

이 글에서 나는 테트리스 빈을 구축하고 구현하는 방법을 안내하겠다.

조각과 부분

테트리스는 자바 객체로 표현될 수 있는 몇 개의 구성 요소를 가지고 있다.:

  • The Tetris pieces 테트리스 조각
  • 조각을 가지고 있는 테트리스 판
  • 판 위의 조각 제어, 점수 관리 등을 하는 게임

이 요소들 각각을 좀 더 자세히 살펴보자.

테트리스 조각 : TetrisPiece 클래스
그림 1과 같이, Tetris 조각의 핵심 요소는 다음과 같다.:

  • 각 요소는 정확히 네 개의 블록으로 구성되어 있다.
  • 조각 내의 각 블럭은 테트리스 판 내에 (x.y) 좌표를 가지고 있다.
  • 각 조각은 0, 2, 4의 회전율을 가지고 있다.
  • 각 조각은 L, J, S, O, I, Z, 혹은 T 모양을 지닐 수 있다.

그림 1. 각 테트리스 조각에 네 요소가 필요함 : 블록, (x,y) 좌표, 회전율 및 모양
Four elements of each Tetris piece

테트리스 빈으로 무엇을 할 수 있을까?

다음은 테트리스 빈을 만든 후 내가 수행한 몇 가지 일들이다.

  • 빈의 두 인스턴스를 연결시켜 대결 게임을 만들다.
  • Netris라는 이름을 가진, 최초의 Psion Netpad용 테트리스 게임을 만들다
  • 빈을 애플릿에 통합시켜 브라우저와 호환되는 테트리스 게임을 만들다.

첫번째 두 아이템은 매우 간단한 시스템을 사용해 구현될 수 있다. 각 조각에 대해 중앙 블록을 선택하고 이 블록의 (x,y) 좌표를 저장하면, 조각 내의 나머지 블록들을 이 블록을 중심으로 한 상대 좌표로 저장할 수 있다. 이 방식은 중앙 조각에 대한 상대 블록으로 모양을 저장함으로써 조각의 어떤 형태도 기술할 수 있게 해준다. 중앙 지점은 java.awt.Point로 저장될 수 있고 상대 좌표는 java.awt.Point 배열에 저장될 수 있다. 좌표 계산을 쉽게 하기 위해, 중앙 조각을 (0,0) 상대 좌표를 가진 블록으로 저장할 수 있다.

조각 회전시키기

이 시스템은 또한 조각의 회전을 계산할 때 더 쉽다. 간단한 행렬 조작을 사용하여 단순히 y좌표를 x 좌표로 바꾸고 x 좌표는 y 좌표의 마이너스 값으로 바꾸면 조각을 시계방향으로 90도 회전시킬 수 있다. 우리는 중앙 지점을 중심으로 상대 좌표를 사용하고 있기 때문에, 여기에서도 마찬가지로 할 수 있다:


temp = x;
x = -y;
y = temp;

여러분은 또한 시계방향 회전을 3번 적용하면 조각을 시계 반대 방향으로 90도 회전시킬 수 있다. (믿지 못하겠다면 한 번 해보기 바란다.)

마지막으로, 모든 조각이 동일한 회전율을 가지는 것은 아니므로, 이 문제를 보충하기 위해 회전 기법을 체크해 보아야 할 것이다.

에게 L을 주시오

우리는 모든 유형의 조각을 표시하는데 동일한 TetrisPiece 클래스를 사용하기 때문에 이들을 구분할 방법이 필요하다. 이를 위해 우리는 몇 개의 정적인 int 생성자를 사용하여 다른 유형들을 표시하고 지역 변수가 조각의 유형을 저장하도록 한다. 다음은 이 생성자들 중 하나의 예이다:


public static final int L_PIECE = 0;

조각 움직이기

조각을 테트리스 판 위에서 이동시킬 것이므로 이를 위한 이동 메소드를 제공해야 한다. 몇 가지 이동 (이미 가능한 한 제일 오른쪽 끝에 와 있는데 다시 오른쪽으로 가려는 시도)은 불법적일 수 있다. 따라서 우리는 모든 이동 요청을 확인할 필요가 있다. 우리는 참조를 저장할 테트리스 판에서 이것을 구현할 것이다. 따라서 우리 클래스의 생성자는 여기에서 두 가지 매개 변수를 가질 것이다: 첫번째는 만들어진 조각의 유형이고, 두번째는 테트리스판에 대한 참조이다. 생성자에서 우리는 initalizeBlocks()이라는 private 유틸리티 메소드를 호출할 것인데, 이 메소드는 조각의 상대 좌표값을 각각의 조각 유형에 설정할 것이다.

이동이 합법적인지를 체크하는 간단한 방법은 판에서 조각을 떼내어 원하는 방향으로 이동시킨 후 맞는지 보는 것이다. 맞으면 조각을 보드의 새 위치에 둔다. 그렇지 않으면 이동을 취소하고 원래 있던 자리에 다시 둔다. 반환되는 값은 그 결과에 따라 true (이동이 맞으면)나 false(이동이 맞지 않으면)가 될 것이다.

좀 더 주의를 기울여야 할 이동의 한 유형은 조각이 떨어지는 경우이다. 떨어진다는 것은 조각이 판의 제일 아래쪽으로 바로 내려간다는 의미이다. 이를 위해 우리는 더 이상 움직일 수 없을 때가지 조각을 아래로 계속 이동시키는 while 루프가 필요하다. 그러면 조각은 그 위치에 배치될 것이다.

조각에 적용될 수 있는 다양한 이동들을 구별하기 위해 우리는 다음 예제에 나타난 것과 같이 몇 가지 추가적인 static int 생성자를 사용할 것이다.:


public static final int LEFT = 10;

조각이 맞는지 보기 위해 나중에 willFit() 메소드가 TetrisBoard 클래스에서 구현된 것이다.

TetrisPiece 마무리하기

마지막으로 TetrisPiece 클래스를 포장하기 위해 우리는 중앙 지점과 상대 좌표와 같은 몇 가지 변수에 대한 getters와 setters, 그리고 무작위 유형의 TetrisPiece 인스턴스를 반환할 getRandomPiece()라는 정적인 메소드를 가진다.

TetrisPiece 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

테트리스 판 : TetrisBoard 클래스

테트리스 판은 빈 블록과 색깔 있는 블록을 가지고 있는 2D 격자판으로 생각할 수 있다. 다양한 유형의 테트리스 조각들이 int 생성자에 의해 구별되기 때문에, 우리가 해야 할 일은 빈 블록의 값을 정의하는 것 뿐이고 우리는 판을 2D int 배열로 저장할 수 있다. 이 방식을 사용하면 판 내의 빈 블록은 다음에 의해 표시될 것이다.:


public static final int EMPTY_BLOCK = -1;

유연성을 유지하기 위해 판의 크기를 가변적으로 하겠지만 이것을 생성자 내에 정의할 것이다. 따라서 생성자는 열과 행의 수를 나타내는 두 ints를 받아들일 것이다. 그리고 나서 2D 배열 내의 모든 값을 기본적으로 빈 블록으로 만드는 resetBoard() 메소드를 호출할 것이다.

조각 추가 및 제거하기

조각들이 판에 추가되고 제거되기 때문에 우리는 addPiece()removePiece()메소드를 제공한다. addPiece() 메소드는 TetrisPiece()를 취하고 판에서 이 메소드가 차지하는 모든 위치의 값을 자신의 유형으로 설정함으로써 작동한다. removePiece() 메소드는 판의 값이 빈 블록의 값으로 설정된다는 점을 제외하면 비슷하다.

사용자에게 변경 통지하기

판에 변화가 있을 때 사용자가 알 수 있도록 하기 위해, 조각이 추가되거나 이동되었을 때 BoardEvent가 구동될 것이다. 이 이벤트를 듣는 클래스들에 대해 우리는 BoardListener 인터페이스가 필요한데, 이벤트가 구동되었을 때 이 인터페이스의 boardChange()메소드가 호출된다. 이 이벤트들은 화면 수정이 필요할 때 통지되도록 테트리스 판 GUI에 의해 사용될 수 있다. listener를 저장하기 위해 우리는 java.util.Vector를 사용할 것이고, listener를 추가/삭제하고 이벤트를 구동시키기 위한 관련 메소드를 제공할 것이다.

때때로 여러분이 조각을 추가하고 삭제할 때 BoardEvents를 구동시키는 것이 부적절할 수 있다. 조각이 떨어져야 할 때 (이 이동은 조각을 떨어뜨리기 위해 while 루프를 사용한다는 것을 기억하라)를 예로 들 수 있다. 이 경우 조각이 바닥에 부딪쳤을 때만 이벤트가 필요하다. 이를 용이하게 하기 위해 우리는 boolean 매개변수를 취하도록 addPiece() 메소드를 만들어 값이 true일 경우에만 이벤트가 구동되도록 할 것이다.

완성된 행 없애기

테트리스 게임의 중요 요소 중 하나는 한 행이 완료되면 그 행은 사라지고 그 위의 모든 행들이 내려온다는 것이다. 이를 위해 우리는 삭제될 행의 지수를 매개변수로 취하는 removeRow() 메소드를 제공할 것이다. 행이 없어진 후에 BoardEvent가 구동될 것이다.

TetrisBoard 마무리하기

private 변수들에 접근하기 위해 필요한 getter와 setter들 외에도, 우리는 하나의 메소드가 더 필요하다. 앞에서 설명willFit()가 그것이다. 이 메소드는 TetrisPiece를 매개변수로 취해 그 조각이 판에 맞는지 결정하기 위한 boolean 값을 돌려준다. 맞는다는 것은 그 조각이 판의 경계 안에 있고 판에서 그 조각이 맞춰질 곳에 있는 블록의 값이 비어 있다고 설정되어 있음을 의미한다. 이런 경우 true 값이 반환된다.

이제 TetrisBoard 클래스가 완성되었다.이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

100 피트 벽을 가진 테트리스?

어느날 나는 이 빈을 타워 블록의 점등 시스템에 연결시키고 빌딩의 측면을 따라 내려가면서 테트리스 게임을 하고 싶어졌다. 나는 누군가가 이렇게 했다는 것을 신문에서 읽은 후 내내 이것을 하고 싶어해 왔다.

테트리스 게임 모델 : TetrisGame 클래스

이제 테트리스 게임에서 사용되는 두 개의 주 컴포넌트를 만들었으므로, 이들을 모아 게임 로직을 만들면 된다.

GameThread 내부 클래스

게임의 흐름을 제어하기 위한 좋은 방법은 이것을 java.lang.Thread를 확장하는 내부 클래스에 내장시키는 것이다. 이 방식의 한 가지 장점은 게임 속도를 제어하기 위해 스레드 sleep 호출을 추가할 수 있다는 것이다. 또 다른 장점은 현재 주 애플리케이션 스레드가 자유롭기 때문에 하나의 GUI가 첨부될 때 색칠 문제가 없어진다는 것이다. 이 문제는 주 스레드가 계속 묶여 있어 색칠할 시간이 없을 때 때때로 발생할 수 있다.

스레드 내의 로직은 run() 메소드 내의 while 루프 속에 구현될 것이다. 루프는 계속해서 조각을 만들어 내고 조각을 더 이상 맞출 수 없을 때까지 이들을 게임판으로 떨어뜨릴 것이다. 이 때 fPlaying이라는 지역 boolean 변수가 false로 설정되어 루프를 끝내고 GameEvent를 구동시켜 게임이 종료되었음을 표시할 것이다.

while 루프 내에 fPaused의 boolean 값을 체크하는 if 절이 있다. 이 값이 true로 설정되었을 경우 루프는 계속 실행되겠지만 모든 게임 로직이 무시되어 종료되는 느낌을 줄 것이다. Boolean이 false로 다시 바뀌면 게임이 계속될 것이다.

우리는 한 번에 하나씩 떨어지는 조각에만 관심을 가지고 있으므로, 여기에 대한 참조를 저장할 fCurrPiece라는 변수를 만들 것이다. 이 변수가 null 값으로 설정되면 이전의 조각이 더 이상 아래로 내려갈 수 없으며 판의 최종 위치에 도착했음을 의미한다. 이 때 우리는 새 조각을 만들어 판의 맨 위 중앙에 둔다. fCurrPiece 변수가 null값이 아닌 모든 경우에 우리가 해야 할 일은 그 것을 한 위치로 떨어뜨리고 주어진 시간 동안 스레드를 휴면 상태로 만드는 것이다.

한 조각이 더 이상 움직일 수 없게 되었을 때 우리는 행이 완성되었는지 보아야 한다. 이를 위한 손쉬운 방법은 한 쌍의 중첩 for 루프를 사용하는 것이다. 이 for 루프의 바깥 쪽 루프는 행의 지수를 따라 작업하며, 안쪽의 루프는 지수 전체에 걸쳐 확인 작업을 수행한다. 만일 우리가 완성된 행을 발견하면, TetrisBoard 클래스에 구현된 removeRow() 메소드를 호출하며 완성된 행의 지수를 전달할 수 있다. 이제 제거된 행 위의 모든 행들이 하나씩 내려올 것이기 때문에 우리는 이들을 다시 체크해야 할 것이다. 여러 행을 한번에 완성하도록 장려하기 위해 우리는 완성된 행의 개수를 저장하고 각각 더 높은 점수를 줄 것이다.

테트리스 게임의 또 다른 주요 요소는 더 많은 행이 완성될수록 조각이 더 빨리 내려온다는 것이다. 이 기능은 지금까지 완성된 행의 개수를 체크하고 이에 따라 스레드의 휴면 주기를 점차 감소시켜가는 방식으로 구현될 수 있다.

GameThread 내부 클래스를 만들기 위해 필요한 것은 이것이 전부지만, 구현해야 할 또 다른 내부 클래스가 있다. 다수의 이벤트가 구동될 것이고 이들은 listerners가 저장되도록 요구할 것이므로, 이들을 모두 한 장소에 두는 것이 좋을 것이다. 우리는 EventHandler 내부 클래스를 사용하여 이를 수행할 것이다.

EnvetnHandler 내부 클래스
이 클래스는 우리가 구동하는 이벤트에 관심이 있는 listener들에 대한 참조를 저장할 것이다. listener들에 대한 addremove 메소드를 제공할 뿐 아니라 이벤트들을 구동하기 위한 유틸리티 메소드도 있을 것이다.

이 클래스는 다음 유형의 이벤트들을 다룬다. :

  • GameEvent : 게임이 시작되거나 멈출 때마다 구동된다. 게임 START 혹은 END를 표시하기 위한 값을 가지고 있다.

  • BoardEvent : 게임판에 변경 사항이 있을 때 구동된다. EventHandler 클래스에서 추가/삭제 listener 호출이 TetrisBoard 클래스로 전달된다.

  • ScoreEvent: 점수가 바뀔 때 구동된다.

구동될 수 있는 많은 다른 유형의 이벤트들이 있지만, 간편성을 위해 나는 위에서 설명한 이벤트들만 사용하였다. 우리가 구현할 수 있는 다른 이벤트에는 LineEvent가 있는데, 한 행, 혹은 여러 행이 완성되었을 때 구동되고 화면 애니메이션을 일으키는데 사용될 수 있다.

TetrisGame 마무리하기

이제 내부 클래스들을 완성하였으므로 TetrisGame 클래스의 나머지 부분을 설명해야 한다. 모든 자바 빈과 마찬가지로 우리는 매개 변수 없는 생성자가 필요하다. 이 생성자에서 우리는 EventHandler 클래스와 TetrisBoard 클래스의 인스턴스를 만들 것이다. TetrisBoard 클래스 10x20이라는 기본 사이즈를 가질 것이다.

게임 상태를 제어하기 위해 우리는 개시, 중지, 일시 중지 메소드를 사용할 것이다. startGame() 메소드는 모든 게임 변수를 리셋하고 ScoreEvent (이제 0으로 리셋됨)와 GameEvent (START라는 매개 변수 유형을 가짐)를 구동시킬 것이다. 또한 GameThread를 생성하여 개시할 것이다. stopGame() 메소드는 fPlaying 변수를 false로 바꾸어 GameThread가 끝나도록 하고 END라는 매개변수 유형으로 GameEvent를 구동시킨다. setPause() 메소드는 한 게임을 일시 중지시키는 역할만 한다.

필요한 모든 getters와 setters와 별도로, 구현할 메소드가 하나 더 있는데, move() 메소드가 그것이다. 이 메소드는 이동 방향을 매개변수로 취하는데, 이것은 TetrisPiece 클래스에서 나오는 생성자이다. move() 메소드는 게임이 진행중이며 일시 중지 상태가 아니라고 가정하고 이동하려고 시도한다. 그 이동이 아래로 떨어지는 요청인데 성공하지 못한다면 fCurrPiece가 null 값으로 설정되고 조각은 게임판 내의 현재 위치에 남아 있을 것이다. 그러면 GameThread는 새로운 조각을 생성한다.

TetrisGame에 대해서는 이게 전부이다. 이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드 받을 수 있다.

이제 완벽함을 위해 우리는 BeanInfo 클래스를 만들고 이 클래스들을 적절한 파일들로 채울 수 있지만, 여기에서는 그럴 필요가 없다. 우리가 필요한 것은 우리의 빈을 테스트할 간단한 GUI이고 참고 자료에 하나가 제공되고 있다. 이것은 테트리스 게임판을 그리기 위해 한 개의 내부 클래스를 사용하고 키 조작을 조정하기 위한 몇 가지 로직을 포함하고 있는 간단한 클래스이다. GUI는 javax.swing.JFrame에 표시되며, 선택적으로 java.applet.Applet이 될 수도 있다.

우리의 테트리스 빈을 테스트하기 위해 소스 파일을 푸는데, 디렉토리 구조를 그대로 두기 바란다. (왜냐하면 내가 빈 클래스들이 TetrisBean 디렉토리에 있도록 이들을 TetrisBean 패키지에 두었기 때문에 때문이다.) 여러분이 소스 파일을 푼 경로를 자바 클래스 경로에 추가하고 파일을 컴파일한다. 이제 여러분이 해야 할 일은 "java Scottris"를 실행시키는 것 뿐이다.

나는 이러한 테트리스 빈을 구현할 수 있는 많은 방법이 있다는 것을 알고 있다. 내가 이 글에서 소개한 것은 아주 간단한 방법이며, 이것이 여러분의 창조력에 불을 붙일 수 있기를 바란다. 나는 여러분이 이것을 자유롭게 개선시키기 바란다.

참고자료

필자소개
Photo of Scott CleeScott Clee는 현재 IBM의 CICS 제품에 대한 FV Tester로 일하고 있다. 4년간 자바 프로그래머로 일했으며 자바와 관련된 재미있는 프로젝트를 취미 삼아 수행하는 것을 즐긴다.


이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의