자바로 구현하는 애플릿 게임 1

일반입력 :2001/03/05 00:00

김상우

난 95년 썬에 의해 개발돼 모습을 드러낸 자바는 현재 다른 어떤 프로그래밍 언어보다 빠른 속도로 확산되고 있다. 이렇게 큰 호응을 얻을 수 있었던 것은 C++와 거의 비슷한 문법적 구조를 가지면서 C나 C++가 가지고 있던 메모리 관리 문제 등의 단점들을 제거함으로써 프로그래머의 짐을 덜어주었을 뿐더러 다른 어떤 언어보다도 객체지향적 프로그래밍에 적합하기 때문이다. 또한 언어 차원에서 멀티쓰레드를 지원한다는 것과 플랫폼 독립적이라는 특성, 그리고 이것을 활용한 웹페이지 상에서의 애플릿 프로그램 작성을 가능하게 해 줌으로써 인터넷의 확산과 더불어 자바는 그 세력을 점점 넓혀가고 있다.

하지만 이러한 장점에도 불구하고 느린 네트웍 속도와 컴퓨터 환경, 자바의 특성인 실행시간 바이트 코드 해석을 해야 된다는 점 때문에 실행 속도가 늦어 아직까지 제한된 분야에서만 활용되고 있는 것이 현실이다. 이런 이유로 혹자는 자바로 빠른 처리를 필요로 하는 게임 작성은 불가능하다고까지 말하기도 한다. 하지만 과연 그럴까?

만약 그렇게 말하는 사람이 있다면 그는 아직 자바의 강력함을 모르고 있음이 분명하다. 자바는 비주얼 C++나 비주얼 베이직, 기타 다른 어떤 언어보다도 그래픽 분야에 대해서는 탁월한 능력을 가지고 있다. 물론 자바가 탄생한지 얼마 되지 않았고 아직 버전이 낮기에 전체화면 중 일부만을 다시 그린다든지 하는 기능에는 미흡한 부분이 있지만, 현재의 기능만으로도 게임을 만들기에는 부족함이 없다.

그리고 다른 언어보다 쉽고 안전하게 쓰레드를 사용할 수 있다는 점 역시 게임 작성에 더없이 큰 이점으로 작용하고 있다.

또 자바는 이러한 처리를 쉽게 할 수 있도록 다양한 클래스와 함수를 지원함으로써 프로그래머가 코딩량을 상당히 줄일 수 있도록 해주는 것도 장점으로 꼽힌다. 아마도 이 글을 읽는 독자들은 지금부터 만들어 나갈 게임의 최종 코드가 의외로 크지 않다는 사실에 놀라게 될지도 모른다. 그럼 지금부터 놀라운 자바 게임의 세계로 들어가 보기로 하겠다.

미리 알아둬야 할 것들

게임 개발 기법에 대한 설명에 앞서 이해를 돕고자 미리 알아둬야 할 사항 몇 가지를 언급하고 넘어가야 할 것 같다. 우선 이 연재에서 구현하게 될 게임은 전혀 새로운 것이 아니다. 아마 독자들도 대부분 알고 있을 '팡(Pang)'이라는 게임으로 1989년에 개발돼 90년대 초에 인기를 얻었던 프로그램이다. <화면 1>은 이 게임을 자바로 구현한 것의 일부분을 보여주고 있다. 필자가 이 게임을 선택하게 된 이유는 첫째, 이 게임이 고전에 속하므로 사용된 테크닉이 최근의 게임에 비해 그리 어렵지 않다는 것이며, 둘째 게임 룰이 간단명료한 편이라 구현이 쉽고, 마지막으로 이 게임의 자바 애플릿 버전이 이미 작성돼 있기 때문이다.

