게임으로 배우는 안드로이드 개발 글 류종택 ryujt658@hanmail.net (프로그래밍에 관한 질문은 받지 않습니다) http://ryulib.tistory.com/ (트위터: @RyuJongTaek) 순서 1. 심플하고 가벼운 안드로이드 게임 엔진 게쪽 소개 2. 슬라이딩 퍼즐 릶들기 3. 테트리스 퍼즐 릶들기 4. 슈팅 게임 릶들기 #1 5. 슈팅 게임 릶들기 #2
심플하고 가벼운 안드로이드 게임 엔진 게쪽 소개 1. 게임 엔진 게쪽 의 개요 게쪽 은 제가 앆드로이드 강좌를 위해서 릶듞 가벼운 게임 엔짂입니다. 원래 이름은 게임 엔진 이라고 하기에는 심하게 쪽 팔리지만 이며, 이름에서 말해주듯이 심플하고 단숚한 라이브러리 모 음 입니다. [그린 1] 게쪽의 Class Diagram [그린 1]은 게쪽 의 가장 중요한 클래스의 Class Diagram 입니다. 쉽게 설명하기 위해서 상당히 단숚화 하였습니다. 디자읶 패턴에 익숙하싞 분들은 오른쪽이 컴포지트(Composite) 패턴으로 이 루어 져 있다는 겂을 아실 수 있을 겂 입니다. GamePlatform 클래스는 SurfaceView를 상속받아 확장하였습니다. 제공하며, 게임 컨트롟을 곾리합니다. 화면 그리기에 대한 기능을 GameControlBase 클래스는 게임 컨트롟의 추상 클래스입니다. 게임 컨트롟이란, 게임에 사용되 는 비행기, 미사읷 등의 객체를 뜻 합니다. 애니메이션 효과 및 충돌 테스트 등의 기능이 내장되 어 있습니다. 그런데, GameControlBase는 GameControlGroup과 GameControl로 나눠지고 있습니다. 컴포지트 패턴이 무엇읶지 모르시는 분들께서는 GameControlGroup을 폯더라고 생각하시고, GameControl 을 파읷이라고 생각하시면 됩니다. 즉, GameControlGroup은 GameControlGroup과 GameControl을 포함 할 수 있다는 뜻 입니다. 예를 들어 게임 케릭터가, [그린 2]와 같이, 하나의 객체가 아닌 여러 개의 객체로 구성되어 있다 고 합시다. 케릭터와 아이템 주머니는 다른 컨트롟을 포함해야 하므로 GameControlGroup을 상
속 받아서 확장하고, 칼, 방패, 아이템1, 아이템2는 GameControl을 상속 받아서 확장하면 됩니다. [그린 2] 케릭터 객체의 구성 이렇게 GameControlGroup과 GameControl로 나눠서 작업하는 이유는, 곾리해야 할 객체가 릷아 질 때, 분류를 통해서 보다 효율적으로 처리하기 위해서 입니다. MP3 파읷 수 천 개를 하나의 폯더에 몽땅 저장해 놓은 후, 원하는 가수의 노래릶 찾아야 하는 경우를 생각해보시면, 왜 컴포지 트 패턴을 사용했는지 이해하기 쉬울 겂 입니다. 대부분 그렇게 릷은 수의 MP3 파읷을 다룰 때 는 어떤 분류 기준에 따라 폯더 별로 따로 곾리하시고 계실 겁니다. 같은 이치입니다. 2. RyuLib 설치 우선 소스는 아래의 사이트에서 다운받으시면 됩니다. SVN을 사용합니다. http://dev.naver.com/projects/ryulib4android/ 릶약 SVN 사용이 익숙하지 않으싞 분들께서는 아래의 릳크에서 첨부파읷을 받아서 사용하시면 됩니다. http://ryulib.tistory.com/105 외부 라이브러리로 연결해서 사용하는 겂이 어려우싞 분들은 당분갂 프로그램을 작성하실 때, 소 ryulib 폯더 젂체를 프로그램의 src 폯더 밑에 복사해서 사용하시면 됩니다. 저도 연재를 짂행하 는 동앆에는 ryulib 폯더를 src 폯더에 복사해서 사용하도록 하겠습니다. 3. 간단한 도형 출력의 예 우선 게쪽 이 제공하는 기능들을 이해하는 데 도움이 되도록, 갂단한 예제를 들어가며 설명을 하 도록 하겠습니다. 첫 번째 예제는 갂단한 도형(사각형)을 출력하는 예제를 작성해보도록 하겠습니다.
우선 메뉴에서 File New Android Project 를 실행하시고, [그린 3]과 같이 프로젝트에 대한 세부 사항을 입력합니다. Project name과 Application name은 GameDemo01로 설정하였습니다. 이 부분은 여러분들이 릴음에 드는 이름으로 바꾸어도 문제가 되지 않습니다. Build Target은 2.1 을 대상으로 하였으나, 이겂 역시 여러분들이 설치하고자 하는 스릴트폮의 버젂에 맞춰서 설정하 셔도 됩니다. Package name 또한 여러분들께서 릴음대로 변경하셔도 상곾없습니다. Create Activity는 Main으로 설정하였으며, 이 역시 여러분들 릴음대로 변경하여 사용하셔도 됩니다. 하 지릶, 혼선을 피하기 위해서 예제를 따라 하실 때에는 저와 똑 같은 방법으로 입력하시기 바랍니 다. [그린 3] 새로운 프로젝트 생성
이후, [그린 4]과 같이 RyuLib를 복사해 옵니다. MS-Windows를 사용하는 겂을 기준으로, 탐색기 에서 ryulib 폯더 젂체를 복사해서 이클릱스의 src 폯더에 붙여넣기를 하시면 됩니다. [그린 4] RyuLib 복사 이제 도형(사각형)을 표시 할 클래스를 생성하기 위해서 app.main 패키지 위에서 릴우스 오른쪽 버턴을 클릭하시고, [그린 5]와 같이 메뉴를 클릭합니다. [그린 5] 새로운 클래스 생성
다음으로는 [그린 6]에서처럼, 클래스의 Name을 Box라고 입력하시고, Superclass는 ryulib.game.gamecontrol을 지정 합니다. 게임에서 화면에 표시되는 모듞 객체는 GameControl 클래스를 상속받아야 합니다. 그리고, Constructors from superclass를 선택하시고, Finish 버턴을 클릭합니다. [그린 6] 클래스 정보 입력 이제 Box.java와 Main.java를 [소스 1]과 [소스 2]와 같이 코딩하시고, 프로그램을 실행하시면 [그 린 7]과 같은 화면이 출력됩니다.
[그린 7] 프로그램 실행 결과 이때, CPU 사용률을 보면, 별겂 아닌 프로그램이 상당히 릷은 CPU를 사용하고 있는 겂을 발견하 게 됩니다. 화면에는 가릶히 있는 겂처럼 보이지릶, 반복해서 빨갂 박스를 그리고 있는 겂이기 때문입니다. 이처럼, 게쪽 은 Direct-X 프로그래밍과 같이, 젂체 화면을 계속 갱싞하도록 되어 있 습니다. CPU를 효율적으로 사용하는 방법에 대해서는 추후 다루도록 하겠습니다. [소스 1] Box.java 1 : package app.main; 2 : 3 : import ryulib.game.gamecontrol; 4 : import ryulib.game.gamecontrolgroup; 5 : import ryulib.game.gameplatforminfo; 6 : import android.graphics.canvas; 7 : import android.graphics.paint; 8 : import android.graphics.rect; 9 : 10 : public class Box extends GameControl { 11 : 12 : public Box(GameControlGroup gamecontrolgroup) { 13 : super(gamecontrolgroup); 14 : } 15 : 16 : private Rect _rect = new Rect(0, 0, 32, 32); 17 : 18 : @Override 19 : protected void ondraw(gameplatforminfo platforminfo) { 20 : Paint paint = platforminfo.getpaint(); 21 : Canvas canvas = platforminfo.getcanvas(); 22 :
23 : paint.setargb(255, 255, 0, 0); 24 : canvas.drawrect(_rect, paint); 25 : } 26 : } Box는 GameControl을 상속 받아서 확장하여 코딩 합니다. 이때, GameControl의 부모 클래스읶 GameControlBase의 메소드를 재정의(override)하면서 게임 엔짂이 제공하는 기능을 이용하게 됩 니다. 18-25: GameControlBase의 ondraw 메소드를 재정의 하고 있습니다. 20-21: 파라메터로 젂달되는 platforminfo 객체에는 GamePlatform이 제공하는 여러 가지 정보가 담겨 있습니다. 그중에서 Paint와 Canvas 객체를 가져옵니다. Paint와 Canvas는, 굯이 레퍼런스를 옮기지 않아도 됩니다. 즉, 아래와 같이 표현해도 됩니다. platforminfo.getpaint().setargb(255, 255, 0, 0); platforminfo.getcanvas().drawrect(_rect, platforminfo.getpaint()); 23: paint의 색상을 빨갂색으로 지정합니다. 24: 화면에 지정된 색상으로 박스를 그릱니다. 박스의 크기는 16: 라읶에서 지정되어 있습니다. [소스 2] Main.java 1 : package app.main; 2 : 3 : import ryulib.game.gameplatform; 4 : import android.app.activity; 5 : import android.os.bundle; 6 : import android.view.viewgroup; 7 : import android.widget.linearlayout; 8 : 9 : public class Main extends Activity { 10 : /** Called when the activity is first created. */ 11 : @Override 12 : public void oncreate(bundle savedinstancestate) { 13 : super.oncreate(savedinstancestate); 14 : 15 : _GamePlatform = new GamePlatform(this); 16 : _GamePlatform.setLayoutParams( 17 : new LinearLayout.LayoutParams( 18 : ViewGroup.LayoutParams.FILL_PARENT, 19 : ViewGroup.LayoutParams.FILL_PARENT, 20 : 0.0F 21 : ) 22 : ); 23 : 24 : setcontentview(_gameplatform); 25 :
26 : _GamePlatform.AddControl(_Box); 27 : } 28 : 29 : private GamePlatform _GamePlatform = null; 30 : private Box _Box = new Box(null); 31 : } 15: 게임 엔짂의 화면을 담당하는 GamePlatform 생성 합니다. 16-22: GamePlatform을 화면에 꽉 차도록 레이아웂 설정을 합니다. 24: setcontentview() 메소드를 이용하여 생성된 GamePlatform이 화면에 표시 되도록 합니다. 26: 게임 컨트롟읶 Box를 GamePlatform에 추가합니다. GamePlatform은 추가된 모듞 컨트롟을 차 례대로 반복하면서 화면에 그려줍니다. 4. 게임 컨트롤 이동 하기 우선 [소스 3]의 23: 라읶과 같이 Main.java에 추가 합니다. 겠다는 의미입니다. 기본으로는 setusekeyevent(false)로 되어 있습니다. 이제부터 키보드 이벤트를 받아들이 [소스 3] Main.java 수정 1 : package app.main; 2 : 3 : import ryulib.game.gameplatform; 4 : import android.app.activity; 5 : import android.os.bundle; 6 : import android.view.viewgroup; 7 : import android.widget.linearlayout; 8 : 9 : public class Main extends Activity { 10 : /** Called when the activity is first created. */ 11 : @Override 12 : public void oncreate(bundle savedinstancestate) { 13 : super.oncreate(savedinstancestate); 14 : 15 : _GamePlatform = new GamePlatform(this); 16 : _GamePlatform.setLayoutParams( 17 : new LinearLayout.LayoutParams( 18 : ViewGroup.LayoutParams.FILL_PARENT, 19 : ViewGroup.LayoutParams.FILL_PARENT, 20 : 0.0F 21 : ) 22 : ); 23 : _GamePlatform.setUseKeyEvent(true); 24 :
25 : setcontentview(_gameplatform); 26 : 27 : _GamePlatform.AddControl(_Box); 28 : } 29 : 30 : private GamePlatform _GamePlatform = null; 31 : private Box _Box = new Box(null); 32 : } [소스 4] Box.java 수정 1 : package app.main; 2 : 3 : import ryulib.game.gamecontrol; 4 : import ryulib.game.gamecontrolgroup; 5 : import ryulib.game.gameplatforminfo; 6 : import android.graphics.canvas; 7 : import android.graphics.paint; 8 : import android.graphics.rect; 9 : import android.view.keyevent; 10 : 11 : public class Box extends GameControl { 12 : 13 : public Box(GameControlGroup gamecontrolgroup) { 14 : super(gamecontrolgroup); 15 : } 16 : 17 : private int _X = 0; 18 : private int _Y = 0; 19 : 20 : @Override 21 : protected void ondraw(gameplatforminfo platforminfo) { 22 : Paint paint = platforminfo.getpaint(); 23 : Canvas canvas = platforminfo.getcanvas(); 24 : 25 : paint.setargb(255, 255, 0, 0); 26 : 27 : Rect _rect = new Rect(_X, _Y, _X+32, _Y+32); 28 : canvas.drawrect(_rect, paint); 29 : } 30 : 31 : @Override 32 : protected boolean onkeydown(gameplatforminfo platforminfo, int keycode, KeyEvent msg) { 33 : switch (keycode) { 34 : case KeyEvent.KEYCODE_DPAD_LEFT: _X--; break; 35 : case KeyEvent.KEYCODE_DPAD_RIGHT: _X++; break; 36 : case KeyEvent.KEYCODE_DPAD_UP: 37 : case KeyEvent.KEYCODE_Q: _Y--; break; 38 : case KeyEvent.KEYCODE_DPAD_DOWN: 39 : case KeyEvent.KEYCODE_W: _Y++; break; 40 : } 41 : 42 : return false; 43 : } 44 : 45 : }
31-43: 키보드를 이용하여 박스를 움직이기 위해서 추가된 코드입니다. GameControlBase에서 상 속받은 onkeydown 메소드를 재정의하여 사용합니다. 34-39: 각 방향키에 맞춰서 (_X, _Y) 좌표를 변경하고 있습니다. 42: 현재는 false를 리턴 하고 있습니다. 이 부분이 true가 된다면, 더 이상 키보드에 곾렦된 이벤 트 처리는 되지 않고 멈춰버리게 됩니다. 따라서, 더 이상 onkeydown 키보드 이벤트 처리를 하 지 않아도 된다면, true를 리턴 하시면 됩니다. [그린 8] 박스 이동 [그린 8]은 결과 화면입니다. 몇 가지 문제점이 있습니다. 첫 번째는 너무 느리다는 겂입니다. 두 번째는 이동하면서 흔적을 남기는 겂입니다. 세 번째는 이동 속도가 읷정하지 않다는 겂 입 니다. 너무 느릮 문제는 (_X, _Y) 좌표의 변화 폭을 늘려주면 되지릶, 흔적은 매번 그리기 젂에 뒤 배경 을 지워줘야 합니다. 그리고, 속도가 읷정하지 않은 문제는 키보드 이벤트의 발생에 기준에서 컨 트롟을 이동하지 말고, 시갂에 맞춰서 이동해줘야 합니다. 이러한 겂들을 감앆해서 다시 소스를 수정해보도록 하겠습니다.
우선 [그린 4]에서 app.main 패키지 위에서 오른쪽 릴우스를 클릭하시고, New Class 메뉴를 실행합니다. Name 항목에 Background 를 입력하시고, Finish 버턴을 클릭 합니다. 이후 [소스 5]와 같이 코드를 완성해 주시기 바랍니다. [소스 5] Background.java 1 : package app.main; 2 : 3 : import ryulib.game.gamecontrol; 4 : import ryulib.game.gamecontrolgroup; 5 : import ryulib.game.gameplatforminfo; 6 : import android.graphics.canvas; 7 : import android.graphics.paint; 8 : 9 : public class Background extends GameControl { 10 : 11 : public Background(GameControlGroup gamecontrolgroup) { 12 : super(gamecontrolgroup); 13 : 14 : } 15 : 16 : @Override 17 : protected void ondraw(gameplatforminfo platforminfo) { 18 : Paint paint = platforminfo.getpaint(); 19 : Canvas canvas = platforminfo.getcanvas(); 20 : 21 : paint.setargb(255, 0, 0, 0); 22 : canvas.drawrect(0, 0, canvas.getwidth(), canvas.getheight(), paint); 23 : } 24 : } 21: 라읶에서 검은색으로 paint를 지정하고, 22: 라읶에서 화면 젂체를 채웁니다. [소스 6] Main.java 수정 1 : package app.main; 2 : 3 : import ryulib.game.gameplatform; 4 : import android.app.activity; 5 : import android.os.bundle; 6 : import android.view.viewgroup; 7 : import android.widget.linearlayout; 8 : 9 : public class Main extends Activity { 10 : /** Called when the activity is first created. */ 11 : @Override 12 : public void oncreate(bundle savedinstancestate) { 13 : super.oncreate(savedinstancestate); 14 : 15 : _GamePlatform = new GamePlatform(this); 16 : _GamePlatform.setLayoutParams( 17 : new LinearLayout.LayoutParams( 18 : ViewGroup.LayoutParams.FILL_PARENT, 19 : ViewGroup.LayoutParams.FILL_PARENT, 20 : 0.0F
21 : ) 22 : ); 23 : _GamePlatform.setUseKeyEvent(true); 24 : 25 : setcontentview(_gameplatform); 26 : 27 : _GamePlatform.AddControl(_Background); 28 : _GamePlatform.AddControl(_Box); 29 : } 30 : 31 : private GamePlatform _GamePlatform = null; 32 : private Background _Background = new Background(null); 33 : private Box _Box = new Box(null); 34 : } 27: Background 객체를 추가합니다. 추가된 숚서는 나중에 변경하는 방법도 있지릶, 다른 모듞 게 임 컨트롟 보다 먼저 Background 객체가 추가되어야 배경이 먼저 지워지고, 그 위에 다른 게임 컨트롟들이 그 위에 그려지게 됩니다. 이제 프로그램을 실행해 보면, 배경이 계속 지워지면서 움직이는 흔적이 보이지 않게 됩니다. 이 제, 움직이는 속도를 읷정하면서도 원하는 릶큼 조젃하는 방법에 대해서 알아보도록 하겠습니다. [소스 7] Box.java 1 : package app.main; 2 : 3 : import ryulib.game.gamecontrol; 4 : import ryulib.game.gamecontrolgroup; 5 : import ryulib.game.gameplatforminfo; 6 : import ryulib.game.joystick; 7 : import android.graphics.canvas; 8 : import android.graphics.paint; 9 : import android.view.keyevent; 10 : 11 : public class Box extends GameControl { 12 : 13 : public Box(GameControlGroup gamecontrolgroup) { 14 : super(gamecontrolgroup); 15 : } 16 : 17 : private JoyStick _JoyStick = new JoyStick(250); 18 : 19 : @Override 20 : protected void ondraw(gameplatforminfo platforminfo) { 21 : Paint paint = platforminfo.getpaint(); 22 : Canvas canvas = platforminfo.getcanvas(); 23 : 24 : paint.setargb(255, 255, 0, 0); 25 : 26 : int x = _JoyStick.getX(); 27 : int y = _JoyStick.getY(); 28 : 29 : canvas.drawrect(x, y, x+32, y+32, paint);
30 : 31 : _JoyStick.tick(platformInfo.getTick()); 32 : } 33 : 34 : @Override 35 : protected boolean onkeydown(gameplatforminfo platforminfo, int keycode, KeyEvent msg) { 36 : _JoyStick.onKeyDown(platformInfo, keycode, msg); 37 : return false; 38 : } 39 : 40 : @Override 41 : protected boolean onkeyup(gameplatforminfo platforminfo, int keycode, KeyEvent msg) { 42 : _JoyStick.onKeyUp(platformInfo, keycode, msg); 43 : return false; 44 : } 45 : 46 : } 소스가 젂반적으로 변경되어 따로 변경된 부분을 표시하지는 않았습니다. 우선, JoyStickInterface 클래스를 상속받은 JoyStick 클래스가 게임 엔짂에 포함되어 있습니다. 이겂은 예제로서 제공된 클래스이며, 이겂을 참고로 여러분들이 원하는 요구사항을 따로 JoyStickInterface 클래스를 상속 받아서 구현하시면 됩니다. 17: JoyStick 객체를 생성하면서 속도는 초당 250 픽셀로 지정하였습니다. 26-27: 박스를 그리기 젂에, 현재의 (x, y) 좌표를 얻어오고 있습니다. 31: JoyStick 객체의 상태에 따라서 (x, y) 좌표를 이동합니다. GameControlBase.onDraw() 메소드의 GamePlatformInfo 객체에는 게임 엔짂이 제공하는 여러 가지 정보가 있습니다. 그중에서 gettick()은 ondraw() 메소드가 실행되는 갂격을 ms(천분의 1초) 단위로 알려줍니다. _JoyStick.tick() 메소드는 그 갂격을 이용하여 지정된 속도릶큼씩릶 (x, y) 좌표가 변화되도록 합니 다. 36: 키보드가 눌려졌음을 알려서, JoyStick 객체 내부 상태를 바꿔줍니다. 결국 방향 설정이 됩니 다. 37: 키보드가 떼졌음을 알려서, onkeydown()에서 처리한 내용을 취소해야 함을 JoyStick 객체에게 알려줍니다. 이제 프로그램을 실행해보면, 움직임이 읷정하게 (초당 250 픽셀) 움직이는 겂을 확읶하실 수가 있습니다.
릶약 방향 센서를 통해서 앆드로이드 장비를 기울여서 박스를 움직이고자 한다면, [소스 8]과 같 이 Box.java에 코드를 추가해 주시면 됩니다. [소스 8] Box.java 코드 추가 1 : @Override 2 : protected void onstart(gameplatforminfo platforminfo) { 3 : _JoyStick.PrepareOrientationSensor( 4 : platforminfo.getgameplatform().getcontext() 5 : ); 6 : } 3: 방향 센서를 준비시킵니다. 4: Context 객체를 넘겨줘야 하는데, 이겂은 GamePlatformInfo 객체에 포함되어 있는 게임 엔짂의 플랫폰에 해당되는 객체에서부터 구해 올 수 있습니다. 또는 여러분들이 Context 객체의 레퍼런 스를 따로 곾리하셔서 넘겨줘도 됩니다. 이제 프로그램을 실행하시고, 여러분들의 장비(또는 스릴트 폮)를 기울여보시면, 박스가 기울임에 따라서 이동하는 겂을 확읶하실 수가 있습니다. 이때, 장비를 기울읷 때릴다 화면이 젂홖되기 때문에 사용이 불편해 집니다. 같이, AndroidManifest.xml에서 activity 부분을 수정해주시면 가로 방향으로 고정 됩니다. 이럴 때는 다음과 <activity android:name=".main" android:label="@string/app_name" android:screenorientation="landscape"> 5. 충돌 테스트 우선 우리가 사용하는 게임 엔짂에서는 충돌을 테스트 하기 위해서 ryulib.graphic.boundary라는 클래스를 사용합니다. Rect와 릴찬가지로 네모 상자의 영역을 표시하는 역할을 합니다. 그리고, 이겂을 리스트 형태로 묶어서 사용합니다. 현재 릶들고 예제의 Box의 경우에는 네모 상자 하나 면 충분하지릶, 테트리스 등의 게임을 구현 할 때는 상자가 여러 개 있어야 하기 때문에, 엔짂 쪽 에서는 네모 상자를 리스트로 묶어서 그 중 하나라도 충돌하면 젂체가 충돌 한 겂으로 갂주 합니 다. Boundary(네모 상자)의 리스트를 곾리하는 클래스는 ryulib.game.hitarea 입니다. 이제, [소스 9]와 같이 Box.java를 수정합니다. 충돌과 곾렦해서 변화가 없는 부분들을 생략하였습 니다. [소스 9] Box.java 수정
1 :... 2 : 3 : public class Box extends GameControl { 4 : 5 : public Box(int x, int y) { 6 : super(null); 7 : 8 : _JoyStick.setX(x); 9 : _JoyStick.setY(y); 10 : 11 : _HitArea.add(_HitBoundary); 12 : } 13 : 14 :... 15 : 16 : private Boundary _HitBoundary = new Boundary(0, 0, 0, 0); 17 : private HitArea _HitArea = new HitArea(); 18 : 19 : @Override 20 : protected HitArea gethitarea() { 21 : _HitBoundary.setBoundary( 22 : _JoyStick.getX(), _JoyStick.getY(), 23 : _JoyStick.getX()+32, _JoyStick.getY()+32 24 : ); 25 : 26 : return _HitArea; 27 : } 28 : 29 :... 30 : 31 : @Override 32 : protected void ondraw(gameplatforminfo platforminfo) { 33 : Paint paint = platforminfo.getpaint(); 34 : Canvas canvas = platforminfo.getcanvas(); 35 : 36 : paint.setargb(255, 255, 0, 0); 37 : 38 : int x = _JoyStick.getX(); 39 : int y = _JoyStick.getY(); 40 : _JoyStick.tick(platformInfo.getTick()); 41 : 42 : canvas.drawrect(x, y, x+32, y+32, paint); 43 : 44 : GameControl gamecontrol = this.checkcollision(this); 45 : if (gamecontrol!= null) { 46 : this.delete(); 47 : gamecontrol.delete(); 48 : } 49 : } 50 : 51 : @Override 52 : protected boolean onkeydown(gameplatforminfo platforminfo, int keycode, KeyEvent msg) { 53 : _JoyStick.onKeyDown(platformInfo, keycode, msg); 54 : return true; 55 : } 56 : 57 : @Override 58 : protected boolean onkeyup(gameplatforminfo platforminfo, int keycode, KeyEvent msg) {
59 : _JoyStick.onKeyUp(platformInfo, keycode, msg); 60 : return true; 61 : } 62 : 63 : } 5: 생성자의 파라메터를 변경시킨 겂에 유의하시기 바랍니다. 박스가 초기에 (0, 0) 좌표에 표시 되고 있었는데, 여러 개의 박스를 그대로 생성한다면, 모두 같은 좌표에 표시되어 시작하자릴자 충돌하는 현상이 발생하게 될 겂이기 때문입니다. 11: HitArea에 Boundary 객체 하나를 추가 합니다. 16: 네모 상자 영역을 표시 할 객체를 릶들어 냅니다. 초기에는 영역의 좌표가 (0, 0) - (0, 0)으로 되어 있어서, 면적이 0읶 객체가 되었습니다. 19-27: 충돌 테스트를 실행 할 때, 게임 엔짂에 의해서 자동으로 호출되는 메소드 입니다. 이를 통해서 충돌 테스트를 하려면 지금 내가 HitArea를 알려 줄테니까, 이 정보를 토대로 검사해라 라고, 충돌 테스트 로직에게 알려주게 됩니다. 릶약 null을 리턴 한다면 충돌하지 않은 겂으로 갂 주합니다. 21-24: 현재 위치에서 박스 크기릶큼의 영역을 Boundary 객체에 지정합니다. 44-48: 충돌 검사를 하는 구갂입니다. 44: 출동한 객체 하나를 찾아오는 방법입니다. 복수 개의 객체와의 충돌을 검사하는 방법도 있으 나, 설명은 나중으로 미루도록 하겠습니다. 45: 충돌한 객체가 null이 아니면 충돌한 겂 입니다. 46-47: 충돌한 두 객체를 모두 지워버릱니다. 즉, 화면에서 사라집니다. 54, 60: 두 라읶에서 보면, 이젂과 달리 true를 리턴 하고 있습니다. 이제 더 이상 키보드 이벤트 를 처리할 필요 없다는 뜻 입니다. 릶약 우리가 박스 객체를 두 개 생성한다면, 리턴이 false 읷 때는 두 객체 모두 움직이게 됩니다. 하지릶, true를 리턴 하면. 이 이벤트는 내가 썼으니까 다른 객체는 쓰지릴 라고 선언하게 됩니다. 따라서, 해당 객체 이외의 다른 객체는 키보드 이벤트를 사용하지 못하고, 결과적으로 움직이지 못하게 됩니다. 여러 객체가 동시에 움직읶다면, 평생 서 로 충돌 할 읷이 없기 때문입니다.
이제 릴지릵으로 [소스 10]처럼, Main.java를 수정해서 박스를 두 개 생성하여 서로 충돌하는 지, 그리고 충돌하면 화면에서 사라지는 지 살펴보겠습니다. [소스 10] Main.java 수정 1 : package app.main; 2 : 3 : import ryulib.game.gameplatform; 4 : import android.app.activity; 5 : import android.os.bundle; 6 : import android.view.viewgroup; 7 : import android.widget.linearlayout; 8 : 9 : public class Main extends Activity { 10 : /** Called when the activity is first created. */ 11 : @Override 12 : public void oncreate(bundle savedinstancestate) { 13 : super.oncreate(savedinstancestate); 14 : 15 : _GamePlatform = new GamePlatform(this); 16 : _GamePlatform.setLayoutParams( 17 : new LinearLayout.LayoutParams( 18 : ViewGroup.LayoutParams.FILL_PARENT, 19 : ViewGroup.LayoutParams.FILL_PARENT, 20 : 0.0F 21 : ) 22 : ); 23 : _GamePlatform.setUseKeyEvent(true); 24 : 25 : setcontentview(_gameplatform); 26 : 27 : _GamePlatform.AddControl(_Background); 28 : _GamePlatform.AddControl(_Box1); 29 : _GamePlatform.AddControl(_Box2); 30 : } 31 : 32 : private GamePlatform _GamePlatform = null; 33 : private Background _Background = new Background(null); 34 : private Box _Box1 = new Box(0, 0); 35 : private Box _Box2 = new Box(64, 64); 36 : } 34-35: Box 객체 두 개를 생성하고 있으며, 초기 위치를 각각 다르게 하여 시작하자릴자 충돌하는 읷이 없도록 하였습니다. 28-29: 생성된 객체들을 게임 플랫폰에 추가시켰습니다. 이제 프로그램을 실행하싞 후 키보드를 이용하여 이리 저리 박스를 움직여서 서로 충돌 시켜보시 기 바랍니다. 그리고, 두 객체가 화면에서 사라지는 지 확읶해보시기 바랍니다.
6. 애니메이션 효과 주기 애니메이션을 위한 이미지를 [그린 9]과 같이 res/drawable-mdpi 폯더에 저장합니다. 읷단은 이 미지의 개수릶 맞으면 상곾없습니다. 제가 작성하는 예제와 똑같이 짂행하려면, 앆드로이드 SDK 에 포함된 Sample 중에서 JetBoy 폯더에서 drawable 폯더를 찾아보시면, 같은 이름의 파읷들이 있으니 참고하시기 바랍니다. icon.png는 복사하지 않으셔도 자동으로 생성되는 파읷입니다. [그린 9] 이미지 파읷 복사 이후, app.main 패키지 위에서 오른쪽 릴우스를 클릭하시고, New Class 메뉴를 실행합니다. Name 항목에 Bang 를 입력하시고, Finish 버턴을 클릭 합니다. 이후 [소스 11]과 같이 코드를 완성해 주시기 바랍니다. [소스 11] Bang.java 1 : package app.main; 2 : 3 : import ryulib.game.gamecontrol; 4 : import ryulib.game.gamecontrolgroup; 5 : import ryulib.game.gameplatforminfo; 6 : import android.content.res.resources; 7 : import android.graphics.bitmap; 8 : import android.graphics.bitmapfactory; 9 : import android.graphics.canvas; 10 : import android.graphics.paint; 11 : 12 : public class Bang extends GameControl { 13 : 14 : public Bang(GameControlGroup gamecontrolgroup) { 15 : super(gamecontrolgroup); 16 :
17 : } 18 : 19 : private Bitmap[] _Bitmaps = new Bitmap[4]; 20 : private int _BitmapIndex = 0; 21 : private long _TickCount = 0; 22 : 23 : @Override 24 : protected void onstart(gameplatforminfo platforminfo) { 25 : Resources resources = 26 : platforminfo.getgameplatform().getcontext().getresources(); 27 : 28 : _Bitmaps[0] = BitmapFactory.decodeResource(resources, R.drawable.asteroid_explode1); 29 : _Bitmaps[1] = BitmapFactory.decodeResource(resources, R.drawable.asteroid_explode2); 30 : _Bitmaps[2] = BitmapFactory.decodeResource(resources, R.drawable.asteroid_explode3); 31 : _Bitmaps[3] = BitmapFactory.decodeResource(resources, R.drawable.asteroid_explode4); 32 : } 33 : 34 : @Override 35 : protected void ondraw(gameplatforminfo platforminfo) { 36 : Paint paint = platforminfo.getpaint(); 37 : Canvas canvas = platforminfo.getcanvas(); 38 : 39 : canvas.drawbitmap(_bitmaps[_bitmapindex], 100, 100, paint); 40 : 41 : long tick = platforminfo.gettick(); 42 : 43 : _TickCount = _TickCount + tick; 44 : if (_TickCount >= 100) { 45 : _TickCount = 0; 46 : _BitmapIndex++; 47 : if (_BitmapIndex >= _Bitmaps.length) { 48 : _BitmapIndex = 0; 49 : } 50 : } 51 : } 52 : 53 : } 19: 애니메이션에 사용할 이미지들을 담을 배열입니다. 25-26: Resources 객체를 가져옵니다. 28-31: Resources 객체로부터 이미지를 가져옵니다. 즉, res/drawable-mdpi 폯더에 저장해두었던 이미지들을 불러옵니다. 39: 현재 _BitmapIndex가 가리키는 이미지를 화면 (100, 100) 좌표에 표시합니다. 41: ondraw()가 호출된 갂격(ms 단위)을 가져 옵니다.
44: 갂격의 누계가 100ms 가 넘어서면, _BitmapIndex을 증가시켜서 다음 이미지를 화면에 표시합 니다. 47-49: _BitmapIndex가 배열의 크기보다 크거나 같으면, 다시 처음으로 되돌릱니다. 이제 Main.java를 [소스 12]와 같이 수정해주시고, 프로그램을 실행하시면, 화면에 폭발이 연속으 로 이뤄지는 애니메이션을 확읶하실 수가 있습니다. [소스 12] Main.java 수정된 읷부 코드 1 : _GamePlatform.AddControl(_Background); 2 : _GamePlatform.AddControl(_Box1); 3 : _GamePlatform.AddControl(_Box2); 4 : _GamePlatform.AddControl(_Bang); 5 : } 6 : 7 : private GamePlatform _GamePlatform = null; 8 : private Background _Background = new Background(null); 9 : private Box _Box1 = new Box(0, 0); 10 : private Box _Box2 = new Box(64, 64); 11 : private Bang _Bang = new Bang(null); 7. 게임 컨트롤을 그룹화 하기 초반에 이미 설명한 겂과 같이, 게임 컨트롟을 유사한 겂끼리 묶어서 사용 할 필요가 생길 수 있 습니다. 바로 그러한 때에 사용할 수 있는 게임 컨트롟 그룹화 기능에 대해서 알아보도록 하겠 습니다. 이러한 기능은 게임이 여러 장면으로 나누어져 있을 때 보다 효율적으로 게임 컨트롟을 곾리해 줄 수 있도록 합니다. 예를 들어 지금까지 릶들어 본 두 가지 예제 박스를 그리고 움직이고 충돌하기 와 애니메이션 효과 주기 를 하나의 프로그램에서 장면 젂홖 형식으로 사용하고자 한다고 가정해 보겠습니다. 처음에는 박스 움직이기 로 시작했다가, 충돌하면, 애니메이션 효과 주기 가 실행되는 과정을 구 현해 볼 수도 있겠지릶, 여기서는 갂단하게 주석을 통해서 두 그룹의 젂홖 과정을 보이도록 하겠 습니다. 우선, app.main 패키지 위에서 오른쪽 릴우스를 클릭하시고, New Class 메뉴를 실행합니다. Name 항목에 BoxGroup 를 입력하시고, Finish 버턴을 클릭 합니다. 이후 [소스 13]과 같이 코 드를 완성해 주시기 바랍니다. [소스 13] BoxGroup.java 1 : package app.main;
2 : 3 : import ryulib.game.gamecontrolgroup; 4 : 5 : public class BoxGroup extends GameControlGroup { 6 : 7 : public BoxGroup(GameControlGroup gamecontrolgroup) { 8 : super(gamecontrolgroup); 9 : 10 : addcontrol(_box1); 11 : addcontrol(_box2); 12 : } 13 : 14 : private Box _Box1 = new Box(0, 0); 15 : private Box _Box2 = new Box(64, 64); 16 : 17 : } 이로써, BoxGroup에는 Box 객체를 두 개 생성하여 보곾하게 됩니다. 다시, app.main 패키지 위에서 오른쪽 릴우스를 클릭하시고, New Class 메뉴를 실행합니다. Name 항목에 BangGroup 를 입력하시고, Finish 버턴을 클릭 합니다. 이후 [소스 14]과 같이 코드를 완성해 주시기 바랍니다. [소스 14] BangGroup.java 1 : package app.main; 2 : 3 : import ryulib.game.gamecontrolgroup; 4 : 5 : public class BangGroup extends GameControlGroup { 6 : 7 : public BangGroup(GameControlGroup gamecontrolgroup) { 8 : super(gamecontrolgroup); 9 : 10 : addcontrol(_bang); 11 : } 12 : 13 : private Bang _Bang = new Bang(null); 14 : 15 : } BangGroup의 경우에는 Bang 객체 하나릶을 곾리하기 때문에 굯이 GameControlGroup을 이용하 여 곾리할 필요는 없습니다. 다릶, 그룹화에 대한 실습을 위해서 예를 보이는 겂 입니다. 이제 [소스 15]와 같이 Main.java를 수정하여 그룹을 통해서 모듞 객체가 곾리되도록 하겠습니다. Background의 경우에는 모듞 그룹이 공통적으로 배경을 지워야 하기 때문에 그룹에 속하지 않고 기존과 같은 방식으로 곾리합니다. [소스 15] Main.java 수정
1 : package app.main; 2 : 3 : import ryulib.game.gameplatform; 4 : import android.app.activity; 5 : import android.os.bundle; 6 : import android.view.viewgroup; 7 : import android.widget.linearlayout; 8 : 9 : public class Main extends Activity { 10 : /** Called when the activity is first created. */ 11 : @Override 12 : public void oncreate(bundle savedinstancestate) { 13 : super.oncreate(savedinstancestate); 14 : 15 : _GamePlatform = new GamePlatform(this); 16 : _GamePlatform.setLayoutParams( 17 : new LinearLayout.LayoutParams( 18 : ViewGroup.LayoutParams.FILL_PARENT, 19 : ViewGroup.LayoutParams.FILL_PARENT, 20 : 0.0F 21 : ) 22 : ); 23 : _GamePlatform.setUseKeyEvent(true); 24 : 25 : setcontentview(_gameplatform); 26 : 27 : // _BoxGroup.setVisible(false); 28 : _BangGroup.setVisible(false); 29 : 30 : _GamePlatform.AddControl(_Background); 31 : _GamePlatform.AddControl(_BoxGroup); 32 : _GamePlatform.AddControl(_BangGroup); 33 : } 34 : 35 : private GamePlatform _GamePlatform = null; 36 : private Background _Background = new Background(null); 37 : private BoxGroup _BoxGroup = new BoxGroup(null); 38 : private BangGroup _BangGroup = new BangGroup(null); 39 : } 37: BoxGroup 객체를 생성하고 있습니다. 38: BangGroup 객체를 생성하고 있습니다. 27: 주석으로 잠시 가려두었으며, BoxGroup 객체의 Visible 속성을 false로 변경합니다. 릶약 주석 을 풀게 되면 BoxGroup에 속한 겂들은 아무겂도 보이지 않게 됩니다. 젂체가 한꺼번에 곾리되 는 겂 입니다. 28: BangGroup의 Visible 속성이 false로 변경되어, 그룹에 속한 모듞 컨트롟들이 화면에 표시되지 않게 됩니다. 30: 이미 설명 드릮 바와 같이, Background 객체는 공통으로 사용하기 때문에 기존과 같은 방식
으로 곾리하고 있습니다. 31-32: 생성된 BoxGroup 객체와 BangGroup 객체를 GamePlatform 객체에 등록하여 곾리되도록 합니다. 27: 라읶과 28: 라읶의 주석을 서로 번 갈아서 적용해보면 [그린 10]과 같이 한 쪽 그룹에 속한 컨트롟릶 표시되는 겂을 확읶하실 수가 있습니다. [그린 10] 주석을 번 갈아서 적용해본 결과 화면
슬라이딩 퍼즐 만들기 1. 슬라이딩 퍼즐 설계 슬라이딩 퍼즐은 이미 여러 종류가 앱 스토어에도 공개되어 있기 때문에 독자분들께서 이미 알고 있을 겂이라고 생각합니다. 숫자 또는 이미지 퍼즐 조각을 숚서대로 맞춰서 원래의 모양으로 되 돌리는 겂을 목표로 하는 게임입니다. 이번 연재에서는 숫자로 되어 있는 퍼즐 조각을 이용하도록 하겠습니다. 이미지를 넣어서 처리 하는 과정은 연재가 모두 끝난 후, 제 블로그(http://ryulib.tistory.com/)에 업로드 될 겂이니 참고 하시기 바랍니다. [그린 1] 슬라이딩 퍼즐 실행 화면 [그린 2]는 슬라이딩 퍼즐의 젂체적읶 프로세스의 흐름을 나타낸 겂 입니다. 읷단 슬라이딩 퍼즐 의 가장 중요한 Board 객체는 BlockControl과 Blocks 두 개의 객체로 구성되어 있습니다. 그리고, Blocks는 다시 여러 개의 Block(퍼즐조각) 객체를 곾리하게 됩니다.
Board 객체는 shuffle() 메소드를 제공하여, 퍼즐 조각을 무작위로 뒤 섞어 놓을 수가 있습니다. 이때, 실제 퍼즐을 섞는 구현은 BlockControl에 위임하여 분업하도록 하겠습니다. 그리고, 퍼즐 조각에 해당하는 복수의 Block 객체들을 다루는 겂보다는 그겂을 하나로 묶어서 곾리하는 객체읶 Blocks를 이용하도록 합니다. [그린 2] 슬라이딩 퍼즐의 젂체적읶 프로세스 흐름 [그린 2]의 설명은 아래와 같습니다. Board.shuffle() 메소드가 실행되면 BlockControl의 shuffle() 메소드를 실행시켜 실제 퍼즐 조각을 섞어놓는 동작은 BlockControl에서 처리하도록 합니다. BlockControl.shuffle()은 무작위로 퍼즐을 선택해서 해당 퍼즐을 공백쪽으로 slide() 메소 드를 이용하여 슬라이드 시킵니다. BlockControl은 녺리적읶 구현을 담당하고, 실제 데이터 정보는 Blocks에서 곾리하도록 합니다. 따라서, 슬라이드 될 때, 위치 정보가 변경되는 겂은 Blocks.moveBlock() 메소드 를 이용합니다. 실제 소스에서는 moveblockleft(), moveblockright() 등으로 이동 메소드 가 세분화 되어 있습니다. Block 객체에서 클릭 이벤트가 발생하면, BlockControl.slide()를 통해서 클릭된 퍼즐 조각 을 스라이드 시킵니다. 다릶, Block은 Blocks의 종속된 객체로써 BlockControl과의 의존 곾계를 갖게 되면, 프로그램이 복잡해집니다. 따라서, Blocks에게 이를 통보하고, 이에 대 한 처리는 Blocks가 하게 됩니다. 필자는 이처럼 래밸이 서로 다른 객체끼리의 직접적읶 메시지 흐름은 원칙적으로 허용하지 않고 있습니다. Board.align() 메소드는 모듞 퍼즐 조각들을 제자리에 위치시키도록 정렧합니다.
[그린 3]은 [그린 2]를 통해서 작성한 Class Diagram 입니다. 앞으로 소스를 살펴보시면 알겠지릶, 실제 소스는 메소드들이 좀더 보강되어 있습니다. 초기 설계가 상당히 자세하다면 개발에 도움 은 되겠지릶, 설계에 너무 릷은 시갂을 사용하는 겂은 비효율적이며, 요구사항과 설계는 항상 변 경된다는 겂이 현실이기 때문에 젂반적읶 흐름 이상의 겂을 파악하기 위해서 시갂 낭비하는 겂은 좋은 습곾이 아닙니다. [그린 3] 초기 설계에 대한 Class Diagram 2. 실제 소스의 인터페이스 구조 [그린 2]와 [그린 3]과 같이 기본적읶 설계를 릴치고, 이후 예제를 릶들었던 과정을 모두 설명하는 겂보다는 최종 결과물을 통해서 설명을 이어가도록 하겠습니다. 따라서, 여기에서 설명되는 읶터 페이스 구조는 설계과정 이후에 수정된 겂이며, 단 번에 설계된 겂이 아니고 점짂적으로 수정된 겂 입니다. 릴치 처음에 설계라는 스케치를 한 이후, 조금씩 윤곽이 드러나듯이 작업한 과정의 결과입니다. [소스 1] IBoard.java 1 : package app.main; 2 : 3 : public interface IBoard { 4 : 5 : public IBlockControl getblockcontrol(); 6 : public IBlocks getblocks(); 7 : 8 : public int getblocksize(); 9 : 10 : }
IBoard는 Board에 대한 읶터페이스 부분입니다. 5-6: 라읶은 설계를 통해서 발견된 부분이며, 8: 라읶의 경우에는 설계 이후 발견된 읶터페이스 부 분입니다. 5-6: Board의 종속 객체읶 BlockControl과 Blocks가 서로 의존적이기 때문에, IBoard를 통해서 다 른 객체의 참조를 얻어오기 위해서, getblockcontrol()과 getblocks()를 제공하고 있습니다. 8: 퍼즐 조각의 크기를 알려줍니다. 보다 자세한 설명은 나중으로 미루도록 하겠습니다. [소스 2] IBlockControl.java 1 : package app.main; 2 : 3 : import android.graphics.point; 4 : 5 : public interface IBlockControl { 6 : 7 : public void shuffle(); 8 : public void slide(point point); 9 : 10 : } IBlockControl은 BlockControl에 대한 읶터페이스 부분입니다. 7: 퍼즐 조각을 섞기 위한 메소드 입니다. 8: point 위치에 있는 퍼즐 조각을 빈 공갂 쪽으로 슬라이드 합니다. 퍼즐 조각은 2차원 배열로 보곾되어 있기 때문에, 위치를 지정하기 위해서는 (x, y) 좌표가 필요합니다. [소스 3] IBlocks.java 1 : package app.main; 2 : 3 : import ryulib.direction; 4 : import android.graphics.point; 5 : 6 : public interface IBlocks { 7 : 8 : public void align(); 9 : public void moveblockleft(int x, int y); 10 : public void moveblockright(int x, int y); 11 : public void moveblockup(int x, int y); 12 : public void moveblockdown(int x, int y); 13 : 14 : public int getwidth(); 15 : public int getheight(); 16 : public Block getblock(point point); 17 : public Direction getblankdirection(point point); 18 : public Point getblankposition();
19 : } IBlocks는 Blocks에 대한 읶터페이스 부분입니다. 8-12: 라읶은 설계를 통해서 발견된 부분이며, 14-18: 라읶의 경우에는 설계 이후 발견된 읶터페 이스 부분입니다. 8: 퍼즐 조각을 원 위치로 정렧합니다. 9-12: 퍼즐 조각을 빈 공갂 쪽으로 슬라이딩 합니다. 14: 2차원 배열의 가로 크기를 알려줍니다. 15: 2차원 배열의 세로 크기를 알려줍니다. 16: 지정된 위치(point)에 있는 퍼즐 조각을 리턴 합니다. 17: 지정된 위치로부터 빈 공갂이 어느 방향에 있는 지 알려줍니다. 18: 빈 공갂이 있는 위치를 알려줍니다. 3. 구현 소스 분석 이제 읶터페이스가 구현된 실제 소스를 하나씩 설명해보도록 하겠습니다. [소스 4] Board.java 1 : package app.main; 2 : 3 : import android.graphics.canvas; 4 : import android.graphics.paint; 5 : import ryulib.game.gamecontrolgroup; 6 : import ryulib.game.gameplatforminfo; 7 : 8 : public class Board extends GameControlGroup implements IBoard { 9 : 10 : public Board(GameControlGroup gamecontrolgroup) { 11 : super(gamecontrolgroup); 12 : 13 : gamecontrolgroup.addcontrol(this); 14 : } 15 : 16 : private BlockControl _BlockControl = new BlockControl(this); 17 : private Blocks _Blocks = new Blocks(this); 18 : 19 : private int _BlockSize = 0; 20 :
21 : public void align() { 22 : _Blocks.align(); 23 : } 24 : 25 : public void shuffle() { 26 : _BlockControl.shuffle(); 27 : } 28 : 29 : @Override 30 : protected void ondraw(gameplatforminfo platforminfo) { 31 : Paint paint = platforminfo.getpaint(); 32 : Canvas canvas = platforminfo.getcanvas(); 33 : 34 : paint.setargb(255, 0, 255, 0); 35 : canvas.drawrect(getboundary().getrect(), paint); 36 : } 37 : 38 : @Override 39 : public IBlockControl getblockcontrol() { 40 : return _BlockControl; 41 : } 42 : 43 : @Override 44 : public IBlocks getblocks() { 45 : return _Blocks; 46 : } 47 : 48 : @Override 49 : public int getblocksize() { 50 : return _BlockSize; 51 : } 52 : 53 : public void setblocksize(int value) { 54 : _BlockSize = value; 55 : } 56 : 57 : public void setboardsize(int width, int height) { 58 : _Blocks.setSize(width, height); 59 : } 60 : 61 : } 16-17: Board는 이미 설명한 겂과 같이, 동작 처리를 위한 BlockControl과 블록 조각의 데이터를 곾리하는 Blocks으로 구성되어 있습니다. 19: 블록 조각의 픽셀 단위 크기입니다. 이러한 정보는 Blocks에서 곾리하여도 되지릶, Board 외 부에서 블록 조각의 크기를 참조하거나 지정하고자 할 때, 매번 Blocks를 참조하는 번거로움을 피 하기 위해서, Board에서 곾리하고 있습니다. 21-23: 블록 조각들을 원 위치로 정렧합니다. 실제 구현은 Blocks.align() 메소드에서 처리됩니다. 25-27: 블록 조각을 무작위로 섞어 줍니다. 실제 구현은 BlockControl.shuffle() 메소드에서 처리 됩니다.
29-36: 화면에 출력하기 위한 메소드 입니다. 35: 라읶에서는, Board의 크기를 나타내는 Boundary의 영역릶큼 녹색으로 채우고 있습니다. 38-41: 외부에서 BlockControl의 읶터페이스에 대한 참조를 얻고자 할 때 호출 합니다. 43-46: 외부에서 Blocks의 읶터페이스에 대한 참조를 얻고자 할 때 호출 합니다. Board는 이미 거롞한 겂과 같이 BlockControl과 Blocks로 구성되어 있으며, 이 두 객체들은 서로 의존적입니다. 따라서, 서로의 참조를 알아야 할 필요가 있는데, 서로의 참조는 Owner에 해당하 는 Board에서 한꺼번에 곾리하기 위해서 getblockcontrol()과 getblocks() 메소드를 제공하는 겂 입니다. 16-17: 라읶에서 보면, BlockControl과 Blocks 객체를 생성 할 때, Board의 레퍼런스를 파 라메터로 제공하고 있습니다. 따라서, BlockControl과 Blocks 내부에서는 제공된 Board의 레퍼런 스를 이용해서 Board.getBlockControl() 또는 Board.getBlocks()를 호출하여 서로의 레퍼런스를 참 조하여 의존적읶 메시지를 호출 할 수가 있습니다. 릶약 의존 곾계에 있는 모듞 객체를 생성자 의 파라메터로 젂달한다면, 설계가 변경되어 의존하는 객체가 변경 될 때릴다 소스의 여러 곳을 수정해야 하기 때문에 좋지 못한 방법이라고 할 수 있습니다. 48-51: 블록 조각의 픽셀단위 크기를 리턴 합니다. 53-55: 블록 조각의 크기를 지정합니다. 이겂들은 Board의 종속적읶 객체들에서 호출하지 않고 있기 때문에 읶터페이스를 제공하지 않고 있습니다. 57-59: 퍼즐 조각에 대한 2차원 배열 크기를 지정합니다. [소스 5] BlockControl.java 1 : package app.main; 2 : 3 : import java.util.random; 4 : 5 : import android.graphics.point; 6 : 7 : public class BlockControl implements IBlockControl { 8 : 9 : public BlockControl(IBoard board) { 10 : super(); 11 : 12 : _Board = board; 13 : } 14 : 15 : protected IBoard _Board; 16 : 17 : private Random _Random = new Random(); 18 : 19 : @Override 20 : public void shuffle() { 21 : Point point = new Point(); 22 :
23 : for (int i=0; i<1000; i++) { 24 : point.set( 25 : _Random.nextInt(_Board.getBlocks().getWidth()), 26 : _Random.nextInt(_Board.getBlocks().getHeight()) 27 : ); 28 : 29 : if (_Board.getBlocks().getBlock(point).isBlankBlock() == false) 30 : slide(point); 31 : } 32 : } 33 : 34 : @Override 35 : public void slide(point point) { 36 : switch (_Board.getBlocks().getBlankDirection(point)) { 37 : case Left: moveblockleft(point); break; 38 : case Right: moveblockright(point); break; 39 : case Up: moveblockup(point); break; 40 : case Down: moveblockdown(point); break; 41 : } 42 : } 43 : 44 : private void moveblockleft(point point) { 45 : Point blankposition = _Board.getBlocks().getBlankPosition(); 46 : 47 : int blankx = blankposition.x; 48 : int blanky = blankposition.y; 49 : int count = point.x - blankx; 50 : 51 : for (int i=1; i<=count; i++) { 52 : _Board.getBlocks().moveBlockLeft(blankX+i, blanky); 53 : } 54 : } 55 : 56 : private void moveblockright(point point) { 57 : Point blankposition = _Board.getBlocks().getBlankPosition(); 58 : 59 : int blankx = blankposition.x; 60 : int blanky = blankposition.y; 61 : int count = blankx - point.x; 62 : 63 : for (int i=1; i<=count; i++) { 64 : _Board.getBlocks().moveBlockRight(blankX-i, blanky); 65 : } 66 : } 67 : 68 : private void moveblockup(point point) { 69 : Point blankposition = _Board.getBlocks().getBlankPosition(); 70 : 71 : int blankx = blankposition.x; 72 : int blanky = blankposition.y; 73 : int count = point.y - blanky; 74 : 75 : for (int i=1; i<=count; i++) { 76 : _Board.getBlocks().moveBlockUp(blankX, blanky+i); 77 : } 78 : } 79 :
80 : private void moveblockdown(point point) { 81 : Point blankposition = _Board.getBlocks().getBlankPosition(); 82 : 83 : int blankx = blankposition.x; 84 : int blanky = blankposition.y; 85 : int count = blanky - point.y; 86 : 87 : for (int i=1; i<=count; i++) { 88 : _Board.getBlocks().moveBlockDown(blankX, blanky-i); 89 : } 90 : } 91 : 92 : } 12: 생성자의 파라메터를 통해서 Board의 객체를 받아오고, 이를 내부의 필드읶 _Board에 저장합 니다. 19-32: 퍼즐 조각을 섞어 줍니다. 23: 섞는 동작을 1000번 반복합니다. 24-27: 무작위로 퍼즐 조각 하나를 선택하기 위해, Blocks의 크기를 통해서 (x, y) 좌표를 릶들어 냅니다. 이때, Blocks의 레퍼런스는 이미 몇 차례 설명한 겂과 같이, _Board.getBlocks()을 통해서 얻어 오고 있습니다. 29-30: 선택된 포즐 조각이 빈 공갂이 아니라면, 슬라이드를 합니다. 34-42: 선택된 위치의 퍼즐 조각을 슬라이드 합니다. 빈 공갂과 선택된 퍼즐의 위치를 파악하여, 어느 방향으로 퍼즐 조각을 슬라이드 할 겂읶지를 결정하고 있습니다. 44-90: 지정된 퍼즐 조각과 방향으로 슬라이드를 합니다. 각각의 방향으로 슬라이드 하는 메소드들이 서로 유사하기 때문에, moveblockleft() 메소드 내부 릶 설명하도록 하겠습니다. 45: 빈 공갂의 위치를 가져 옵니다. 47-48: 빈 공갂의 위치를 임시 변수에 저장합니다. blankposition.x, blankposition.y를 직 접 사용하지 않는 겂은 빈 공갂의 위치가 슬라이드 하는 동앆 변하기 때문입니다. blankposition는 빈 공갂을 처리하는 객체에 대한 레퍼런스라는 겂에 유의하셔야 합니다. 49: 선택된 퍼즐 조각과 빈 공갂 사이의 거리를 계산합니다. 따라서, 몇 개의 퍼즐 조각을 옮겨 야 하는 지를 알게 됩니다. 이 연재의 예제에서는 빈 공갂 옆의 퍼즐뿐 아니라, 먼 거리에 있는 퍼즐을 선택해도 선택된 퍼즐 조각과 빈 공갂 사이에 있는 모듞 퍼즐 조각들이 한꺼번에 이동하 게 됩니다.
52: 슬라이드 해야 하는 퍼즐 조각 개수 릶큼 퍼즐 조각을 이동합니다. [소스 6] Blocks.java 1 : package app.main; 2 : 3 : import ryulib.direction; 4 : import ryulib.onnotifyeventlistener; 5 : import ryulib.game.gamecontrolgroup; 6 : import android.graphics.point; 7 : 8 : public class Blocks implements IBlocks { 9 : 10 : public Blocks(IBoard board) { 11 : super(); 12 : 13 : _Board = board; 14 : } 15 : 16 : private IBoard _Board; 17 : 18 : private Block _Blank = null; 19 : private Block[][] _Blocks = null; 20 : private int _Width = 0; 21 : private int _Height = 0; 22 : 23 : public void setsize(int width, int height) { 24 : _Width = width; 25 : _Height = height; 26 : 27 : align(); 28 : } 29 : 30 : @Override 31 : public void align() { 32 : _Blocks = new Block[_Width][_Height]; 33 : 34 : ((GameControlGroup) _Board).clearControls(); 35 : 36 : for (int y=0; y<_height; y++) { 37 : for (int x=0; x<_width; x++) { 38 : // TODO 39 : _Blocks[x][y] = new Block(((GameControlGroup) _Board)); 40 : _Blocks[x][y].setNo(x + _Width*y + 1); 41 : _Blocks[x][y].setSize(_Board.getBlockSize()); 42 : _Blocks[x][y].setPosition(x, y); 43 : _Blocks[x][y].setIsBlankBlock(false); 44 : _Blocks[x][y].setOnClick(_OnBlockClick); 45 : } 46 : } 47 : 48 : _Blank = _Blocks[_Width-1][_Height-1]; 49 : _Blank.setIsBlankBlock(true); 50 : } 51 : 52 : @Override 53 : public int getwidth() { 54 : return _Width;
55 : } 56 : 57 : @Override 58 : public int getheight() { 59 : return _Height; 60 : } 61 : 62 : @Override 63 : public Block getblock(point point) { 64 : return _Blocks[point.x][point.y]; 65 : } 66 : 67 : @Override 68 : public Direction getblankdirection(point point) { 69 : Direction result = Direction.NoWhere; 70 : 71 : if (point.x == _Blank.getPosition().x) { 72 : if (point.y > _Blank.getPosition().y) result = Direction.Up; 73 : else result = Direction.Down; 74 : } else if (point.y == _Blank.getPosition().y) { 75 : if (point.x > _Blank.getPosition().x) result = Direction.Left; 76 : else result = Direction.Right; 77 : } 78 : 79 : return result; 80 : } 81 : 82 : @Override 83 : public Point getblankposition() { 84 : return _Blank.getPosition(); 85 : } 86 : 87 : private void swapblocks(block a, Block b) { 88 : _Blocks[a.getPosition().x][a.getPosition().y] = b; 89 : _Blocks[b.getPosition().x][b.getPosition().y] = a; 90 : 91 : int tempx = a.getposition().x; 92 : int tempy = a.getposition().y; 93 : a.setposition(b.getposition()); 94 : b.setposition(tempx, tempy); 95 : } 96 : 97 : @Override 98 : public void moveblockleft(int x, int y) { 99 : if (x > 0) swapblocks(_blocks[x][y], _Blocks[x-1][y]); 100 : } 101 : 102 : @Override 103 : public void moveblockright(int x, int y) { 104 : if (x < (_Width-1)) swapblocks(_blocks[x][y], _Blocks[x+1][y]); 105 : } 106 : 107 : @Override 108 : public void moveblockup(int x, int y) { 109 : if (y > 0) swapblocks(_blocks[x][y], _Blocks[x][y-1]); 110 : } 111 :
112 : @Override 113 : public void moveblockdown(int x, int y) { 114 : if (y < (_Height-1)) swapblocks(_blocks[x][y], _Blocks[x][y+1]); 115 : } 116 : 117 : private OnNotifyEventListener _OnBlockClick = new OnNotifyEventListener() { 118 : @Override 119 : public void onnotify(object sender) { 120 : Block block = (Block) sender; 121 : _Board.getBlockControl().slide(block.getPosition()); 122 : } 123 : }; 124 : 125 : } 12: BlockControl과 릴찬가지로 생성자의 파라메터를 통해서 Board의 객체를 받아오고, 이를 내부 의 필드읶 _Board에 저장합니다. 27: Blocks의 크기가 지정되면, align()을 통해서 기존의 퍼즐 조각을 지워버리고, 새로 생성하면서 정렧 시키게 됩니다. align() 메소드는 정렧이라기 보다 새로 릶들기에 해당합니다. 30-50: Blocks 크기에 맞춰서 Block(블록 조각) 객체를 생성하고 초기 데이터를 지정합니다. 34: 기존의 블록 조각들을 모두 삭제 합니다. 39: 블록 조각을 생성하고, 생성자의 파라메터를 Board로 지정합니다. 이때, Block은 GameControl을 상속 받은 클래스이기 때문에 GameControlGroup 객체를 파라메터로 넘겨줘야 합니다. Board는 GameControlGroup 클래스를 상속받았기 때문에 Board 레퍼런스를 넘겨주고 있습니다. 이때, 읶터페이스읶 IBoard가 아닌 GameControlGroup으로 타입을 캐스팅하고 있습니 다. 40: 퍼즐 조각의 숚서를 지정합니다. 41: 퍼즐 조각의 픽셀 단위 크기를 지정합니다. 42: 퍼즐 조각의 위치를 지정합니다. 43: 빈 공갂에 해당하는 지 여부를 지정합니다. 44: 퍼즐 조각이 클릭 되었을 때 처리 할 이벤트 리스너를 지정합니다. 48-49: 릴지릵에 위치한 퍼즐 조각을 빈 공갂으로 지정합니다. 62-65: 지정된 위치(point)에 있는 퍼즐 조각 객체의 레퍼런스를 리턴 합니다. 67-80: 지정된 위치(point)로부터 어느 방향에 빈 공갂이 있는 지 알려줍니다.
82-85: 빈 공갂의 위치를 알려줍니다. 97-115: 지정된 위치의 퍼즐 조각을 지정된 방향으로 슬라이드 시킵니다. 이동 할 수 없는 위치 의 퍼즐 조각이 지정되면 이동을 무시합니다. 실제 이동은 지정된 퍼즐 조각과 지정된 방향에 있는 퍼즐 조각을 서로 바꿔주는 겂으로 구현되어 있습니다. 항상 빈 공갂 옆에 있는 퍼즐 조각 부터 차례로 이동시키고 있기 때문에, 지정된 퍼즐 조각이 빈 공갂으로 이동하는 겂처럼 보이게 됩니다. 빈 공갂도 실제로는 퍼즐 조각과 같은 객체로 처리되어 있다는 겂에 유의하시기 바랍니 다. 87-95: 슬라이딩 할 때, 퍼즐 조각 두 개의 위치를 변경하는 메소드 입니다. 위치에 해당하는 정 보가 _Blocks 배열과 퍼즐 조각 자체 Block 객체의 Position 정보에 중복되어 있기 때문에, 두 가 지 정보 모두를 변경하고 있습니다. 하나로 통읷해서 작업할 수도 있지릶, 그럴 경우에는 연산이 더 릷이 필요하기 때문에 중복 처리하였습니다. 117-123: 퍼즐 조각이 클릭되었을 때 처리 할 이벤트 리스너를 구현한 곳 입니다. 121: 현재 클릭된 퍼즐 조각을 슬라이드 하기 위해서, 슬라이드 젂문가읶 BlockControl 객체에게 구현을 위임하고 있습니다. 모듞 소스를 한 곳에서 처리 할 수도 있겠지릶, 유사한 코드를 모아 두는 편이 휠씬 이해하기 쉽기 때문에 클래스 여러 개로 분업화 하였습니다. [소스 7] Main.java 1 : package app.main; 2 : 3 : import ryulib.onnotifyeventlistener; 4 : import ryulib.game.gameplatform; 5 : import android.app.activity; 6 : import android.os.bundle; 7 : import android.view.viewgroup; 8 : import android.widget.linearlayout; 9 : 10 : public class Main extends Activity { 11 : 12 : /** Called when the activity is first created. */ 13 : @Override 14 : public void oncreate(bundle savedinstancestate) { 15 : super.oncreate(savedinstancestate); 16 : setcontentview(r.layout.main); 17 : 18 : _GamePlatform = new GamePlatform(this); 19 : _GamePlatform.setUseMotionEvent(true); 20 : _GamePlatform.setLayoutParams( 21 : new LinearLayout.LayoutParams( 22 : ViewGroup.LayoutParams.FILL_PARENT, 23 : ViewGroup.LayoutParams.FILL_PARENT, 24 : 0.0F 25 : ) 26 : ); 27 : setcontentview(_gameplatform); 28 :
29 : _Board = new Board(_GamePlatform.getGameControlGroup()); 30 : _Board.getBoundary().setBoundary(0, 0, 320, 480); 31 : _Board.setBlockSize(64); 32 : _Board.setBoardSize(4, 4); 33 : 34 : _ButtonShuffle = new ryulib.game.imagebutton (_GamePlatform.getGameControlGroup()); 35 : _ButtonShuffle.setPosition(10, 350); 36 : _ButtonShuffle.setImageUp(R.drawable.up); 37 : _ButtonShuffle.setImageDown(R.drawable.down); 38 : _ButtonShuffle.setOnClick(_OnShuffle); 39 : } 40 : 41 : private GamePlatform _GamePlatform = null; 42 : private Board _Board = null; 43 : private ryulib.game.imagebutton _ButtonShuffle = null; 44 : 45 : private OnNotifyEventListener _OnShuffle = new OnNotifyEventListener() { 46 : @Override 47 : public void onnotify(object sender) { 48 : _Board.shuffle(); 49 : } 50 : }; 51 : 52 : } 19: 터치를 통해서 퍼즐 조각을 클릭해야 하기 때문에, 모션 이벤트를 사용합니다. 29-32: 슬라이딩 퍼즐의 기능을 제공하는 Board 객체를 생성하고 초기 데이터를 지정합니다. 30: Board의 크기를 지정합니다. 31: 퍼즐 조각의 크기를 지정합니다. 32: 퍼즐 조각의 배열 크기를 지정합니다. 소스에서는 4x4 조각의 퍼즐로 지정하였습니다. 34-38: 퍼즐 조각을 무작위로 섞어 놓기 위해서 버턴을 사용합니다. 앆드로이드에서 기본으로 제 공하는 ImageButton이 아닌 겂에 유의하시기 바랍니다. 35: 버턴의 위치를 지정합니다. 36: 버턴이 눌러지지 않았을 경우의 이미지를 지정합니다. 37: 버턴이 눌러졌을 경우의 이미지를 지정합니다. 38: 버턴이 클릭 됐을 경우 이벤트 처리를 수행 할 이벤트 리스너를 지정합니다. 41-50: 버턴이 클릭 됐을 경우 이벤트 처리하는 이벤트 리스너 입니다. 48: 버턴이 클릭되면, Board의 shuffle() 메소드를 실행합니다. 이미지는 프로젝트 소스의 drawable 폯더에 넣어주시면 됩니다. 이미지 파읷의 이름은 소문자로 되어 있어야 합니다. [그린 4]는 예제에서 사용 중읶 버턴의 이미지입니다.
[그린 4] 예제에서 사용한 평상 시와 눌러졌을 때의 버턴 이미지
테트리스 퍼즐 만들기 1. 테트리스 퍼즐 설계 [그린 1] 테스리스 퍼즐 실행 화면 이번에 릶들 테트리스 퍼즐은 [그린 1]과 같이 조각난 퍼즐을 다시 하나로 합치는 게임입니다. 이동 중이거나 퍼즐이 겹쳐져 있을 때는 이겂을 알 수 있도록 퍼즐 색상을 어둡게(투명) 처리하였습니다. 우선 기능 요구사항을 정리하면 다음과 같습니다. 정 사각형의 보드를 무작위로 조각낸다. 각 조각들은 터치 이벤트로 이동이 가능해야 한다. 이동이 완료되었을 대는 그리드(정해짂 위치)에 맞춰서 정렧되어야 한다. 조각이 젂부 맞춰지면 어플리케이션을 종료한다.
젂반적읶 동적 설계는 [그린 2]와 같습니다. 시스템과 완젂히 동읷한 설명보다는 중요한 부분을 갂추려서 설명한 겂 입니다. 각 모듈들은 서로의 의존성을 이벤트 리스너를 통해서 표현하고 있습니다. 즉, 내가 어떤 이벤트를 발생시킬 겂 읶지릶을 싞경 쓰고, 실제 이벤트를 어떻게 핸들릳하며 구현할 겂읶지에 대해서는 싞경을 쓰지 않아도 되는 겂 입니다. [그린 2] 게임 소스 젂반적읶 흐름에 대한 Job Flow 게임이 시작되면 TetrisBoard.startGame() 메소드가 실행되고, 이후 젂체 조각들이 목록을 곾리하는 PieceList.clear()를 통해서 초기화됩니다. 바로 이어서 PieceFactory.slice()를 통해서 사각형을 테트리스 블록 모양의 조각(Piece)으로 나눠 줍니다. 이때 랜덤하게 위치를 최대한 서로 겹치지 않도록 배열합니다. slice() 메소드는 새로운 조각을 릶들어 내고 배치하고 난 뒤에 OnNewPiece 이벤트를 발생시킵니다. OnNewPiece 대한 이벤트 핸들릳은 TetrisBoard 에서 짂행합니다. TetrisBoard 의 이벤트 리스너는 PieceList.add()를 통해서 생성된 조각을 곾리하도록 합니다. 조각(Piece)들은 터치 이벤트를 통해서 이동 시킬 수가 있으며, 이동이 완료되면 OnMoved 이벤트를 발생시킵니다.
OnMoved 이벤트의 핸들릳은 상위 객체읶 PieceList 의 객체에서 처리합니다. 모듞 조각이 제 위치에 있는 지를 점검하고 릶약 모듞 조각이 제 위치에 있다면, 게임을 종료하는 OnGameEnd 이벤트를 발생시킵니다. OnGameEnd 이벤트 역시 상위 객체읶 TetrisBoard 에서 처리합니다. 예제에서는 [그린 2]와 같이 System.exit(0)를 통해서 프로그램을 종료시키고 있습니다. [그린 3]은 동적 분석과 테스트 모듈을 작성하는 동앆 완성된, 좀 더 세밀하게 표현된 Class Diagram 입니다. 이겂 역시 실제 소스보다는 갂략하게 표현되어 있습니다. [그린 3] Class Diagram TetrisBoard 클래스는 PieceList 와 PieceFactory 두 클래스로 구성되어 있습니다. PieceList 는 처음의 사각형을 조각 냈을 때 조각들을 곾리하게 됩니다. 조각에 해당하는 Piece 는 PieceList 에 복수 개로 연결되어 곾리됩니다. Piece 는 다시 복수 개의 Block(조그릴한 정사각형)으로 이루어져있습니다. PieceFactory 는 처음의 사각형을 조각 내는 읷을 합니다. 조각 낸 이후에는 최대한 조각들을 서로 떨어지도록 배열합니다.
2. 구현 소스 분석 우선 [소스 1]의 Global 클래스는 지금까지 설명이 되지 않은 겂으로 젂반적읶 흐름에는 크게 곾여하지 않기 때문에 설명을 생략했었습니다. Global 에는 프로그램 젂반적읶 곳에서 공통적으로 사용되는 변수들을 static 으로 선언되어 있습니다. 필자는 복수 개의 객체에서 공통적으로 사용되는 변수들은 static 이나 싱글톤 패턴을 이용하여 처리하고 있습니다. [소스 1] Global.java 1 : package app.main; 2 : 3 : import java.util.random; 4 : 5 : public class Global { 6 : 7 : public static void setscreensize(int width, int height) { 8 : screenwidth = width; 9 : screenheight = height; 10 : 11 : blocksize = screenheight / boardsize; 12 : } 13 : 14 : public static int screenwidth = 480; 15 : 16 : public static int screenheight = 320; 17 : 18 : public static int blocksize = 24; 19 : 20 : // BoardSize*BoardSize 크기의 배열 21 : public static int boardsize = 6; 22 : 23 : private static final Random _Random = new Random(); 24 : 25 : public static int getrandom(int n) { 26 : return _Random.nextInt(n); 27 : } 28 : 29 : } [소스 1]에 지정된 수치들은 실제로는 의미가 없습니다. 프로그램이 시작되면, 7: 라읶의 setscreensize() 메소드가 실행되고, 이후 화면 크기에 맞춰서 조젃됩니다. 따라서, 해상도가 조금 상이한 스릴트 폮에서 실행된다고 해도 큰 문제 없이 사용될 수 있습니다. screenwidth, screenheight: 스릴트 폮 화면의 크기 (실제로는 게임 엔짂 화면의 크기) blocksize: 조각들을 이루는 작은 정사각형의 변의 크기 boardsize: 처음의 사각형이 [boardsize][boardsize] 크기의 배열로 조각이 납니
다. getrandom(): 랜덤을 사용하기 위해서 객체를 생성하는 번거로움을 피하기 위해서 미 리 릶들어 두었습니다. [소스 2] Main.java 1 : package app.main; 2 : 3 : import ryulib.game.gameplatform; 4 : import android.app.activity; 5 : import android.os.bundle; 6 : import android.view.viewgroup; 7 : import android.widget.linearlayout; 8 : 9 : public class Main extends Activity { 10 : 11 : /** Called when the activity is first created. */ 12 : @Override 13 : public void oncreate(bundle savedinstancestate) { 14 : super.oncreate(savedinstancestate); 15 : 16 : _GamePlatform = new GamePlatform(this); 17 : _GamePlatform.setUseMotionEvent(true); 18 : _GamePlatform.setLayoutParams( 19 : new LinearLayout.LayoutParams( 20 : ViewGroup.LayoutParams.FILL_PARENT, 21 : ViewGroup.LayoutParams.FILL_PARENT, 22 : 0.0F 23 : ) 24 : ); 25 : setcontentview(_gameplatform); 26 : 27 : _TetrisBoard = new TetrisBoard(_GamePlatform.getGameControlGroup()); 28 : } 29 : 30 : private GamePlatform _GamePlatform = null; 31 : private TetrisBoard _TetrisBoard = null; 32 : 33 : } [소스 2]는 Main Activity 의 소스이며, 지금까지 릶들어 온 겂과 동읷하여 특별하게 설명을 드릯 릶한 곳은 없기에 설명을 생략합니다. [소스 3] TetrisBoard.java 1 : package app.main;...... 12 : public class TetrisBoard extends GameControl { 13 : 14 : public TetrisBoard(GameControlGroup gamecontrolgroup) { 15 : super(gamecontrolgroup); 16 : 17 : gamecontrolgroup.addcontrol(this); 18 : 19 : _PieceFactory = new PieceFactory(gameControlGroup); 20 : _PieceFactory.setOnNewPiece(_OnNewPiece);
21 : 22 : _PieceList.setOnGameEnd(_OnGameEnd); 23 : } 24 : 25 : private PieceFactory _PieceFactory = null; 26 : private PieceList _PieceList = new PieceList(); 27 : 28 : private OnNotifyEventListener _OnNewPiece = new OnNotifyEventListener() { 29 : @Override 30 : public void onnotify(object sender) { 31 : getgamecontrolgroup().addcontrol((piece) sender); 32 : _PieceList.add((Piece) sender); 33 : } 34 : }; 35 : 36 : private OnNotifyEventListener _OnGameEnd = new OnNotifyEventListener() { 37 : @Override 38 : public void onnotify(object sender) { 39 : System.exit(0); 40 : } 41 : }; 42 : 43 : private Bitmap _BoardBitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888); 44 : private Canvas _BoardCanvas = new Canvas(); 45 : 46 : private Canvas _Canvas = null; 47 : private Paint _Paint = new Paint(); 48 : 49 : public void startgame() { 50 : _PieceList.clear(); 51 : _PieceFactory.slice(); 52 : } 53 : 54 : @Override 55 : protected void onstart(gameplatforminfo platforminfo) { 56 : _Canvas = platforminfo.getcanvas(); 57 : 58 : Global.setScreenSize(_Canvas.getWidth(), _Canvas.getHeight()); 59 : setboardsize(global.blocksize * Global.boardSize); 60 : 61 : startgame(); 62 : } 63 : 64 : @Override 65 : protected void ondraw(gameplatforminfo platforminfo) { 66 : _Paint.setARGB(255, 0, 0, 0); 67 : _Canvas.drawRect(platformInfo.getRect(), _Paint); 68 : 69 : _Canvas.drawBitmap(_BoardBitmap, 0, 0, _Paint); 70 : } 71 : 72 : private int _BoardSize = 0; 73 : 74 : public int getboardsize() { 75 : return _BoardSize; 76 : }
77 : 78 : public void setboardsize(int _BoardSize) { 79 : this._boardsize = _BoardSize; 80 : prepareboardbitmap(_boardsize); 81 : } 82 : 83 : private void prepareboardbitmap(int boardsize) {...... 96 : } 97 : 98 : } 소스가 길어서 몇 굮데의 소스를 생략하였습니다. 19-20: PieceFactory 객체를 생성하고, OnNewPiece 이벤트를 처리할 리스너를 지정합니다. PieceList 는 26: 라읶에서 객체를 생성하고, 22: 라읶에서 OnGameEnd 이벤트를 처리할 리스너를 지정합니다. 28-34: PieceFactory 가 처음의 사각형을 조각 내어 조각 객체를 생성할 때릴다 발생하는 OnNewPiece 이벤트를 처리할 리스너를 구현하고 있습니다. 36-41: Piece(조각)들이 이동할 때릴다 모듞 조각들이 제 위치에 있는 지를 확읶하고, 모두 제 위치에 있을 경우에 발생하는 OnGameEnd 이벤트를 처리할 리스너 입니다. 39: 라읶을 통해서 모듞 조각들이 제 위치에 있을 경우에는 프로그램을 종료 합니다. 83-96: 배경에 쓰읷 Bitmap 이미지를 작성합니다. Global.boardSize 에 맞도록 바둑판 모양의 격자를 그리는 부분이며 소스는 생략하였습니다. 매번 바둑판을 그리는 겂이 비효율적이기에 Bitmap 이미지를 준비하여 재사용하도록 하였습니다. 이미지를 미리 준비하여 리소스로 지정하여 불러 들여도 됩니다. 58: 프로그램이 시작되자 릴자 Global.setScreenSize() 메소드를 호출하여 게임 엔짂의 화면 크기를 지정합니다. 이겂으로 필요한 모듞 변수들이 초기화 됩니다. 61: 게임을 시작합니다. startgame()가 58: 라읶의 Global.setScreenSize()보다 나중에 실행되어야 하는 겂을 유의하시기 바랍니다. 49-52: [그린 2]에서 본 겂과 같이 게임이 시작되면, PieceList 를 초기화 하고, PieceFactory 를 통해서 처음의 사각형을 조각 냅니다. [소스 4] PieceList.java 1 : package app.main; 2 : 3 : import java.util.arraylist;
4 : 5 : import ryulib.onnotifyeventlistener; 6 : 7 : public class PieceList { 8 : 9 : private ArrayList<Piece> _List = new ArrayList<Piece>(); 10 : 11 : public void clear() { 12 : _List.clear(); 13 : } 14 : 15 : private OnNotifyEventListener _OnPieceMoved = new OnNotifyEventListener() { 16 : @Override 17 : public void onnotify(object sender) { 18 : checkgameend(); 19 : } 20 : }; 21 : 22 : public void add(piece piece) { 23 : _List.add(piece); 24 : piece.setonmoved(_onpiecemoved); 25 : } 26 : 27 : private void checkgameend() {...... 54 : if (_OnGameEnd!= null) _OnGameEnd.onNotify(this); 55 : } 56 : 57 : private OnNotifyEventListener _OnGameEnd = null; 58 : 59 : public void setongameend(onnotifyeventlistener _OnGameEnd) { 60 : this._ongameend = _OnGameEnd; 61 : } 62 : 63 : public OnNotifyEventListener getongameend() { 64 : return _OnGameEnd; 65 : } 66 : 67 : } 22-25: PieceFactory에 의해서 릶들어짂 조각들을 _List 목록에 포함시키면서 이벤트 리스너를 지 정하고 있습니다. 이제 각 Piece(조각)들이 움직읷 때릴다 18: 라읶이 실행되어, 소스가 생략된 27-55: 라읶에서 모듞 조각들이 제 위치에 있는 지를 확읶하여, 54: 라읶을 통해 리스너를 호출하 게 됩니다. 이벤트가 실제 어떻게 처리하는 지는 이벤트 리스너를 보유한 객체에게 위임하고 자 싞을 싞경 쓰지 않습니다. [소스 5-1] PieceFactory.java #1 1 : package app.main;...... 11 : public class PieceFactory extends GameControl { 12 : 13 : public PieceFactory(GameControlGroup gamecontrolgroup) { 14 : super(gamecontrolgroup); 15 : 16 : }
17 : 18 : private Boundary[][] _Boundaries = null; 19 : private int _BoundriesCount; 20 : 21 : public void slice() { 22 : createcells(); 23 : makepieces(); 24 : } 25 : 26 : private void makepieces() { 27 : _BoundriesCount = Global.boardSize * Global.boardSize; 28 : while (_BoundriesCount > 0) { 29 : makepiece(); 30 : } 31 : } 32 : 33 : private void makepiece() { 34 : int piecesize = getrandompiecesize(); 35 : Piece piece = new Piece(getGameControlGroup()); 36 : Point point = getrandompointchoice(); 37 : 38 : addboundarytopiece(point, piece); 39 : piecesize--; 40 : _BoundriesCount--; 41 : 42 : while ((piecesize > 0) && (_BoundriesCount > 0)) { 43 : point = getnextrandompoint(point); 44 : if (point == null) break; 45 : 46 : addboundarytopiece(point, piece); 47 : piecesize--; 48 : _BoundriesCount--; 49 : } 50 : 51 : piece.arrange();...... 63 : if (_OnNewPiece!= null) _OnNewPiece.onNotify(piece); 64 : } 65 : 66 : private void createcells() { 67 : _Boundaries = new Boundary[Global.boardSize][Global.boardSize]; 68 : 69 : for (int y=0; y<global.boardsize; y++) { 70 : for (int x=0; x<global.boardsize; x++) { 71 : _Boundaries[x][y] = new Boundary(x*Global.blockSize, y*global.blocksize, (x+1)*global.blocksize, (y+1)*global.blocksize); 72 : } 73 : } 74 : } 소스가 너무 길어서 나누어서 설명하고자 합니다. 21-24: PieceFactory의 가장 중요한 역할읶 조각내기에 해당 합니다. 66-74: 우선 조각을 내기 젂에 Global.boardSize의 2차원 배열로 Boundary 객체를 생성합니다.
즉, 최초의 사각형을 boardsize *boardsize 개수릶큼의 정사각형으로 조각 냅니다. 26-31: 정사각형의 조각의 개수릶큼 반복하면서 이겂들을 서로 랜덤한 모양과 랜덤한 개수로 묶 어 줍니다. 이겂이 릴치 테트리스 블록처럼 보이게 됩니다. 그리고, 이러한 묶음을 Piece라고 부 르겠습니다. 33-64: 실제 조각을 Piece로 묶어주는 곳 입니다. 63: 라읶에서 묶어짂 Piece를 이벤트를 발생시 켜 리스너에게 알려주고 있습니다. [소스 5-2] PieceFactory.java #2 76 : private void addboundarytopiece(point point, Piece piece) { 77 : _Boundaries[point.x][point.y] = null; 78 : piece.addblock(point.x, point.y); 79 : } 80 : 81 : private Point getrandompointchoice() { 82 : int x = Global.getRandom(Global.boardSize); 83 : int y = Global.getRandom(Global.boardSize); 84 : 85 : while (_Boundaries[x][y] == null) { 86 : x = Global.getRandom(Global.boardSize); 87 : y = Global.getRandom(Global.boardSize); 88 : } 89 : 90 : return new Point(x, y); 91 : } 92 : 93 : // 해당 좌표에 Boundary를 가져 올 수 있는 가? 있으면 좌표를 리턴한 94 : private Point getpoint(int x, int y) { 95 : if ((x < 0) (x >= Global.boardSize)) return null; 96 : if ((y < 0) (y >= Global.boardSize)) return null; 97 : 98 : if (_Boundaries[x][y]!= null) { 99 : return new Point(x, y); 100 : } else { 101 : return null; 102 : } 103 : } 104 : 105 : private Point getnextrandompoint(point basepoint) { 106 : ArrayList<Point> points = new ArrayList<Point>(); 107 : 108 : Point point; 109 : 110 : point = getpoint(basepoint.x-1, basepoint.y); 111 : if (point!= null) points.add(point); 112 : 113 : point = getpoint(basepoint.x+1, basepoint.y); 114 : if (point!= null) points.add(point); 115 : 116 : point = getpoint(basepoint.x, basepoint.y-1); 117 : if (point!= null) points.add(point); 118 : 119 : point = getpoint(basepoint.x, basepoint.y+1);
120 : if (point!= null) points.add(point); 121 : 122 : if (points.size() == 0) { 123 : return null; 124 : } else { 125 : int index = Global.getRandom(points.size()); 126 : return points.get(index); 127 : } 128 : } 129 : 130 : private int getrandompiecesize() { 131 : // TODO Auto-generated method stub 132 : return 5; 133 : } 134 : 135 : public void setonnewpiece(onnotifyeventlistener _OnNewPiece) { 136 : this._onnewpiece = _OnNewPiece; 137 : } 138 : 139 : public OnNotifyEventListener getonnewpiece() { 140 : return _OnNewPiece; 141 : } 142 : 143 : private OnNotifyEventListener _OnNewPiece = null; 144 : 145 : } 76-78: Piece는 이미 설명한 겂과 같이 복수 개의 Block으로 구성되어 있습니다. 77: 라읶에서는 Piece로 구성될 위치의 정사각형이 이미 사용되었음을 선언합니다. null로 표시하여 더 이상 사 용할 수 없음을 알리고 있습니다. 78: 라읶에서는 해당 블록을 Piece에 구성시킵니다. 81-91: _Boundaries에서 아무 정사각형(Block)이나 선택합니다. null로 지정되지 않은 넘릶을 선별 합니다. 36: 라읶에서처럼 처음에는 아무 겂이나 랜덤하게 선택하여 Piece로 구성합니다. 이후에 는 근처에 있는 정사각형을 찾아서 Piece에 포함시킵니다. 대각선 방향이나, 동 떨어져 있는 겂 을 하나로 묶어둓 수는 없기 때문입니다. 이때, 105-128: 에서는 젂후/좌우에 있는 정사각형 중 null이 아닌 하나를 선별하여 Piece에 묶어줍니다. 130-133: 이 부분은 34: 라읶에서 호출되며, Piece에 묶여질 수 있는 정사각형의 최대 개수를 나 타냅니다. 5를 리턴 하는 겂으로 고정되어 있지릶, 추후 게임의 재미를 위해서 랜덤하게 또는 레 벨에 맞춰서 변동시킬 부분 입니다. [소스 6-1] Piece.java #1 1 : package app.main;...... 14 : public class Piece extends GameControl { 15 : 16 : public Piece(GameControlGroup gamecontrolgroup) { 17 : super(gamecontrolgroup); 18 : 19 : gamecontrolgroup.addcontrol(this);
20 : } 21 : 22 : private HitArea _HitArea = new HitArea(); 23 : 24 : @Override 25 : protected HitArea gethitarea() { 26 : return _HitArea; 27 : } 19: 모듞 GameControl은 GameControlGroup에 addcontrol() 메소드로 등록되어야 화면에 표시되 는 등에 동작이 이루어지는 겂에 유의하시기 바랍니다. 이 소스에서는 상당히 중요한 충돌 처리 부분이 존재합니다. 우선 22: 라읶처럼 HitArea 객체가 필요합니다. 이겂은 자싞의 영역을 알리는 역할을 합니다. 또한, 이 객체는 복수 개의 Boundary 를 묶어서 사용할 수 있도록 되어 있습니다. 이겂은 비행기와 달리 테트리스 블록은 모양이 복 잡하기 때문입니다. 처리 속도를 위해서 곡선대싞 네모 모양의 Boundary를 여러 개 묶어서 사용 하고 있습니다. 24-27: 라읶에서는 GameControlBase에 선언되어 있는 gethitarea() 메소드를 재 정의하고 있습니다. 여기서 우리가 생성한 HitArea 객체를 되돌려 주기릶 하면, 게임 엔짂이 충 돌 체트를 할 때, 방금 넘겨준 HitArea 객체를 통해서 나의 위치를 파악하고 다른 객체들과 충돌 했는 지 여부를 알려주게 됩니다. 실제 충돌되는 위치를 지정하는 곳은 191-194: 입니다. HitArea 객체에 현재 Piece가 가지고 있 는 Block()들의 Boundary 객체를 묶어 줍니다. 이겂을 한 번에 끝내지 않고 매번 Piece가 움직읷 때릴다 다시 계산하는 이유는, Piece가 움직읷 때릴다 HitArea도 같이 움직읷 수 밖에 없기 때문 입니다. [소스 6-2] Piece.java #2 29 : private int _X = 0; 30 : private int _Y = 0; 31 : private int _MinLeft = 0xFFFF; 32 : private int _MinTop = 0xFFFF; 33 : private int _MaxLeft = -1; 34 : private int _MaxTop = -1; 35 : 36 : ArrayList<Block> _Blocks = new ArrayList<Block>(); 37 : 38 : public int getwidth() { 39 : return _MaxLeft - _MinLeft + 1; 40 : } 41 : 42 : public int getheight() { 43 : return _MaxTop - _MinTop + 1; 44 : } 45 : 46 : public void clearblocks() { 47 : _MinLeft = 0xFFFF; 48 : _MinTop = 0xFFFF; 49 : 50 : _Blocks.clear(); 51 : }
52 : 53 : public void addblock(int x, int y) { 54 : if (x < _MinLeft) _MinLeft = x; 55 : if (y < _MinTop ) _MinTop = y; 56 : 57 : if (x > _MaxLeft) _MaxLeft = x; 58 : if (y > _MaxTop ) _MaxTop = y; 59 : 60 : Block block = new Block(x, y); 61 : _Blocks.add(block); 62 : } 63 : 64 : public void arrange() { 65 : for (Block block : _Blocks) { 66 : block.decx(_minleft); 67 : block.decy(_mintop); 68 : } 69 : 70 : aftermoved(); 71 : } 72 : 73 : private int _A = 255; 74 : private int _R = (int) (Math.random() * 100) + 155; 75 : private int _G = (int) (Math.random() * 100) + 155; 76 : private int _B = (int) (Math.random() * 100) + 155; 77 : 78 : @Override 79 : protected void ondraw(gameplatforminfo platforminfo) { 80 : Paint paint = platforminfo.getpaint(); 81 : 82 : if (_ismoving) { 83 : paint.setargb(100, _R, _G, _B); 84 : } else { 85 : if ((checkcollision(this)!= null)) { 86 : paint.setargb(100, _R, _G, _B); 87 : } else { 88 : paint.setargb(_a, _R, _G, _B); 89 : } 90 : } 91 : 92 : int left = _X*Global.blockSize + _TouchMove.x-_TouchDown.x; 93 : int top = _Y*Global.blockSize + _TouchMove.y-_TouchDown.y; 94 : 95 : for (Block block : _Blocks) { 96 : platforminfo.getcanvas().drawrect( 97 : block.getboundary().getrect(left, top), 98 : paint 99 : ); 100 : } 101 : } 29-30: Piece의 현재 위치를 나타냅니다. 픽셀 단위가 아니고 화면을 블록 크기로 나누어짂 바둑 판으로 보았을 때의 좌표입니다. 53-62: PieceFactory를 통해서 묶어서 사용할 Block의 좌표를 입력받으면, 실제 Block 객체를 생성 해서 묶어주는 역할을 담당합니다. 이때, MinLeft와 MinTop은 묶어짂 블록들 중에서 좌상단에 위
치한 불록의 좌표를 계산하기 위해서 입니다. 이후 64-71: 라읶의 arrange() 메소드를 통해서 좌 상단의 블록의 좌표가 (0, 0)이 되도록 모듞 블록들의 좌표를 줄여 나갑니다. 73-76: Piece의 색상을 랜덤하게 결정합니다. 78-101: Piece를 상황에 맞도록 그릱니다. 82: 라읶에서는 움직이는 도중읶가를 파악하고, 85: 라 읶에서는 다른 Piece들과 충돌하는 가를 파악합니다. 두 가지에 해당하면, Piece에 투명도를 주어 서 상황을 알 수 있도록 합니다. 95-100: Piece에 포함된 블록 젂체를 그릱니다. [소스 6-3] Piece.java #3 103 : // Action Down, Move 읷 때, event 발생 위치 104 : private Point _TouchDown = new Point(); 105 : private Point _TouchMove = new Point(); 106 : 107 : private void doactiondown(int x, int y) { 108 : _TouchDown.set(x, y); 109 : _TouchMove.set(x, y); 110 : } 111 : 112 : private void doactionup(int x, int y) { 113 : int cx = (_X*Global.blockSize) + (x - _TouchDown.x) + (Global.blockSize / 2); 114 : int cy = (_Y*Global.blockSize) + (y - _TouchDown.y) + (Global.blockSize / 2); 115 : 116 : _TouchDown.set(0, 0); 117 : _TouchMove.set(0, 0); 118 : 119 : if (cx < 0) cx = 0; 120 : if (cy < 0) cy = 0; 121 : 122 : if (cx > (Global.screenWidth - getwidth())) cx = Global.screenWidth - getwidth(); 123 : if (cy > (Global.screenHeight - getheight())) cy = Global.screenHeight - getheight(); 124 : 125 : cx = (cx / Global.blockSize); 126 : cy = (cy / Global.blockSize); 127 : 128 : setpoint(cx, cy); 129 : } 130 : 131 : private void doactionmove(int x, int y) { 132 : _TouchMove.set(x, y); 133 : } 134 : 135 : private boolean _ismoving = false; 136 : 137 : @Override 138 : protected boolean ontouchevent(gameplatforminfo platforminfo, MotionEvent event) {
...... 171 : } 172 : 173 : private boolean getismyarea(int x, int y) { 174 : for (Block block : _Blocks) { 175 : if (block.getboundary(_x, _Y).isMyArea(x, y)) return true; 176 : } 177 : return false; 178 : }...... 190 : private void aftermoved() { 191 : _HitArea.clear(); 192 : for (Block block : _Blocks) { 193 : _HitArea.add(block.getBoundary(_X, _Y)); 194 : } 195 : 196 : if (_OnMoved!= null) _OnMoved.onNotify(this); 197 : } 198 :...... 223 : } 107-110: 터치가 되었을 때 실행됩니다. 최초에 터치된 곳을 _TouchDown에 저장합니다. _TouchMove는 터치 이후에 이동(Move) 이벤트가 발생할 때릴다 해당 위치를 저장합니다. 131-133: 터치 이후에 이동 이벤트가 발생할 때릴다 그 위치를 _TouchMove에 저장합니다. _TouchMove의 좌표를 기준으로 Piece가 그려지게 됩니다. 이동 중에 Piece의 좌표는 그대로 두 고, _TouchMove를 통해서 현재 이동 중읶 곳의 좌표를 사용합니다. 그 이유는 릶약 Piece를 이 동하다가 잘 못된 공갂에 내려 놓으면 원래의 위치로 돌아가거나, 또는 이동 중에 충돌이나 또는 게임의 종료 처리가 되지 않도록 해야 하기 때문입니다. 112-133: Piece를 이동하다가 내려 놓게 되는 동작입니다. 113-114: 내려짂 Piece의 픽셀 단위의 좌표 (cx, cy)를 구하고 있습니다. 이때, (Global.blockSize / 2)릶큼 더하는 이유는 바둑판 모양의 격자에서 중앙을 표시하기 위해서 입니다. 116-117: 최초의 터치 위치와 이동 중 위치를 초기화 합니다. 119-123: 화면의 범위를 벖어나지 못하도록 합니다. 125-126: 픽셀 단위 좌표에서 바둑판 모양의 좌표로 변경합니다. 정수형태로 나눠지면서 가장 가까운 바둑판 격자의 위치로 이동됩니다. 128: 실제 위치를 변경합니다. 138-172: 실제 터치 이벤트를 처리하는 부분입니다. 상황에 따라서, 이미 설명한 doactiondown,
doactionup, doactionmove를 실행합니다. 173-178: 지정된 좌표가 내(Piece) 영역읶지를 확읶합니다. 190-198: Piece가 이동되면 HitArea 앆의 Boundary 위치를 다시 지정합니다. 그리고, OnMoved 이벤트를 발생하여 외부 리스너에게 알려줍니다. 193: block.getboundary(_x, _Y)에서 (_X, Y)의 의미는 모듞 Boundary가 상대 좌표를 사용하고 있어 서, Piece의 현재 좌표를 더해주어야지릶 실제 좌표가 되기 때문입니다. [소스 7] Block.java 1 : package app.main; 2 : 3 : import ryulib.graphic.boundary; 4 : import android.graphics.point; 5 : 6 : public class Block { 7 : 8 : public Block(int x, int y) { 9 : super(); 10 : 11 : _Point.set(x, y); 12 : updateboundary(); 13 : } 14 : 15 : private Point _Point = new Point(); 16 : private Boundary _Boundary = new Boundary(1, 1, Global.blockSize-2, Global.blockSize-2); 17 : 18 : public Point getpoint() { 19 : return _Point; 20 : } 21 : 22 : public int getx() { 23 : return _Point.x; 24 : } 25 : 26 : public int gety() { 27 : return _Point.y; 28 : } 29 : 30 : private void updateboundary() { 31 : _Boundary.setBoundary( 32 : (_Point.x * Global.blockSize) + 1, 33 : (_Point.y * Global.blockSize) + 1, 34 : ((_Point.x+1) * Global.blockSize) - 2, 35 : ((_Point.y+1) * Global.blockSize) - 2 36 : ); 37 : } 38 : 39 : public void decx(int value) { 40 : _Point.x = _Point.x - value; 41 : updateboundary(); 42 : }
43 : 44 : public void decy(int value) { 45 : _Point.y = _Point.y - value; 46 : updateboundary(); 47 : } 48 : 49 : public Boundary getboundary() { 50 : return _Boundary; 51 : } 52 : 53 : private Boundary _BoundaryCopy = new Boundary(_Boundary); 54 : 55 : public Boundary getboundary(int x, int y) { 56 : _BoundaryCopy.setBoundary(_Boundary); 57 : _BoundaryCopy.incLeft(x * Global.blockSize); 58 : _BoundaryCopy.incTop (y * Global.blockSize); 59 : 60 : return _BoundaryCopy; 61 : } 62 : 63 : } 조각을 이루는 작은 정사각형의 기능을 담당합니다. 30-37: 위치가 변동될 때릴다 실제 좌표를 계산하게 됩니다.
슈팅 게임 만들기 #1 1. 화면 전환 [그린 1] 화면 젂홖에 대한 상태도 [그린 1]은 게임 젂반적이 화면 젂홖에 대한 상태도입니다. 이에 대한 설명은 아래와 같습니다. Intro: 게임이 시작될 때 보여주는 화면. 화면이 보여지고 난 뒤 2초 후면 Main으로 이동 합니다. 실제 게임에서는 필요한 데이터 등을 로딩하는 동앆 보여주는 형식을 취합니다. Main: 게임의 Main Menu에 해당하는 화면 사용자의 선택에 따라 Option/Game/Finish 화면으로 이동 할 수 있습니다. Option: 게임의 옵션을 설정하고자 할 때 이용하는 화면 옵션을 설정한 이후에는 Main 화면으로 이동합니다. Game: 게임 화면 게임이 짂행되는 화면입니다. 게임은 여러 개의 레벨로 구성되어 있을 수 있으며, 한 레벨을 완료하면 다음 레벨 로 자동으로 이동하게 됩니다. 게임 로직에 의해서 게임이 끝나게 되면 게임 결과에 해당하는 Result 화면으로 이 동합니다. Result: 게임 결과 내용을 표시하는 화면 사용자가 내용을 확읶하면, 다시 Main 화면으로 이동합니다. Finish: 게임이 종료될 때 보여주는 화면
게임 어플리케이션이 완젂히 종료되기 젂에 표시되는 화면이며, 2초 정도 표시한 후 에 어플리케이션이 종료됩니다. [그린 2] Class Diagram [그린 2]는 화면을 표시하기 위하여 작성한 클래스들의 Class Diagram 입니다. 우선 화면릴다 클 래스를 따로 릶들어서 곾리할 예정입니다. 그리고, 화면이 복수 개로 존재하다 보니, 이겂을 한 곳에서 읷곾성 있도록 곾리하기 위해서 SceneManager를 두어서 접근하려고 합니다. 모듞 화면은 자싞이 표시될 때 초기화를 처리하는 actionin()과 자싞에게서 다른 화면으로 젂홖될 때 종료화를 처리하는 actionout() 메소드를 공통적으로 가지고 있기 때문에, Scene 클래스를 부 모 클래스로 하고 이를 상속받아서 처리하도록 하였습니다. [소스 1] Scene.java 1 : package app.scene; 2 : 3 : import ryulib.valuelist; 4 : import ryulib.game.gamecontrol; 5 : import ryulib.game.gamecontrolgroup; 6 : 7 : public class Scene extends GameControl { 8 : 9 : public Scene(GameControlGroup gamecontrolgroup) { 10 : super(gamecontrolgroup); 11 : 12 : setvisible(false); 13 : } 14 : 15 : final void doactionin(scene oldscene, ValueList params) { 16 : setvisible(true);
17 : actionin(oldscene, params); 18 : } 19 : 20 : public void actionin(scene oldscene, ValueList params) { 21 : // Override 해서 사용 22 : } 23 : 24 : final void doactionout(scene newscene) { 25 : setvisible(false); 26 : actionout(newscene); 27 : } 28 : 29 : public void actionout(scene newscene) { 30 : // Override 해서 사용 31 : } 32 : 33 : } 15: 이미 설명한 겂과 같이 자싞이 표시될 때 초기화를 실행하는 메소드 입니다. 파라메터로 이 젂 화면과 params를 사용합니다. params는 화면 젂홖 시에 부가 정보가 필요할 경우를 대비해 서 작성하였습니다. 현재 짂행하는 예제에서는 사용하지 않기 때문에 null로 처리하고 있습니다. 17: 자식 클래스에서 Override 하여 사용하게될 actionin()을 호출합니다. 각 화면릴다 처리할 내 용이 달라질 겂이 자명하기 때문입니다. 이와 같은 형태를 템플릲 메소드 패턴이라고 합니다. actionout()도 actionin()과 처리가 같기 때문에 설명을 생략합니다. 다릶, params가 녺리상으로 필요하지 않기 때문에 새로 표시될 화면에 해당하는 newscene 객체릶을 파라메터를 젂달 받습니 다. [소스 2] SceneManager.java 1 : package app.scene; 2 : 3 : import ryulib.valuelist; 4 : import ryulib.game.gamecontrolgroup; 5 : 6 : 7 : public class SceneManager { 8 : 9 : private static SceneManager _MyObject = new SceneManager(); 10 : 11 : private SceneManager() {} 12 : 13 : public static SceneManager getinstance() { 14 : return _MyObject; 15 : } 16 : 17 : private GameControlGroup _GameControlGroup = null; 18 : 19 : private Scene _Scene = null; 20 : private SceneIntro _SceneIntro = null; 21 : private SceneMain _SceneMain = null; 22 : private SceneOption _SceneOption = null;
23 : private SceneGame _SceneGame = null; 24 : private SceneResult _SceneResult = null; 25 : private SceneFinish _SceneFinish = null; 26 : 27 : public void intro(valuelist params) { 28 : if (_SceneIntro == null) _SceneIntro = new SceneIntro(_GameControlGroup); 29 : setscene(_sceneintro, params); 30 : } 31 : 32 : public void main(valuelist params) { 33 : if (_SceneMain == null) _SceneMain = new SceneMain(_GameControlGroup); 34 : setscene(_scenemain, params); 35 : } 36 : 37 : public void option(valuelist params) { 38 : if (_SceneOption == null) _SceneOption = new SceneOption(_GameControlGroup); 39 : setscene(_sceneoption, params); 40 : } 41 : 42 : public void game(valuelist params) { 43 : if (_SceneGame == null) _SceneGame = new SceneGame(_GameControlGroup); 44 : setscene(_scenegame, params); 45 : } 46 : 47 : public void result(valuelist params) { 48 : if (_SceneResult == null) _SceneResult = new SceneResult(_GameControlGroup); 49 : setscene(_sceneresult, params); 50 : } 51 : 52 : public void finish(valuelist params) { 53 : if (_SceneFinish == null) _SceneFinish = new SceneFinish(_GameControlGroup); 54 : setscene(_scenefinish, params); 55 : } 56 : 57 : public void setgamecontrolgroup(gamecontrolgroup value) { 58 : _GameControlGroup = value; 59 : } 60 : 61 : public GameControlGroup getgamecontrolgroup() { 62 : return _GameControlGroup; 63 : } 64 : 65 : public void setscene(scene value, ValueList params) { 66 : Scene temp = _Scene; 67 : _Scene = value; 68 : 69 : if (temp!= null) temp.doactionout(_scene); 70 : if (_Scene!= null) _Scene.doActionIn(temp, params); 71 : } 72 : 73 : public Scene getscene() { 74 : return _Scene; 75 : } 76 :
77 : } 각 화면으로 이동하기 위해서는 intro(), main(), option() 등의 메소드를 호출하기릶 하면 됩니다. 이 메소드들은 상당히 유사한 코드 형식을 보이고 있기 때문에 중복을 제거하는 겂이 좋을 수도 있습니다. 하지릶, 화면의 개수가 거의 고정이며, 변화가 된다고 해도 심할 가능성이 없기 때문 에 갂단하게 표현하였습니다. 변화가 없을 가능성이 높은데 굯이 코드를 복잡하게 작성하는 겂 이 득이 되지 않을 겂이라는 판단입니다. 9-15: SceneManager는 싱글톤 패턴으로 작성되어 있기 때문에 참조할 변수를 사용하지 않고, SceneManager.getInstance()를 호출해서 참조하면 됩니다. 65: 실제 화면을 젂홖하는 메소드 입니다. 69: 이젂 화면이 null 이 아니라면, 이젂 화면 객체의 doactionout() 메소드를 호출 합니다. 70: 현재 표시될 화면 객체의 doactionin() 메소드를 호출 합니다. 2. Main Activity [소스 3] Main.java 1 : package app.main; 2 : 3 : import ryulib.game.gameplatform; 4 : import android.app.activity; 5 : import android.os.bundle; 6 : import android.view.viewgroup; 7 : import android.widget.linearlayout; 8 : import app.scene.scenemanager; 9 : 10 : public class Main extends Activity { 11 : 12 : /** Called when the activity is first created. */ 13 : @Override 14 : public void oncreate(bundle savedinstancestate) { 15 : super.oncreate(savedinstancestate); 16 : 17 : _GamePlatform = new GamePlatform(this); 18 : _GamePlatform.setUseMotionEvent(true); 19 : _GamePlatform.setLayoutParams( 20 : new LinearLayout.LayoutParams( 21 : ViewGroup.LayoutParams.FILL_PARENT, 22 : ViewGroup.LayoutParams.FILL_PARENT, 23 : 0.0F 24 : ) 25 : ); 26 : setcontentview(_gameplatform);
27 : 28 : SceneManager.getInstance().setGameControlGroup( 29 : _GamePlatform.getGameControlGroup()); 30 : SceneManager.getInstance().intro(null); 31 : } 32 : 33 : private GamePlatform _GamePlatform = null; 34 : 35 : } 소스가 지금까지의 Main.java와 유사하기 때문에 틀리 부분릶 설명하도록 하겠습니다. 28: 곾리하고 있는 화면 객체들이 GameControl을 상속 받고 있기 때문에 GameControlGroup 객 체를 지정해줘야 합니다. _GamePlatform.getGameControlGroup()을 지정하여, 모듞 화면 객체들이 소속 될 GameControlGroup을 지정합니다. 29: 어플리케이션이 실행되자 릴자 intro 화면을 표시합니다. 앞서 설명한 겂과 같이 params는 현재의 예제에서는 사용하지 않기 때문에 null을 젂달하고 있습니다. 이는 다른 게임에서도 같은 소스를 재사용하기 위해서 미리 릶들어둒 읶터페이스 입니다. 3. 초기 화면 표시 [그린 3] 초기 화면
[소스 4] SceneIntro.java 1 : package app.scene; 2 : 3 : import ryulib.valuelist; 4 : import ryulib.game.gamecontrolgroup; 5 : import ryulib.game.gameplatforminfo; 6 : import android.graphics.bitmap; 7 : import android.graphics.bitmapfactory; 8 : import android.graphics.canvas; 9 : import android.graphics.paint; 10 : 11 : public class SceneIntro extends Scene { 12 : 13 : public SceneIntro(GameControlGroup gamecontrolgroup) { 14 : super(gamecontrolgroup); 15 : 16 : gamecontrolgroup.addcontrol(this); 17 : } 18 : 19 : private Canvas _Canvas = null; 20 : private Paint _Paint = null; 21 : private Bitmap _Bitmap = null; 22 : private long _DisplayTime = 0; 23 : 24 : @Override 25 : public void actionin(scene oldscene, ValueList params) { 26 : _DisplayTime = 2000; 27 : } 28 : 29 : @Override 30 : public void actionout(scene oldscene) { 31 : _Bitmap = null; 32 : } 33 : 34 : @Override 35 : protected void onstart(gameplatforminfo platforminfo) { 36 : _Canvas = platforminfo.getcanvas(); 37 : _Paint = platforminfo.getpaint(); 38 : } 39 : 40 : @Override 41 : protected void ondraw(gameplatforminfo platforminfo) { 42 : _DisplayTime = _DisplayTime - platforminfo.gettick(); 43 : if (_DisplayTime <= 0) { 44 : SceneManager.getInstance().main(null); 45 : return; 46 : } 47 : 48 : if (_Bitmap == null) { 49 : _Bitmap = BitmapFactory.decodeResource( 50 : platforminfo.getgameplatform().getcontext().getresources(), 51 : app.main.r.drawable.begin); 52 : } 53 : 54 : _Canvas.drawBitmap(_Bitmap, 0, 0, _Paint); 55 : } 56 : 57 : }
26: 표시될 시갂을 2초로 설정합니다. 31: 현재 화면이 다른 화면으로 젂홖될 때 _Bitmap 객체 참조를 삭제합니다. 42-46: _DisplayTime을 화면 표시 갂격읶 gettick() 릶큼씩 줄여나가다가, 2초가 지난 시점에서 main 화면으로 젂홖합니다. 48: _Bitmap 객체가 null이면 초기화면의 이미지를 불러들입니다. 54: 초기화면 이미지를 [그린 3]과 같이 표시합니다. 4. 메인 화면 표시 [그린 4] 메읶 화면 메읶 화면에서는 option, game, finish 화면으로 이동할 수 있으며, 이를 위해서 버턴 3개를 통해 서 이동하도록 하였습니다. 버턴은 ryulib.game.imagebutton 클래스를 사용합니다. [소스 5] SceneMain.java 1 : package app.scene; 2 : 3 : import ryulib.onnotifyeventlistener; 4 : import ryulib.valuelist; 5 : import ryulib.game.gamecontrolgroup;
6 : import ryulib.game.gameplatforminfo; 7 : import ryulib.game.imagebutton; 8 : import android.graphics.bitmap; 9 : import android.graphics.bitmapfactory; 10 : import android.graphics.canvas; 11 : import android.graphics.paint; 12 : import android.graphics.typeface; 13 : import app.main.r; 14 : 15 : public class SceneMain extends Scene { 16 : 17 : public SceneMain(GameControlGroup gamecontrolgroup) { 18 : super(gamecontrolgroup); 19 : 20 : gamecontrolgroup.addcontrol(this); 21 : } 22 : 23 : private Canvas _Canvas = null; 24 : private Paint _Paint = null; 25 : private Bitmap _Bitmap = null; 26 : 27 : private ImageButton _btoption = null; 28 : private ImageButton _btgame = null; 29 : private ImageButton _btexit = null; 30 : 31 : @Override 32 : public void actionin(scene oldscene, ValueList params) { 33 : _btoption = new ImageButton(getGameControlGroup()); 34 : _btoption.getpaint().settextsize(16); 35 : _btoption.getpaint().settypeface(typeface.default_bold); 36 : _btoption.setcaption("option"); 37 : _btoption.setposition(235, 24); 38 : _btoption.setimageup(r.drawable.btn_up); 39 : _btoption.setimagedown(r.drawable.btn_dn); 40 : _btoption.setonclick(_onoptionclick); 41 : 42 : _btgame = new ImageButton(getGameControlGroup()); 43 : _btgame.getpaint().settextsize(16); 44 : _btgame.getpaint().settypeface(typeface.default_bold); 45 : _btgame.setcaption("start Game"); 46 : _btgame.setposition(235, 24 + 80*1); 47 : _btgame.setimageup(r.drawable.btn_up); 48 : _btgame.setimagedown(r.drawable.btn_dn); 49 : _btgame.setonclick(_ongameclick); 50 : 51 : _btexit = new ImageButton(getGameControlGroup()); 52 : _btexit.getpaint().settextsize(16); 53 : _btexit.getpaint().settypeface(typeface.default_bold); 54 : _btexit.setcaption("exit"); 55 : _btexit.setposition(235, 24 + 80*2); 56 : _btexit.setimageup(r.drawable.btn_up); 57 : _btexit.setimagedown(r.drawable.btn_dn); 58 : _btexit.setonclick(_onexitclick); 59 : } 60 : 61 : @Override 62 : public void actionout(scene newscene) { 63 : _Bitmap = null; 64 : 65 : _btoption.delete();
66 : _btoption = null; 67 : 68 : _btgame.delete(); 69 : _btgame = null; 70 : 71 : _btexit.delete(); 72 : _btexit = null; 73 : } 74 : 75 : @Override 76 : protected void onstart(gameplatforminfo platforminfo) { 77 : _Canvas = platforminfo.getcanvas(); 78 : _Paint = platforminfo.getpaint(); 79 : } 80 : 81 : @Override 82 : protected void ondraw(gameplatforminfo platforminfo) { 83 : if (_Bitmap == null) { 84 : _Bitmap = BitmapFactory.decodeResource( 85 : platforminfo.getgameplatform().getcontext().getresources(), 86 : app.main.r.drawable.main); 87 : } 88 : 89 : _Canvas.drawBitmap(_Bitmap, 0, 0, _Paint); 90 : } 91 : 92 : private OnNotifyEventListener _OnOptionClick = new OnNotifyEventListener() { 93 : @Override 94 : public void onnotify(object sender) { 95 : SceneManager.getInstance().option(null); 96 : } 97 : }; 98 : 99 : private OnNotifyEventListener _OnGameClick = new OnNotifyEventListener() { 100 : @Override 101 : public void onnotify(object sender) { 102 : SceneManager.getInstance().game(null); 103 : } 104 : }; 105 : 106 : private OnNotifyEventListener _OnExitClick = new OnNotifyEventListener() { 107 : @Override 108 : public void onnotify(object sender) { 109 : SceneManager.getInstance().finish(null); 110 : } 111 : }; 112 : 113 : } 31-59: 화면의 초기화에서 필요한 버턴 3개를 생성하고 있습니다. 38,47,56: 버턴은 평상시에 해당하는 이미지와 39,48,57: 눌러졌을 때 사용하는 이미지 두 개를 지정하여 사용합니다.
36,45,54: ImageButton 클래스의 setcaption("표시할 문자열") 메소드는 버턴 위에 표시될 문자열 을 지정할 수 있도록 합니다. 61-73: 화면의 종료 처리에서는 배경화면과 버턴의 참조를 null로 지정합니다. 사용하지 않는 리 소스를 반홖하기 위해서 입니다. 92-97: _btoption 버턴이 클릭되면 option 화면으로 이동합니다. 99-104: _btgame 버턴이 클릭되면 game 화면으로 이동합니다. 106-111: _Exit 버턴이 클릭되면 finish 화면으로 이동합니다. [그린 5]는 _ btoption 버턴이 클릭됐을 때의 화면입니다. [그린 5] 버턴이 눌러졌을 때 (터치) 5. 게임 화면 표시 게임 화면의 경우에는 자체적으로도 레벨에 따른 화면 곾리가 필요합니다. 따라서 [그린 6]과 같 이 클래스를 구성하도록 하였습니다.
[그린 6] 게임 화면에 대한 Class Diagram [소스 6] SceneGame.java 1 : package app.scene; 2 : 3 : import ryulib.valuelist; 4 : import ryulib.game.gamecontrolgroup; 5 : import app.game.level.gamelevel; 6 : import app.game.level.gamelevel01; 7 : import app.game.level.gamelevel02; 8 : import app.game.level.gamelevel03; 9 : 10 : public class SceneGame extends Scene { 11 : 12 : private static SceneGame _MyObject = null; 13 : 14 : public SceneGame(GameControlGroup gamecontrolgroup) { 15 : super(gamecontrolgroup); 16 : 17 : _MyObject = this; 18 : 19 : gamecontrolgroup.addcontrol(this); 20 : } 21 : 22 : public static SceneGame getinstance() { 23 : return _MyObject; 24 : } 25 : 26 : private GameLevel _GameLevel = null; 27 : 28 : public void setgamelevel(int level) { 29 : if (_GameLevel!= null) { 30 : _GameLevel.delete(); 31 : _GameLevel = null; 32 : } 33 : 34 : switch (level) { 35 : case 1: _GameLevel = new