사실 앞으로 만들게 될 자바 버전의 팡 게임은 이미 Boris Granveaud라는 프랑스 개발자에 의해 만들어 졌었고 그의 홈페이지(http://myweb.worldnet.net/~granveau/PANG/Pang.shtml)에서 사용되고 있다. 필자는 우연한 기회에 이곳을 방문하게 됐는데 그가 만든 팡 게임이 완성도가 높았고, 또한 이 홈페이지에서 다운받은 자료에는 게임 구현에 필요한 이미지 파일과 사운드 파일이 모두 들어 있었으므로 이러한 자원을 활용한다면 게임 개발 시간을 상당히 단축할 수 있으리라 생각했다.

필자가 이러한 자원을 이용해 개발한 게임이 Boris Granveaud가 개발한 원작과 거의 유사한 형태를 갖추게 됐을 때 필자는 그에게 E-메일을 통해 그 게임을 필자 나름대로 재개발했으며, 그 과정에서 얻은 지식을 잡지에 기고하고자 한다는 것과 그러기 위해 원작에 포함돼 있는 자원들을 사용하고 싶다는 뜻을 알렸고, 고맙게도 그는 제안을 흔쾌히 받아들였다. 이 연재의 이해를 돕기 위해 또는 완성된 게임의 모습이 궁금한 독자라면 그의 홈페이지를 한 번 방문해 보는 것도 좋을 것이다. 다음으로 알아둬야 할 것은 전체적인 프로그램의 구성과 게임의 룰이다. 이 게임의 전체적인 구성은 <표 1>과 같이 크게 5부분으로 나눌 수 있다.

이러한 분류법은 90년대 초에 등장한 아케이드 게임에서 하나의 법칙처럼 여겨졌던 부분인데 필자와 동시대에 살고 있는 독자들도 이미 이러한 구성에 익숙하리라 생각된다. 필자 역시 이러한 분류에 따라 각각의 클래스를 설계해 작업했고, 이번 시간에는 이러한 각각의 창을 구현하는 방법과 여기서 사용된 테크닉을 중심으로 설명할 예정이므로 관심을 가지고 봐주길 바란다.

게임의 룰 역시 간단하다. 게임을 해본 독자라면 이미 알고 있겠지만 이 게임의 존재조차 모르고 있는 독자가 있을 수 있으므로 간략한 설명을 하는 것이 좋을 듯 하다. <화면 1>에서 보듯이 이 게임에는 공과 사람이 등장하는데, 사람은 게이머의 키 조작에 의해 움직이고 공은 게이머의 적이 된다. 공은 지구 중력을 받고 있는 것처럼 포물선을 그리며 튀어 다닌다. 사람이 화살을 쏴 공을 맞추면 공은 보다 작은 크기의 두 개의 공으로 분리되고 더이상 나눠질 수 없는 작은 크기가 됐을 때 화살에 맞으면 사라지게 된다. 주어진 시간 안에 화면 안의 모든 공을 없애는 것이 이 게임에서 승리하는 길이다. 또한 게임적인 흥미를 더하기 위해 여타 게임과 같이 각종 팁과 보너스가 존재한다. 이처럼 게임의 진행을 위해 공과 사람, 화살이 움직이고 충돌이 일어났는지를 판단하는 작업은 다음 회에 다룰 것이고 게임적인 요소를 갖추는 방법에 대해서는 마지막 회에 설명할 예정이다.

마지막으로 이 연재의 설명 방법과 변수 사용에 대한 양해를 구하고 싶다. 앞으로 설명하게 될 이 게임은 가능한 객체지향적인 설계를 따르려고 노력하다 보니 각각의 클래스에 오버라이드된 함수를 사용했고 비슷하지만 조금씩 다른 부분이 많이 존재하게 됐다. 이런 부분에 대해서 한정된 지면에 일일이 설명하는 것은 비효율적이라 생각해 진행에 따라 꼭 필요한 부분만 설명했으니 지면을 통해 언급되지 않은 부분은 '이달의 디스켓'의 소스 코드를 참조하기 바란다.

그리고 소스 코드를 보면 알겠지만 필자가 제작한 소스에서 사용한 변수의 명명법은 자바에서 사용하는 방법이 아닌 비주얼 C++에서 사용하고 있는 헝가리언 표기법을 사용했다. 이는 필자가 비주얼 C++ 개발자이기에 이러한 표기법에 익숙해서 이기도 하지만 자바 표기법이 큰 프로젝트의 개발에는 부적합한 면이 있다는 개인적인 생각을 가지고 있어 헝가리언 표기법을 사용했으니 이점 이해해주기 바란다.

기본 구조의 설계

우리가 구현하려는 게임은 웹페이지 상에서 애플릿으로 작동되는 것을 목표로 한다. 따라서 프로그램의 가장 큰 틀이 되는 것은 자연히 애플릿 클래스에서 상속받은 클래스가 될 것이다. 이렇게 만들어질 클래스의 이름을 PangApplet으로 정하도록 하자. 이 클래스는 앞서 설명한 분류에 따른 5개의 창을 보여주고 관리하게 될 것이다.

가장 큰 틀은 필연적으로 애플릿 클래스로 만들었지만 내부의 창들을 구성하는 것은 다양한 방법으로 구현될 수 있을 것이다. 가령 각각의 창들을 Container 클래스에서 상속받아 구현해 놓고 필요에 따라 Show와 Hide를 반복할 수도 있고, 또 다른 방법으로 이러한 자원의 사용없이 애플릿 내부에서 각각의 창을 그리는 함수를 만들어 놓고 필요한 함수를 호출하는 방법도 사용할 수 있다. 그러면 어떠한 방법을 선택할지를 놓고 각각의 장단점을 파악해 보도록 하자.

우선 Container 클래스에서 상속받아 각각의 창을 클래스로 구현하는 방법에 대해 생각해 보자. 이러한 방식의 가장 큰 장점은 창의 개수에 맞춰 클래스를 따로 설계할 수 있으므로 작업이 쉽고 관리가 용이하다는 장점이 있다. 또한 각각의 창에서 필요한 작업이 내부에 국한되므로 하위 객체들의 관리가 쉽다. 하지만 이처럼 컨테이너 객체를 사용할 경우에는 리소스의 낭비가 있고 화면의 깜빡임 현상이 발생할 수 있다.

가령 애플릿에서 Repaint가 발생할 경우 애플릿이 먼저 그려지는 작업이 발생하고 그 후 컨테이너 객체를 그리게 되는데, 이러한 작업이 반복되면 깜빡임이 발생하게 된다. 게임 프로그램의 경우는 1초에 수십 번의 화면 repaint가 일어나는 게 일반적인 현상이므로 이것은 큰 단점으로 작용한다.

다른 한 가지 방법인 애플릿에서 모든 창을 그리는 작업을 하는 경우를 생각해 보자. 이러한 방법은 그리기 속도를 높일 수 있고 중복된 그리기 코드가 필요하지 않다는 장점이 있지만, 5개의 모든 창과 내부에서 처리해야 하는 작업을 애플릿 클래스에서 모두 관리해야 하므로 하나의 클래스에 코드가 집중돼 관리가 힘들어지게 된다(Boris Granveaud의 원작 게임은 이러한 방법으로 구현됐다).

그래서 필자는 이러한 두 가지 방법의 장점만을 택하기 위해 전혀 새로운 클래스를 설계했다. 필자가 설계한 이 클래스는 Container 클래스는 아니지만 똑같은 기능을 제공하면서 애플릿의 그리기 기능을 분산시키는 역할을 하도록 구현했다. 필자는 이 객체가 자바의 모든 창 객체의 기반 클래스인 Component 클래스처럼 5개의 창 객체의 베이스 클래스 역할을 하도록 그 기능을 정의했고 이름 역시 VirtualComponent라 명명했다. 이번 호에서 설명할 각각의 창 객체들은 전부 이 클래스에서 상속받은 클래스이므로 이 클래스에 대한 이해는 반드시 하고 넘어가야 할 것이다.

이러한 클래스를 구현함으로써 PangApplet의 작업은 아주 간소화될 수 있다.

PangApplet클래스는 5개의 창 객체를 관리하고 현재의 작업 진행 순서에 따라 각각의 창 객체를 액티브시키고 화면을 그리거나 사용자 키 입력이 있을 때 이러한 처리의 실제 코드가 들어 있는 창 객체의 함수를 호출해 주기만 하면 되는 것이다. 이러한 구조로 설계된 애플릿과 창 객체의 관계를 도식화하면 <그림 1>과 같이 나타낼 수 있다.

<그림 1>에서 각각의 상자는 객체를 나타내며 연결선은 객체간의 관계와 관리 구조를 도식화한 것이다. 타이틀 창에서의 객체는 Image 객체일 것이고 게임 창에서 객체는 사람, 공, 화살 등이 될 것이다. 그림에서 보듯이 애플릿 클래스는 창 객체만을 다루면 되고 그 하부 객체들은 창 객체가 다루므로 애플릿은 이에 대해 신경 쓸 필요가 없게 된다. 이러한 설계 구조의 장점은 한 클래스에서 다른 클래스로의 관계가 단순화됨으로써 일부 클래스로 코드가 집중되는 현상을 막고 작업 단위를 나누어 할 수 있다는 장점이 있다. 즉 타이틀 창 클래스 내부에서의 작업과 수정이 다른 창 클래스의 작업에 전혀 영향을 주지 않으므로 관리와 공동 작업이 쉬워진다. 그러면 이렇게 설계된 PangApplet 클래스가 어떻게 구현됐는지 자세히 살펴보도록 하자.

PangApplet 클래스의 구현

PangApplet 클래스가 어떠한 상속 관계를 갖는지는 마소 독자라면 이미 짐작하고 있을 것이다. 이 클래스는 Applet 클래스에서 상속받았으며 Runnable 인터페이스를 사용하고 있다. Runnable 인터페이스를 사용하는 이유는 이 클래스가 다중 쓰레드를 지원해야 할 필요가 있기 때문이다. 가령 이러한 경우를 생각해 보자. 게임이 진행되는 중간에 공과 사람이 서로 충돌했는지, 또는 화살과 공이 충돌했는지를 판단하는 루틴이 있다고 하자. 이러한 루틴이 루프를 돌며 각각의 좌표를 계산하고 있다고 할 때 사용자가 키 입력을 하거나 화면이 무효화되어 다시 그려질 필요가 있는 경우가 발생했다면?

만약 단일 쓰레드라면 계산이 끝나기 전까지는 화면이 그려지지도 않을 뿐더러 사용자의 키 입력은 무시될 것이다. 우리가 제작하려는 게임이 고전적인 어드벤처 게임이면 이러한 것이 큰 문제가 되지 않겠지만 아케이드 게임 제작에서는 간과할 수 없는 큰 문제가 된다. 우리가 제작하려는 게임 프로그램은 이러한 처리를 동시에 가능하도록 하기 위해 다중 쓰레드를 사용하고 있다. 자바는 언어 차원에서 다중 쓰레드를 지원하므로 사용 방법이 다른 언어에 비해 훨씬 쉬울 뿐더러 안정적으로 작동한다. 만약 이에 대한 이해가 부족한 독자라면 지난 99년 3월호 마소 주니어의 자바 코너를 읽어보기 바란다.

PangApplet 클래스는 창 객체들을 관리하고 각각의 기능을 호출하는 중계자 역할만을 한다고 했다. 이러한 역할을 하는 코드를 간략히 나타낸 것이 바로 <리스트 1>이다. <리스트 1>은 설명을 위해 단지 2개의 창 객체만을 다루도록 간략화했다. 여기서 중요한 것은 현재 액티브된 창이 어떤 것이냐라는 것이다. 창 객체가 몇 개이든 상관없이 현재 화면에 보여지고 사용자의 명령을 기다리는 창은 단지 하나일 뿐이기 때문이다.

이러한 작업은 객체지향 프로그래밍에서는 쉽게 구현된다. 모든 창들이 VirtualComponent 클래스에서 상속받았으므로 모든 창 객체를 이 클래스로 치환해 사용하는 것이 가능하다. 이렇게 함으로써 현재 액티브돼야 할 창이 무엇인지를 판단하는 작업이 필요치 않게 된다.

그러면 액티브된 창의 전환은 어떤 식으로 하는 것일까? 그것은 changeRunnerStep()이라는 함수에 의해 행해진다. 각각의 창은 자신의 작업이 완료되면 애플릿의 이 함수를 호출하게 되고, 이 함수는 다음에 액티브 되어져야 할 창을 선택한다. 그러면 다른 코드들은 여기서 변경된 액티브 창에게 지금까지 하던 작업을 계속해서 수행하게 될 것이다.

이 클래스에서 사용된 중요한 테크닉 중 한 가지는 '버퍼를 사용한 그리기'이다. 아무리 빠른 그리기 코드라도 1초에 수십 번씩 다시 그리기를 수행하면 깜빡임 현상을 피할 수 없다. 그래서 이러한 깜빡임을 최소화하기 위한 테크닉이 바로 이미지 객체를 버퍼로 사용해 그리기를 하는 것이다. 이러한 기법은 윈도우 프로그래밍에도 많이 사용되는데, 화면에 그려질 내용들을 빈 이미지 객체에 미리 그려 넣고 마지막에 이 이미지를 화면에 뿌림으로써 그리기가 한 순간에 이루어지는 효과를 얻는 것이다. 이 클래스의 paint() 함수가 이러한 기능을 구현하는 코드의 전부인데 상당히 간단하다. 만약 비주얼 C++로 이 기능을 구현한다면 적어도 이보다 두 배 이상의 코드가 있어야 하는 것을 볼 때 자바가 그래픽 처리에 얼마나 효과적인지를 간접적으로 느낄 수 있다.

아마 <리스트 1>을 눈 여겨 본 독자라면 '이 클래스가 하는 일은 아무 것도 없다'라고 말할 것이다. 만약 그렇게 느꼈다면 이 클래스의 역할을 완벽히 파악한 것이다.

이 클래스가 하는 일은 단지 창 객체들을 관리해 주는 것이 전부이기 때문이다. 그러면 이제부터 독자들이 궁금해하던 실제 작업을 담당하는 창 객체를 구현해 보겠다.

다중 쓰레드와 창 객체

각각의 창 객체를 구현하고 이용된 테크닉을 알아보기 전에 먼저 PangApplet 클래스와 창 객체의 상호작용 방법에 대해 좀더 살펴볼 필요가 있을 것 같다. 다시 한 번 말하지만 각각의 창 객체는 VirtualComponent를 공통 기저 클래스로 사용한다고 했다.

그리고 PangApplet 클래스는 이 클래스에서 상속된 창 객체들을 관리하고 자신이 처리해야 할 일들을 창 객체에 전달하는 역할만을 한다. 그러면 이 두 클래스들이 상호작용하기 위해서는 어떠한 작업이 필요한 것일까?

우선 다중 쓰레드를 사용한 이유와 이로 인해 고려해야 될 문제에 대해 다시 한 번 짚어보도록 하자. 다중 쓰레드를 사용해야만 하는 이유는 이미 앞에서 설명됐으므로 여기서는 이로 인해 발생되는 문제와 이를 위해 해줘야 할 작업에 중점을 두고 알아보자. 우선 다중 쓰레드를 고려하지 않은 상태에서 PangApplet 객체와 창 객체와의 관계를 <리스트 1>을 상기하며 생각해 보자.

<리스트 1>에서 보듯이 PangApplet 객체에서 Applet 클래스로부터 오버라이드를 해 사용된 함수는 update(), paint() 함수와 keyDown(), keyUp() 함수뿐이다. 4개의 함수를 오버라이드했지만 결국 이것은 그리기와 키 입력 처리라는 두 가지 문제로 생각할 수 있다. PangApplet 클래스는 이러한 이벤트를 받아 직접 처리하지 않고 5개의 창 객체 중 액티브 된 하나의 창 객체로 그 처리를 넘겨 버린다. 창 객체는 이러한 요구를 처리하기 위해 똑같은 이름을 가진 함수를 만들어 놓았고 자신이 표시해야 할 내용을 표시하고 그때의 상황에 맞게 키 입력을 판단하고 처리하게 된다.

하지만 이러한 작업만으로는 게임이 진행되지 않는다. 키 입력에 상관없이 공의 좌표를 이동시켜야 하고 벽이 있으면 방향을 바꿔야 하고 사람과 충돌이 발생하면 게임을 중지시켜야 한다. 바로 이러한 처리를 위해 또 하나의 쓰레드가 필요한 것이다. 게임 진행에 필요한 이 쓰레드는(편의상 이 쓰레드를 게임 쓰레드, 앞에 설명한 쓰레드를 기본 쓰레드라 부르겠다)

PangApplet 내부에서 생성되고 이 클래스의 run() 함수에서 작업을 시작한다. PangApplet 클래스의 run() 함수는 현재의 진행 상태가 EXIT_STEP이 되기 전까지 무한루프를 돌며 액티브된 창 객체의 run() 함수를 호출하는 작업을 반복한다.

만약 현재의 액티브 창 객체가 타이틀 창이라면 애플릿은 타이틀 창 객체의 run() 함수를 호출할 것이고, 이 타이틀 창의 run() 함수 작업이 완료돼 게임 쓰레드의 진행 상태가 이 함수를 벗어나 다시 애플릿의 run() 함수로 돌아오면, 그때 당시에 액티브된 창 객체의 run() 함수를 다시 호출하게 된다. 즉 현재의 액티브된 창 객체가 액티브 권한을 다음 창 객체로 넘겨주고 싶으면 자신의 run() 함수를 끝내기 전에 PangApplet 클래스의 changeRunnerStep() 함수를 호출하면 될 것이고, 그렇지 않고 자기 자신을 다시 한 번 실행시키고 싶다면 액티브 창 객체의 변경없이 자신의 run() 함수를 벗어나기만 하면 된다. 여기서 구현되어진 대부분의 창 객체는 전자의 방법을 사용하고 있지만 게임 창 객체는 후자의 방법을 사용하므로 이 둘의 사용방법을 잘 비교해 보길 바란다.

타이틀 창을 만들어 보자

그러면 이러한 개념을 가진 창 객체가 실제로 어떻게 구현되고 작동하는지 타이틀 창의 경우를 통해 자세히 알아보자. 설명을 위해 타이틀 창을 구현한 클래스의 이름을 TitleWnd로 정하겠다. 앞의 <표 1>에서 보았듯이 타이틀 창은 게임의 첫 화면으로 게임 명과 크레딧(프로그램이나 개발자에 대한 정보를 간략히 표현하도록 한 것)을 나타내는 역할을 하는 창 객체이다. 이러한 타이틀 창은 모든 게임 프로그램에 존재하는데, 사용자에 대한 첫인상인 만큼 가능한 한 화려하게 꾸미는 게 일반화돼 있다.

이 게임에서도 약간의 테크닉을 사용해 타이틀을 밋밋하지 않게 표현하도록 하고 있는데 그 방법으로 업-스크롤 기법을 사용했다. 업-스크롤 기법은 화면의 하단으로부터 어떠한 그림이 서서히 떠오르는 방식을 말하는 것으로 고전 게임에 많이 애용됐던 방법이다. 우리가 만들 타이틀 창이 해야 할 작업을 정리하면 다음과 같다.

1. Pang이라는 타이틀 그림을 업-스크롤 시킨다.

2. 최상단에 도달했을 때 폭발하는 듯한 타이틀 그림으로 변경하고 폭발음을 낸다.

3. 크레딧 그림을 타이틀과 겹치지 않도록 업-스크롤 시킨다.

이러한 테크닉 구현은 어렵지는 않지만 키 입력과 같이 생각한다면 다중 쓰레드 개념에 약한 독자에게는 방법이 언뜻 떠오르지는 않을 것이다. 하지만 PangApplet과 창 객체와의 관계가 잘 정립돼 있으므로 이 룰을 지키기만 하면 그리 어렵지 않게 구현할 수 있다. 우선 키 입력 부분을 먼저 구현해 보자. 고려해야 할 것은 타이틀이 보여지는 작업이 천천히 이뤄질 것이므로 사용자는 이러한 작업을 다 보지 않고 바로 게임으로 진입하고 싶어할 지도 모른다는 것이다. 그러므로 키 입력이 일어나면 PangApplet 클래스의 change RunnerStep() 함수를 호출해 액티브된 창 객체를 바꿔주고 현재 자신의 작업을 중단하고 run() 함수를 빠져나가야 한다. 이것을 고려해 타이틀 창의 키 입력 처리 코드를 작성한다면 다음과 같다.

public boolean keyDown(Event evt, int key)

{

// 리턴키이거나 스페이스키가 눌러졌다면

if (key == 32

{

m_bRun = false; // 실행상태 변수를 false로 만든다

if (m_Parent != null)

{

((PangApplet)m_Parent).changeRunnerStep(); // 액티브 창 객체를 변경한다

return true;

}

}

return super.keyDown(evt, key);

}

이 코드에서 눈 여겨 볼 부분은 m_bRun 변수를 false로 만드는 부분이다. 이 변수는 현재 키 입력을 처리하고 있는 기본 쓰레드를 위한 변수가 아니다. 앞서 설명했듯이 키 입력이 발생하면 Title Wnd는 하던 작업을 중단해야 한다고 했다. 여기서 얘기한 작업이란 것은 기본 쓰레드가 아닌 게임 쓰레드에서 처리하고 있는 것인데, 이 작업을 어떻게 중단시켜야 할 지 생각해 보자. 키 입력과 중단해야 할 작업의 처리가 서로 다른 쓰레드이므로 키 입력 쓰레드에서 다른 쓰레드의 작업을 강제로 중단시키는 방법은 없다.

하지만 기본 쓰레드가 게임 쓰레드에게 작업을 중단해야 된다는 신호는 보낼 수 있고 게임 쓰레드는 이러한 신호를 받아 자신의 작업을 중단하면 될 것이다. 이처럼 다른 두 쓰레드 사이에서 신호 전달 역할을 하는 변수가 바로 m_bRun 변수인 것이다. 이러한 신호는 타이틀 창 객체에만 필요한 것이 아니라 모든 창 객체가 필요한 것이므로 이 멤버 변수는 공통 기저 클래스인 VirtualComponent가 가지고 있도록 했다. 이번에는 타이틀 표시를 수행하는 게임 쓰레드 쪽에서 이 변수를 어떻게 사용하는지 확인해 보자.

public void run()

{

if (m_bRun)

{ // 타이틀 이미지 업-스크롤

m_nStep = 1;

drawTitle();

}

if (m_bRun)

{ // 폭발 효과음

if (m_PopAudio != null)

m_PopAudio.play();

}

if (m_bRun)

{ // 크레딧 업-스크롤

m_nStep = 2;

drawCredits();

}

// 사용자의 키 입력을 기다림

while(m_bRun)

try { Thread.sleep㊿; } catch(InterruptedException e) {}

}

앞에서 보듯이 run() 함수의 중간 중간에 m_bRun 상태를 체크하고 있다. 이렇게 함으로써 작업 중간에 자신의 작업을 중단하는 것이 가능해지고 기본 쓰레드와의 작업 동기화를 이룰 수 있다. drawTitle() 함수는 타이틀 그림이 표시될 좌표를 업-스크롤 하는 함수인데, 이 함수가 run() 함수에서 불렸으므로 drawTitle() 함수 내부에서도 m_bRun 변수의 값이 변경되는지를 계속 관찰해야만 한다. 이 함수의 중요 부분만을 정리해 보겠다.

protected void drawTitle()

{

for (int i = m_y + m_height ; i > m_y ; i -= 5)

{

// 종표시점 체크

if (!m_bRun)

return;

// 좌표가 변경됐으므로 다시 그리기를 하도록 한다.

m_Parent.repaint();

try { Thread.sleep㊿; } catch(InterruptedException e) {}

}

}

이 함수는 타이틀 이미지의 좌표를 루프문을 돌면서 변경하도록 하는데 루프를 돌 때마다 종료 트리거인 m_bRun을 체크한다.

그리고 좌표가 변경됐으므로 애플릿의 repaint() 함수를 호출해 다시 그리기를 하도록 하는데, 이 함수를 호출한다고 화면이 다시 그려지지는 않는다. 그 이유는 자바에서의 다중 쓰레드는 언어 차원에서의 지원이므로 한 쓰레드에서 다른 쓰레드로 제어권을 넘겨주는 작업을 코드 레벨에서 해줘야만 하기 때문이다.

앞에서 보여지는 sleep()이라는 함수가 바로 자신의 쓰레드를 잠시 suspend시키고 다른 쓰레드로 제어권을 넘기도록 해주는 것인데, 이 함수가 호출돼야만 비로소 PangApplet의 paint() 함수가 실행된다. 이러한 sleep() 함수는 게임 쓰레드 작업 중간 중간에 계속 등장하게 되는데 그것은 기본 쓰레드와 게임 쓰레드가 번갈아가며 제어권을 받게 하기 위해 꼭 필요하다. 게임 쓰레드는 좌표의 변경 작업만을 할뿐 직접적인 그리기는 기본 쓰레드에서 한다고 했는데 이제는 그 부분을 살펴보겠다.

run() 함수의 내부에서는 m_bRun 변수를 수시로 체크함과 동시에 하나의 작업이 진행될 때마다 m_nStep 변수의 값을 변경한다는 점에 주목해야 한다. m_bRun 변수가 중단 시점을 알기 위해 필요한 변수인 반면, m_nStep 변수는 현재 작업이 어디까지 진행됐는지를 판단하기 위해 사용되는 변수이다. 그리기의 시작점은 게임 쓰레드가 아닌 기본 쓰레드이므로 다시 그리기를 할 때 현재 어떤 부분까지 그려진 상태이고, 어디서부터 그려야 할지를 기본 쓰레드가 알 수 있도록 해 줄 필요가 있다.

우리는 빠른 그리기를 실행하기 위해 이미지 버퍼를 사용하기로 했는데 이미지 버퍼의 장점 중 하나는 이미 그려진 부분은 이미지 버퍼에 들어 있으므로 다시 그릴 시에는 아직 그려지지 않은 부분과 다시 그려야 할 부분만을 그리면 이전의 작업은 그대로 화면에 뿌려진다는 것이다. 그래서 각각의 그리기 코드는 현재 새로 그려져야 할 부분만을 판단하고 그리는데 이의 판단 근거가 되는 것이 바로 m_nStep 변수가 된다.

public void paint(Graphics g)

{

switch (m_nStep)

{

case 1: // 타이틀 그림을 그린다

{

g.drawImage(m_TitleImage, m_xImage, m_yImage, m_Parent);

break;

}

case 2: // 크레딧 그림을 그린다

{

g.clipRect(0, 300, m_width, m_height - 300);

g.drawImage(m_CreditsImage, m_xImage, m_yImage, m_Parent);

break;

}

}

즉 처음에는 타이틀 창만을 그리고 크레딧을 그릴 때는 타이틀 그림은 다시 그리지 않고 크레딧만을 그림으로써 그리기 속도를 향상시키는 것이다. 이 변수 역시 모든 창 객체에서 동일하게 사용되므로 VirtualComponent 클래스의 멤버 변수로 두었다. 이렇게 해서 완성된 타이틀 창이 바로 <화면 2>이다. 앞으로 설명될 모든 창 객체는 이 두 변수를 사용해 두 쓰레드 간의 동기화를 맞추므로 항상 머리 속에 기억하고 있어야 한다.

레벨 선택 창과 스테이지 선택 창

타이틀 창에 대한 이해가 완벽하다면 레벨 선택 창과 스테이지 선택 창은 아주 쉽게 구현할 수 있다. VirtualComponent의 기본 설계가 모든 선택창에 적용되도록 되어 있으므로 이 클래스의 함수들을 LevelWnd와 StageWnd 클래스에서 오버라이드해 자신이 처리해야 될 작업에 맞게 구현만 해주면 된다. 다시 말해 이제는 Pang Applet과의 관계를 고려하지 않고 VirtualComponent에 만들어 놓은 함수들을 보며 필요한 함수만 오버라이드하면 된다.

우리는 레벨 선택 창에 시각적인 효과를 주기 위해 네온 기법을 사용할 것이다. 네온 기법이란 특정 부분을 인위적으로 깜빡이도록 함으로써 사용자의 이목을 끌어내기 위해 사용되는 기법이다. 이 게임은 모두 7개의 레벨을 선택할 수 있는데, 각 레벨의 난이도에 맞는 문장들이 나열된다. 그리고 현재 선택된 레벨의 문장에 네온 효과를 준다. 문장을 단순히 보여줬다 감췄다 하는 것만으로도 깜빡임 현상을 만들어 낼 수 있지만, 그것보다는 문자열이 점진적으로 밝아짐과 어두워짐을 반복하는 것이 시각적 효과가 더 클 것이므로 이 방법을 택해 구현해 보겠다.

public void run()

{

m_nStep = 1;

while(m_bRun)

{

if (m_bDarker)

{

m_nColor -= 16;

if (m_nColor <= 128)

{

m_nColor = 128;

m_bDarker = false;

}

}

else

{

m_nColor += 16;

if (m_nColor >= 255)

{

m_nColor = 255;

m_bDarker = true;

}

}

m_Parent.repaint();

try { Thread.sleep㊿; } catch(InterruptedException e) {}

}

LevelWnd의 run() 함수는 아주 단순하다. 단지 무한루프를 돌면서 글자색인 m_nColor의 값을 증가시키거나 감소시키면서 화면이 다시 그려지도록 해주기만 하면 된다. 키 입력 처리 함수는 상, 하키가 입력되면 현재 선택된 레벨을 변경해주는 작업을 하고 그리기 함수는 7개의 레벨을 화면에 그려주면서 현재 선택된 레벨을 그릴 때는 run() 함수에 의해 변경되어지고 있는 m_nColor의 값을 Graphics 클래스의 setColor() 함수에 활용함으로써 네온 효과를 얻을 수 있다.

레벨 선택창의 작업이 쉽게 끝났으니 이제 스테이지 선택 창을 작업해 보자. 스테이지 선택 창의 기본적인 코드 구성은 레벨 선택 창과 크게 다르지 않다. <화면 4>에서 보듯이 상, 하, 좌, 우 키를 사용해 총 50개의 스테이지 중 시작하고자 하는 것을 선택할 수 있도록 해주면 된다. 한 가지 문자열이 아닌 화면 전체를 덮는 이미지를 배경으로 사용하고 있다는 점만이 다르다. 그러므로 여기서는 이미지를 다루는 부분만 짚고 넘어가도록 하겠다.

이미지를 다루기 위해 자바는 Image 클래스를 준비해 놓았다. 이 클래스의 함수들은 상당히 잘 정의돼 있어서 이미지를 읽어 들이고 수정하는 작업들을 쉽게 처리할 수 있도록 해준다. 또한 자바는 이미지를 다루는데 있어 웹 환경을 고려했다는 커다란 장점을 가지고 있다. 자바 애플릿 프로그램은 웹서버 상에 위치하지만 이것이 실행되어지는 곳은 사용자의 컴퓨터에서이다. 따라서 Image 클래스가 어떠한 그림 파일을 읽어 들이기 위해서는 그 파일이 존재하는 URL을 알아야 한다.

그래서 자바에는 URL이라는 클래스가 존재하며, 코드 베이스 주소나, 도큐먼트 베이스 주소를 알려주는 애플릿 함수가 준비돼 있고 Image 클래스나 Audio 클래스는 이러한 URL 클래스를 사용해 자원을 액세스할 수 있다. Image 클래스의 또 하나의 장점은 그림 파일을 읽어 들임이 끝나기 전에 화면 출력이 가능하다는 것이다. 다음의 코드는 URL을 사용해 이미지를 얻어오는 작업을 한다.

image = applet.getImage(gifURL, sample.jpeg);

여기서 한 가지 알아둬야 할 것은 이 코드가 실행됐다고 곧바로 그림 파일을 읽어 들이지는 않는다는 점이다. 만약 어떠한 애플릿에 약간의 문장과 그림을 표시하도록 했는데 이 그림이 상당히 큰 이미지였다고 가정해 보자. 위의 코드가 바로 그림 파일을 읽어 들이도록 되어 있다면 애플릿은 이 파일을 다 읽을 동안 화면에는 회색의 네모난 영역만을 표시하고 있을 것이다.

이러한 현상은 네트웍 속도가 느릴 때면 더더욱 참기 힘들어 질 것이다. 그래서 자바는 위의 코드는 image가 어떠한 자원을 읽어 들여야 하는지만 알아두도록 하고 실제 읽기 작업은 image가 화면에 처음 그려지려 할 때 시작하도록 되어 있다. 더욱더 멋진 것은 화면이 그려질 필요가 있을 때 읽는 작업이 끝나지 않았더라도 지금까지 읽혀진 부분만을 화면에 표시해 준다는 것이다(물론 이것은 JPEG 파일 포맷 때문에 가능한 것이긴 하지만).

하지만 이러한 멋진 기능도 게임에서는 걸림돌로 작용된다. 배경 그림이 읽혀지고 있는 과정이 사용자에게 그대로 드러나기 때문이다. 그래서 자바는 이러한 경우를 위해 MediaTracker 클래스를 준비해 놓고 있다. 이 클래스 객체에 이미지 객체를 연결시켜 놓고 MediaTracker가 이러한 자원을 관리하도록 하면 이러한 문제점은 아주 간단하게 해결된다.

tracker.addImage(image, 1);

tracker.waitForID(1);

이 두 줄만의 코딩으로 원하는 것을 얻을 수 있다. 미디어 트랙커에 이미지를 추가할 때 미디어 트랙커가 관리하기 위한 이미지의 ID를 줄 수 있다. 이 ID는 이미지 하나에만 줄 수도 있고 여러 개의 이미지를 같은 ID로 할당할 수도 있다. 이렇게 정해진 ID는 후에 이미지 트랙커가 이미지를 관리할 때 사용되는데, 그 대표적인 예가 waitForID() 함수의 사용이다. 이 함수는 ID를 인자로 받아 그 ID로 할당되어진 이미지를 읽어 들이도록 한다. 하지만 이러한 로딩 방법이 앞서의 것과 다른 점은 이 로딩 작업이 끝날 때까지 다른 작업을 하지 않고 기다리게 한다는 것이다.

또한 이 함수의 사용은 우리가 이미지를 표시하기 전 어느 곳에서나 사용이 가능하므로 많은 이미지를 사용하는 애플릿에서는 ID를 나누어 할당해 이미지 출력이 필요한 바로 그 시점에 원하는 이미지들만 로딩하도록 할 수 있다. 여기서 만들어지고 있는 팡 게임도 대부분의 이미지를 이렇게 관리해 사용자가 기다리는 시간을 적절히 분산시키도록 해 놓았다.

핵심 기술, 게임 창 꾸미기

지금까지의 작업들이 게임에 좀더 흥미를 유발하고 효율적으로 꾸미기 위한 작업이었다면, 다음부터 설명할 것은 이 프로그램에서 가장 중요한 부분인 실제 게임에 관련된 내용이다. 게임 창은 지금까지 설명된 기법들을 모두 포함하고 있으며, 여기에 애니메이션 기법이 추가된 형태라 말할 수 있을 것이다. 사람을 움직이고, 공을 튀어 오르도록 하고, 충돌을 체크하는 작업 등이 모두 여기서 이루어진다. 이번 시간에서는 이러한 모든 것을 다루지는 않고 게임의 배경을 꾸미는 작업만을 다루도록 하겠다.

게임의 배경은 크게 3개의 요소로 구성돼 있다. 화면 전체를 덮는 배경 이미지와 사람의 움직임을 가로막고 공의 방향을 바꾸게 하는 블럭, 그리고 사람이 오르내릴 수 있는 사다리가 바로 그것이다. 그러면 이러한 요소 하나하나에 대해 알아보겠다. 게임 창에서 배경 그림을 다루는 방법은 스테이지 선택 창에서 배경 그림을 그리는 방법과 동일하다. 한 가지 추가된 기법은 게임의 스테이지가 계속 변경되므로 여기에 맞춰 배경을 계속 변경하도록 해 주기만 하면 된다.

이 게임은 전부 50개의 스테이지로 되어 있다. 하지만 배경 이미지는 25개만을 사용한다. 결국 총 50개의 스테이지에 같은 이미지를 2번씩 사용하는 것이다. 이러한 방법은 25개의 이미지를 불규칙하게 사용하는 것보다 같은 이미지가 나올 확률을 최소화할 수 있으므로 사용자에게 항상 새로운 배경이 사용되고 있다는 생각을 갖게 할 것이다. 하지만 스테이지마다 보여지는 이미지가 정해져 있다면 이러한 배려가 퇴색되어 버릴 것이다. 항상 처음 스테이지부터 게임을 시작하는 사용자는 매번 같은 배경 그림만을 보게 될 것이다. 그래서 사용한 것이 처음 시작하는 스테이지의 배경 그림을 불규칙하게 선택하도록 하는 것이다. 그리고 그 이후에는 순차적으로 25개의 이미지를 순환시키는 방법으로 배경의 단조로움을 피할 수 있다. 이렇게 시작 스테이지의 배경을 불규칙하게 선택하도록 하는 코드는 다음과 같다.

Date date = new Date();

int second = date.getSeconds();

m_nBkgIndex = second % 25 + 1;

이 코드는 게임 창의 생성자에 들어가도록 되어 있는데 게임 창이 처음 시작되는 시간의 초를 구해 시작 이미지의 인덱스를 구하도록 되어 있다. 생성자는 애플릿이 시작된 후 게임의 진행 상태와 상관없이 유일하게 한 번만 호출되므로 이 이후에는 순차적인 배경 이미지 전환이 이뤄지는 것이다. 하지만 배경 이미지가 바뀌는 것만으로 게임의 난이도를 변화시킬 수는 없는 법! 게임의 흥미를 증가시키고 스테이지가 진행될수록 게임이 어려워지도록 하기 위해서 사용되는 것이 바로 블럭과 사다리이다.

블럭은 전부 3가지로 되어 있는데, 일반 블럭, 부서지는 블럭, 미끄러지는 얼음 블럭이 바로 그것이다. 블럭에도 약간씩의 특징을 주어 재미를 더하고 있는 구조로 되어 있다. 이 게임은 이러한 배경 블럭과 사다리의 위치와 개수를 코드 레벨에서 정하도록 하지 않고 애플릿의 파라미터로 받아 처리하도록 해 놓았다.

이것은 개발자나 사용자, 모두에게 큰 이점을 주는 설계 방식인데, 개발자의 입장에서 한 번 생각해 보도록 하자. 개발자가 총 50개의 스테이지를 설계하기 위해 각각의 스테이지에 사용되는 블럭과 사다리의 위치를 정적 변수로 사용하도록 했다면 적어도 수백 개의 변수 배열로 고생하게 될 것이다. 50개의 스테이지에 대한 정확한 스펙이 나와 있다면 그나마 고생이 덜할 것이지만, 블럭의 위치 변수를 설정해 놓고 실제로 프로그램을 돌려보았더니 그 위치가 생각했던 것과 달랐다면? 개발자는 다시 소스 코드를 열어 이 값을 변경하고, 컴파일하고, 실행시켜서 확인하는 작업을 반복해야 할 것이다.

하지만 이것을 파라미터로 처리하도록 했다면 단순히 파라미터의 값을 변경시켜줌으로써 이러한 번거로움을 들 수 있을 것이다. 사용자의 입장에서도 이러한 설계 구조는 도움이 된다. 이 게임에 재미를 느끼고 몰두하다 보니 어느덧 모든 스테이지를 돌파했다면? 이 게임은 아마도 무용지물이 될 것이다.

하지만 사용자가 파라미터를 변경해 줌으로써 새로운 형태의 스테이지를 창조해 낼 수 있으므로 게임을 계속하고자 한다면 스스로 설계하고 즐기면 된다. 물론 개발자는 처음에 이러한 파라미터를 파싱하는 알고리즘을 설계해야 하므로 약간의 수고는 들어가겠지만 앞서 설명한 번거로움과 그에 들어갈 시간을 생각한다면 파싱 알고리즘의 설계는 그리 큰 노력은 아닐 것이다. 여기서 사용하는 파라미터의 방식은 원작자가 사용한 형식을 그대로 사용하도록 할 것인데 그 원형은 다음과 같다.

player1 250 218

player2 326 218

nb_blocks 2

block 298 282 vertical 64 fixed

block 314 282 vertical 64 fixed

nb_balls 1

ball 272 62 -1 big

nb_ladders 2

ladder 258 282 12

ladder 330 282 12>

즉 애플릿의 getParameter() 함수를 사용해 level4로 이름지어진 파라미터를 읽어 온다면 위의 value 값들을 넘겨받을 것이다. 우리는 이렇게 얻은 String 값을 파싱해 원하는 객체들을 만들어 주면 된다. 필자가 사용한 파싱 방법은 다음과 같다.

우선 이러한 문장을 일단 요소 요소로 나누어 관리하도록 한다. 여기서 사용된 토큰(Token)은 공백문자와 개행문자 뿐이므로 이를 기준으로 문장을 나누어 String 객체를 만들 수 있고 이것을 Vector에 넣어 하나의 연결 구조를 만든다. 그리고 우리가 생성할 필요가 있는 객체를 나타내는 문자, 즉 player1, block, ball, ladder 등의 문자를 찾아내면 그 이후에 따라오는 값들을 사용해 객체를 생성시키고 위치를 정해주면 된다. 파싱에 사용하는 문자들은 <표 2>에 나와 있는 것들이고 이에 대한 규칙이 함께 나와 있으니, 이를 사용해 새로운 스테이지를 구성해 볼 수 있을 것이다.

파라미터 중에는 <표 2>에 나와 있지 않은 문장들도 있다. 블럭이나 공들의 개수를 나타내 주는 문장들인데, 원작에서는 이러한 문장을 파싱시에 사용한 것 같은데 필자의 프로그램에서는 이러한 문장은 의미를 가지지 않는다. 왜냐하면 파싱 알고리즘 자체가 생성하려는 객체의 수에 구애받지 않으며 혹시 파라미터의 입력에 오류가 있더라도 의미있는 값들만을 선택해 객체를 만들도록 되어 있으므로 이러한 값들을 사용하지 않았다. <리스트 2>에는 이처럼 문자열을 분류해 내고 파싱하는 함수가 나와 있다. 현재는 블럭과 사다리만을 파싱하도록 되어 있는데 나머지 객체들은 다음 회에서 다루도록 하겠다.

블럭과 사다리 객체

<리스트 2>에서 createObjectByParam() 함수를 보면 블럭과 사다리를 생성하는 함수를 호출하는 것을 볼 수 있다. 이제 GameWnd가 어떻게 블럭과 사다리를 생성하고 관리하는 지를 알아볼 차례이다. 이 게임에서는 보여지고 들려지는 모든 것을 객체로 다룬다. 이미지도 객체이고 사운드도 객체이다. 그러니 블럭과 사다리를 객체로 만들어 관리하는 것은 당연하다.

<표 2>에서 보듯이 블럭과 사다리는 단순한 배경 그림이 아니다. 공이나 사람, 화살 등 움직이는 사물에 영향을 미치는 배경이다. 그러므로 이러한 움직이는 객체와 충돌이 일어났는지를 판단할 필요가 있다. 다음 회에 다룰 내용이지만 이러한 충돌 판단은 단순히 변수 배열을 이용하는 방법보다 객체로 설계해 판단하도록 하는 것이 훨씬 쉽고 효과적이다. 따라서 우리는 객체지향적 방법론에 따라 이러한 모든 것을 객체로 설계하는 것이다.

이 두 객체는 모두 Object 클래스에서 상속받는다. 자바에서 모든 객체들의 최상위 기저 클래스로 객체를 사용하도록 되어 있기도 하지만 이 클래스들이 객체에서 상속받는 직접적인 원인은 Vector 클래스의 사용에 있다. Vector 클래스는 객체를 관리하기 위한 수집기(Collector) 역할을 하는데 이 클래스에 들어가 있는 객체를 꺼낼 때 리턴되는 값이 객체이기 때문이다.

그러면 지금부터 이 객체의 실제 구현 방법에 대해 알아보도록 하자. 이 두 객체들은 충돌 판단이 필요하지만 이러한 내용은 다음 회에 다른 객체들과 함께 다룰 예정이므로 여기서는 객체들을 화면에 표시하는 방법까지만 구현하겠다. 우선 이 객체들의 표현에 필요한 요소들을 보면 화면상에 표시될 좌표와 크기, 표시할 이미지 등이 필요하며 블럭의 경우는 화살에 의해 부서질 수 있는지, 아니면 미끄러지는 블록인지 이에 대한 상태를 담고 있는 변수가 필요하다. 이러한 값들을 가지도록 기본적인 구조가 설계됐다면 이제 이것이 Game Wnd와 어떻게 작용해 화면에 나타내어지는 지를 알아보겠다.

protected void drawBlock(Graphics g)

{

int nObject = m_BlockVector.size();

if (nObject != 0)

{

for (int i = nObject - 1 ; i >= 0 ; i--)

{

Block object = (Block)m_BlockVector.elementAt(i);

if (object != null)

object.draw(g);

}

}

}

이 코드는 GameWnd의 paint() 함수 내에서 불려지는 함수이다. <리스트 2>에서 보았듯이 파라미터를 파싱해서 블럭을 생성하고 이것을 Vector 클래스에 담도록 했는데 블럭을 담고 있는 변수가 m_BlockVector이다. drawBlock() 함수는 결국은 이렇게 m_Block Vector에 담겨 있는 블럭 객체를 하나씩 꺼내어 Block 클래스에 구현되어진 draw() 함수를 호출하는 작업을 한다.

이러한 구조는 PangApplet이 각각의 창 객체를 그리기 위해 사용하는 방법과 동일한 것으로 이렇게 함으로써 이 프로그램안의 모든 요소들은 자신의 표현은 자기 스스로 하도록 하는 것이다. 실제 블럭과 사다리 객체의 draw() 함수가 어떻게 구현됐는지는 다음 회에서 다른 객체와 함께 다루도록 하겠다. 만약 궁금함을 못 참는 독자라면 '이달의 디스켓'에 구현된 코드가 있으니 참조해 보길 바란다.

짚고 넘어가야 할 것들

이러한 구조로 완성된 게임 창의 화면이 <화면 5>에 나와 있다. 아직까지 모든 게임 객체들이 구현되어 있지 않아 실제 게임이 이루어지진 않지만 스페이스 키나 리턴 키를 치면 다음 스테이지로 넘어가도록 되어 있으니 50개의 전체 스테이지를 확인해 볼 수는 있을 것이다.

게임 창 다음에 나타나는 스코어 창은 아직 완성된 상태가 아니다. 게임 창에서 키를 눌러 계속 다음 스테이지로 전진해 마지막 스테이지가 끝나면 스코어 창이 나타나도록 해 놓았다. 스코어 창은 단순히 문자열만을 표시하고 실제 하이 스코어를 기록한 사람에 대한 정보를 표시하지는 않는다. 하이 스코어를 기록한 사람의 이름을 입력받고 이것을 웹 상에서 기억하고 있도록 하는 방법에 대해서는 마지막 회에서 다룰 것이다. 이번 회에서 다룬 내용들은 그 하나 하나만을 보면 그리 어려운 내용은 아니다.

여기서 주의해서 봐야 할 것은 구현 방법보다는 설계 구조에 대한 것이다. 여기서 사용된 설계 구조는 게임뿐 아니라 다른 응용 프로그램을 설계하고 구현할 때 유용하게 사용될 수 있는 것이니 확실하게 분석하고 자신의 것으로 만들기를 바란다.

이것으로 게임의 기본 틀을 작성하는 작업이 끝났다. 물론 이러한 작업만으로 게임을 즐길 수는 없을 것이다. 하지만 다음 회에서 다루게 될 '게임 캐릭터들을 살아 움직이게 하는 방법'은 게임의 기본 틀을 구현하는 방법과 그 설계 구조가 그리 다르지 않다. 여기서 다룬 내용을 잘 파악해 놓는다면 다음 연재의 내용을 아주 쉽게 접근할 수 있을 것이다. 다음 시간에서는 나머지 객체들을 구현해 보고 실제 게임을 즐길 수 있을 테니 기대해 주기 바란다. @

정리 박준상 jspark@infoage.co.kr

소스 화일

지금 뜨는 기사

ZDNet Power Center