안드로이드개발 - 5 회 게임으로배우는안드로이드개발 이제연재의마무리를하게되었습니다. 이번연재에서는지난연재에서구축한슈팅게임의플랫폼에실제게임모듈을얹어서슈팅게임을완성하도록하겠습니다. 이번연재에서는소리를다루는방법과방향센서를이용하여우주선을움직이는방법을다뤄보도록하겠습니다. 글류종택 ryujt658@hanmail.net http://ryulib.tistory.com/ ( 트위터 : @RyuJongTaek) 연재순서 1. 심플하고가벼운안드로이드게임엔짂 게쪽 소개 2. 슬라이딩퍼즐만들기 3. 테트리스퍼즐만들기 4. 슈팅게임만들기 #1 5. 슈팅게임만들기 #2
1. 게임관련패키지추가 이번연재의소스에는아래와같이, 두개의패키지가추가되었습니다. 이패키지에있는소스들은 http://ryulib.tistory.com/ 에서 Jet Boy 따라하기 라는제목으로설명되어있으니참고하시기바랍니다. app.game.resource 게임에서필요한이미지와소리파일을관리합니다. app.game.common 게임에필요한우주선과같은기초클래스들을관리합니다. app.game.resource 패키지에포함된클래스는다음과같습니다. Resource: 리소스를불러들이기위해서 Context 객체의레퍼런스를보관합니다. ResourceSound: 음향효과리소스를관리합니다. ResourceBackground: 배경화면이미지를관리합니다. ResourceShip: 우주선의이미지를관리합니다. ResourceLaser: 레이저의이미지를관리합니다. ResourceAsteroid: 소행성의이미지를관리합니다. ResourceExplodingAsteroid: 소행성이폭발했을때의이미지를관리합니다. app.game.common 패키지에포함된클래스는다음과같습니다. Global: 젂역적으로참조해야하는객체에대한레퍼런스를보관합니다. Messages: 게임에서사용하는메시지의상수들을관리합니다. GameInformation: 남은우주선의개수, 현재점수등의정보를관리합니다. BackgroundImage: 배경이미지를표시하고스크롤하는방법을제공합니다. Background: 배경이미지를관리합니다. 원근감을살리기위하여배경이미지두장이서로다른속도로스크롤하도록합니다. Ship: 우주선을표시하고이동합니다. ShipAnimater: 우주선이비행하고폭발하는것을표현합니다. Laser: 우주선에서발사되는레이저를표현합니다. Lasers: 복수의레이저객체를통합해서관리합니다. Asteroid: 소행성을표현합니다. Asteroids: 복수의소행성객체를통합해서관리합니다. 2. SceneGame 수정 [ 소스 1] SceneGame.java 1 : package app.scene;
2 : 3 : import ryulib.valuelist; 4 : import ryulib.game.gamecontrolgroup; 5 : import app.game.common.background; 6 : import app.game.common.global; 7 : import app.game.common.ship; 8 : import app.game.level.gamelevel; 9 : import app.game.level.gamelevel01; 10 : import app.game.level.gamelevel02; 11 : import app.game.level.gamelevel03; 12 : 13 : public class SceneGame extends Scene { 14 : 15 : private static SceneGame _MyObject = null; 16 : 17 : public SceneGame(GameControlGroup gamecontrolgroup) { 18 : super(gamecontrolgroup); 19 : 20 : _MyObject = this; 21 : 22 : gamecontrolgroup.addcontrol(this); 23 : } 24 : 25 : public static SceneGame getinstance() { 26 : return _MyObject; 27 : } 28 : 29 : private Background _Background = null; 30 : private Ship _Ship = null; 31 : 32 : private GameLevel _GameLevel = null; 33 : 34 : public void setgamelevel(int level) { 35 : Global.gameInformation.setGameLevel(level); 36 : 37 : if (_GameLevel!= null) { 38 : _GameLevel.actionOut(); 39 : _GameLevel = null; 40 : } 41 : 42 : switch (level) { 43 : case 1: _GameLevel = new GameLevel01(getGameControlGroup()); break; 44 : case 2: _GameLevel = new GameLevel02(getGameControlGroup()); break; 45 : case 3: _GameLevel = new GameLevel03(getGameControlGroup()); break; 46 : default: ; // TODO : raise Exception 47 : } 48 : 49 : getgamecontrolgroup().addcontrol(_gamelevel); 50 : 51 : _GameLevel.actionIn(); 52 : } 53 : 54 : @Override 55 : public void actionin(scene oldscene, ValueList params) { 56 : Global.gameInformation.startGame(); 57 : 58 : _Background = new Background(getGameControlGroup());
59 : _Ship = new Ship(getGameControlGroup()); 60 : 61 : getgamecontrolgroup().addcontrol(_background); 62 : getgamecontrolgroup().addcontrol(_ship); 63 : getgamecontrolgroup().addcontrol(global.gameinformation); 64 : 65 : setgamelevel(1); 66 : } 67 : 68 : @Override 69 : public void actionout(scene newscene) { 70 : if (_GameLevel!= null) { 71 : _GameLevel.actionOut(); 72 : _GameLevel = null; 73 : } 74 : 75 : Global.gameInformation.delete(); 76 : _Ship.delete(); 77 : _Background.delete(); 78 : 79 : _Ship = null; 80 : _Background = null; 81 : } 82 : 83 : } 29-30: 배경이미지와우주선을표현할객체를사용할것입니다. 54-66: 화면이 SceneGame 으로변경되었을때실행되는메소드입니다. 56: 게임정보를관리하는객체의레퍼런스는여러곳에서접근할수있기때문에, Global 클래스 에 static 으로선언되어있습니다. 게임이시작되었음을알리고있습니다. 58: 배경이미지를관리할객체를생성합니다. 59: 우주선을관리할객체를생성합니다. 61-63: 게임관련객체들을 GameControlGroup 에등록합니다. 65: 게임레벨을 1 부터시작하도록합니다. 이번연재에서는게임레벨은 1 에서고정되어있습 니다. 레벨을클리어하고다음레벨로이동하는소스는 http://ryulib.tistory.com/ 를통해서추후 배포하도록하겠습니다. 68-81: 게임이끝나서 SceneGame 화면이다른화면으로젂홗될때실행되는메소드입니다. static으로선언된 gameinformation을제외하고는참조를 null로지정하여삭제하고있습니다. 이는게임이메모리를많이사용하게될경우에, 필요없는객체를바로바로삭제하고필요할때만메모리를사용하도록하기위함입니다.
3. GameLevel01 수정 [ 소스 2] GameLevel01.java 1 : package app.game.level; 2 : 3 : import app.game.common.asteroids; 4 : import ryulib.game.gamecontrolgroup; 5 : 6 : public class GameLevel01 extends GameLevel { 7 : 8 : public GameLevel01(GameControlGroup gamecontrolgroup) { 9 : super(gamecontrolgroup); 10 : 11 : } 12 : 13 : private Asteroids _Asteroids = null; 14 : 15 : @Override 16 : public void doactionin() { 17 : _Asteroids = new Asteroids(getGameControlGroup()); 18 : getgamecontrolgroup().addcontrol(_asteroids); 19 : } 20 : 21 : @Override 22 : public void doactionout() { 23 : _Asteroids.delete(); 24 : _Asteroids = null; 25 : } 26 : 27 : } 15-19: 해당게임레벨이시작될때, 실행되는메소드입니다. 17-18: 소행성을관리하는 Asteroids 객체를생성하고, GameControlGroup 에등록합니다. 이로써, 무작위로소행성이생성되어화면에스크롤됩니다. 이때, 우주선이소행성과충돌하면안되며, 레이저를쏴서소행성을폭파시키면점수가올라갑니다. 21-25: 해당게임레벨이종료될때, 실행되는메소드입니다. 23-24: SceneGame 때와마찬가지로필요없을때, 객체를삭제합니다. 4. SceneResult 수정 [ 소스 3] SceneResult.java
27 : @Override 28 : protected void ondraw(gameplatforminfo platforminfo) { 29 : _Paint.setARGB(255, 0, 0, 0); 30 : _Canvas.drawRect(0, 0, _Canvas.getWidth(), _Canvas.getHeight(), _Paint); 31 : 32 : int point = Global.gameInformation.getPoint(); 33 : 34 : _Paint.setARGB(255, 255, 255, 255); 35 : _Canvas.drawText("Game Result...", 100, 100, _Paint); 36 : _Canvas.drawText(" * Point : " + Integer.toString(point), 100, 120, _Paint); 37 : _Canvas.drawText(" Touch the screen to continue...", 100, 200, _Paint); 38 : } 우주선이모두폭파되면게임이끝나면서 SceneResult 화면으로젂홖됩니다. 표시하는것만을추가한상태입니다. 이때얻은점수를 32: gameinformation 객체에서현재의점수를얻어오고있습니다. 5. JoyStick 클래스설명 JoyStick 클래스는센서나키보드또는터치이벤트등을이용해서객체의위치를변경할수있도 록도와주는클래스입니다. ryulib.game 패키지에포함되어있으며, JoyStickinterface 클래스에서 상속받아확장된클래스입니다. JoyStickinterface의설명 Method tick(long; Property int getx(), int gety(), setx(int), sety(int) 객체의현재위치 ( 좌표 ) 가저장되는속성입니다. int getspeed(), setspeed(int) 객체가이동하는속도를지정합니다. 초당이동할수있는픽셀수를지정합니다. int getleftlimit(), setleftlimit(int), int gettoplimit(), settoplimit(int), int getrightlimit(), set RightLimit(int), int getbottomlimit(), set BottomLimit(int) 객체가이동할수있는범위가저장되는속성입니다. 일반적으로화면크기가지정됩니다. int getobjectwidth(), set ObjectWidth(int), int getobjectheight, setobjectheight(int) 객체의크기가저장되는속성입니다. 객체가화면의오른쪽과하단을넘어서지
않도록합니다. boolean isuseboundarylimit(), setuseboundarylimit(boolean) 객체가 LeftLimit, TopLimit, TopLimit, RightLimit, BottomLimit 범위를넘어서게할것인지를결정합니다. 6. 우주선구현 우주선구현의기본적인소스와설명은 http://ryulib.tistory.com/99 에서확인하실수가있습니다. 구현된소스에대한상세한설명이되어있으며, 다양한방법을통해서우주선이이동할수있도록되어있습니다. 이번연재에서는방향센서를통해서우주선을이동하는방법을설명합니다. [ 소스 4] Ship.java #1 61 : @Override 62 : protected void onstart(gameplatforminfo platforminfo) { 63 : _Canvas = platforminfo.getcanvas(); 64 : _Paint = platforminfo.getpaint(); 65 : 66 : getgamecontrolgroup().addcontrol(_lasers); 67 : 68 : _JoyStick.PrepareOrientationSensor(Resource.getInstance().getContext ()); 69 : 70 : _JoyStick.setBoundaryLimit( 71 : true, 72 : 0, 0, _Canvas.getWidth(), _Canvas.getHeight(), 73 : ResourceShip.WIDTH, ResourceShip.HEIGHT 74 : ); 75 : } 68: 방향센서를이용하기위해서는센서를초기화해야합니다. 70: 지정된범위를벖어나지못하도록객체가표시되어야할공간의좌표를지정합니다. [ 소스 5] Ship.java #2 77 : @Override 78 : protected void ontick(gameplatforminfo platforminfo) { 79 : GameMessage msg = platforminfo.getgamemessage(); 80 : switch (msg.what) { 81 : case Messages.SHIP_EXPLODE: _ShipAnimater.explode(); break; 82 : } 83 : 84 : long tick = platforminfo.gettick(); 85 : _ShipAnimater.tick(tick);
86 : _JoyStick.tick(tick); 87 : 88 : if (Global.gameInformation.getLife() == 0) { 89 : _JoyStick.setEnabled(false); 90 : 91 : if (_ShipAnimater.isExploding() == false) { 92 : this.delete(); 93 : SceneManager.getInstance().result(null); 94 : } 95 : } 96 : } 79-82: GameMessage는게임컨트롤객체간의메시지젂달의목적으로작성된클래스입니다. 이는 Thread-safe를보증합니다. 현재게쪽이스레드를사용하고있고, 게임의특성상멀티스레드를이용해서개발해야할경우가많이생기게됩니다. 이를위해서준비된클래스입니다. 우선현재는수싞처리에대한설명만하도록하겠습니다. 79: 새로운메시지가있는가를확인합니다. GameMessage는 Broadcast를기본으로합니다. 즉, 어느누굮가가메시지를발송하면젂체게임컨트롤에게메시지가젂달되는형식입니다. 자싞이처리해야할메시지인지를알기위해서는이미정해짂규약을통해서판단하거나, 메시지안에수싞자가식별할수있는데이터를포함시켜야합니다. 여기서는이미정해짂규약을통해서자싞이사용할메시지를안다고가정하고있습니다. 이를통해서얻을수있는이점은메시지를발송하는쪽이나수싞하는쪽모두서로를의식하지않고개발할수있다는점입니다. 결국결합도가낮아져서유연한코드를유지할수있도록합니다. 80: GameMessage 에는여러가지정보를담을수있도록되어있습니다. 여기서는 waht 이라는 정수형변수를통해서어떤종류의메시지인지를판단하도록되어있습니다. 81: Messages 클래스에정의된 SHIP_EXPLODE 메시지가젂달되면자싞의우주선을폭발하도록 합니다. ShipAnimater 객체는우주선의애니메이션동작을컨트롤하는기능을담당합니다. 85: 우주선의현재애니메이션을 tick(int) 메소드에젂달된시간을기준으로표현합니다. 현재우 주선은비행하는동작과폭발하는동작두가지로나누어져있습니다. 폭발의경우에는실제폭 발은아니고, 우주선이흔들리는것으로표현되고있습니다. 86: JoyStick 객체의경우에도 tick(int) 메소드를통해서젂달된시간을기준으로우주선을이동하 고있습니다. 이는스마트폮의성능에따라다른속도로게임이짂행되는것을방지합니다. 88-95: 우주선이모두폭발하고남은것이없다면, JoyStick 을 Disable 시키고, 우주선객체를삭제 합니다. 91-93: 우주선이폭발중애니메이션동작을하는동안에는우주선객체를삭제하지않고기다립 니다. 우주선이폭발애니메이션이다끝난경우에는게임결과화면으로젂홖합니다.
[ 소스 6] Ship.java #3 98 : @Override 99 : protected void ondraw(gameplatforminfo platforminfo) { 100 : _Canvas.drawBitmap( 101 : ResourceShip.getInstance().getBitmap(_ShipAnimater.getIndex()), 102 : _JoyStick.getX(), _JoyStick.getY(), 103 : _Paint 104 : ); 105 : 106 : GameControl GameControl = this.checkcollision(this); 107 : if ((GameControl!= null) && (GameControl instanceof Asteroid)) { 108 : explode(); 109 : ((Asteroid) GameControl).explode(); 110 : } 111 : } 106-110: 우주선과충돌하는객체를찾아서, 이것이소행성일경우에는우주선과소행성모두를폭발시키는과정입니다. 추후에는게임레벨이높아지면서소행성대싞다른객체들을등장시킬예정이기때문에이부분은수정이가해져야하는부분입니다. [ 소스 7] Ship.java #4 113 : @Override 114 : protected boolean ontouchevent(gameplatforminfo platforminfo, MotionEvent event) { 115 : _Lasers.Fire( 116 : _JoyStick.getX() + ResourceShip.WIDTH, 117 : _JoyStick.getY() + (ResourceShip.HEIGHT / 2)); 118 : 119 : return true; 120 : } 121 : 122 : } 115-117: 화면터치가일어나면레이저를발사하도록합니다. 발사되는위치는우주선의젂방 중심입니다. 7. 소행성구현 소행성에대한기본적인설명은 http://ryulib.tistory.com/102 을참고하시기바랍니다. [ 소스 8] Asteroid.java 69 : public void explode() { 70 : Global.gamePlatform.getGamePlatformInfo().addMessage(this,
Messages.ASTEROID_EXPLODE); 71 : 72 : _AnimationCounter.clear(); 73 : _AnimationCounter.setSize(ResourceExplodingAsteroid.getInstance().ge tcount()); 74 : _AnimationCounter.setAutoRewind(false); 75 : 76 : _BitmapList = ResourceExplodingAsteroid.getInstance(); 77 : 78 : ResourceSound.getInstance().boom(); 79 : } 70: GameMessage 를발송하는과정입니다. gameplatforminfo 객체의 addmessage() 메소드는여 러수준의파라메터를지원하도록 overload 되어있습니다. 여기서는정수형메시지만을젂송하 는경우입니다. 72-78: 소행성의폭발애니메이션을처리하고있습니다. 우주선과다르게두종류의 BitmapList 를통해서일반애니메이션과폭발애니메이션을구별하도록되어있습니다. 우주선의경우에는모든이미지가하나의 BitmapList를이루고있고, 몇번째부터몇번째이미지까지사용할지를결정하여구별하도록되어있습니다. 우주선의경우에는 ShipAnimater 클래스가애니메이션을젂담하도록되어있습니다. 필자의경우에는우주선과같은처리를선호합니다. 79: 소행성이폭발하는소리를냅니다. 8. 음향효과입히기 이번연재에서사용하는소스에서는음향효과를위해서 SoundPool 을사용하고있습니다. 안드로 이드에포함된 JetBoy 의경우에는 JetPlayer 를사용하고있습니다. [ 소스 9] ResourceSound.java 1 : package app.game.resource; 2 : 3 : import android.media.audiomanager; 4 : import android.media.soundpool; 5 : import app.main.r; 6 : 7 : public class ResourceSound { 8 : 9 : // 최대스트림갯수 10 : private static final int _STREAM_COUNT = 4; 11 : 12 : private static ResourceSound _Instance = new ResourceSound(); 13 : 14 : public static ResourceSound getinstance(){ 15 : return _Instance;
16 : } 17 : 18 : private ResourceSound() {} 19 : 20 : private SoundPool _SoundPool = new SoundPool(_STREAM_COUNT, AudioManager.STREAM_MUSIC, 0); 21 : 22 : private int _Fire = -1; 23 : private int _Boom = -1; 24 : 25 : public void prepare() { 26 : _Fire = _SoundPool.load( 27 : Resource.getInstance().getContext(), 28 : R.raw.fire, 29 : 1 30 : ); 31 : 32 : _Boom = _SoundPool.load( 33 : Resource.getInstance().getContext(), 34 : R.raw.boom, 35 : 1 36 : ); 37 : } 38 : 39 : public void fire() { 40 : _SoundPool.play(_Fire, 1, 1, 0, 0, 1); 41 : } 42 : 43 : public void boom() { 44 : _SoundPool.play(_Boom, 1, 1, 0, 0, 1); 45 : } 46 : 47 : } 20: SoundPool 객체를생성합니다. 파라메터는최대스트림개수, 스트림타입, 그리고품질로 나누어져있습니다. 이중에서품질은아직사용되지않는것으로 0 으로넘겨주면됩니다. 25-37: 음원데이터를처음로딩할때시간이다소걸릴수가있습니다. 따라서사용하기젂에 미리로딩해서사용합니다. 39-41: 레이저발사하는음향효과를냅니다. 43-45: 소행성이폭발하는음향효과를냅니다. 9. GameInformation 구현 [ 소스 10] GameInformation.java 1 : package app.game.common; 2 :
3 : import ryulib.game.gamecontrol; 4 : import ryulib.game.gamecontrolgroup; 5 : import ryulib.game.gamemessage; 6 : import ryulib.game.gameplatforminfo; 7 : import android.graphics.canvas; 8 : import android.graphics.paint; 9 : 10 : public class GameInformation extends GameControl { 11 : 12 : public static final int SHIP_COUNT = 3; 13 : 14 : public GameInformation(GameControlGroup gamecontrolgroup) { 15 : super(gamecontrolgroup); 16 : // TODO Auto-generated constructor stub 17 : } 18 : 19 : private Canvas _Canvas = null; 20 : private Paint _Paint = null; 21 : 22 : private int _GameLevel = 0; 23 : private int _Life = 0; 24 : private int _Point = 0; 25 : 26 : public void startgame() { 27 : setgamelevel(0); 28 : _Life = SHIP_COUNT; 29 : _Point = 0; 30 : 31 : this.setenabled(true); 32 : this.setvisible(true); 33 : } 34 : 35 : @Override 36 : protected void onstart(gameplatforminfo platforminfo) { 37 : _Canvas = platforminfo.getcanvas(); 38 : _Paint = platforminfo.getpaint(); 39 : } 40 : 41 : @Override 42 : protected void ondraw(gameplatforminfo platforminfo) { 43 : GameMessage msg = platforminfo.getgamemessage(); 44 : switch (msg.what) { 45 : // TODO : GameLevel 마다다른점수부여 46 : case Messages.ASTEROID_EXPLODE: _Point += 100; break; 47 : 48 : case Messages.SHIP_EXPLODE: if (_Life > 0) _Life--; break; 49 : } 50 : 51 : String info = String.format("Level: %2d, Life: %2d, Point: %6d", _GameLevel, _Life, _Point); 52 : 53 : _Paint.setARGB(255, 255, 255, 255); 54 : _Canvas.drawText(info, 10, 20, _Paint); 55 : } 56 : 57 : public void setgamelevel(int value) { 58 : _GameLevel = value; 59 : }
60 : 61 : public int getgamelevel() { 62 : return _GameLevel; 63 : } 64 : 65 : public int getlife() { 66 : return _Life; 67 : } 68 : 69 : public int getpoint() { 70 : return _Point; 71 : } 72 : 73 : } GameInformation 클래스는게임에대한정보를관리합니다. 26-33: 게임이시작되었을때, 모든데이터를초기화합니다. 41-55: 현재의정보를화면에표시합니다. 현재는게임레벨, 남은우주선개수, 점수를문자로 표시하고있습니다. 43-49: GameMessage 객체를통해서점수를올리거나, 남은우주선개수를줄여나갑니다. 10. 연재를마치며 우선, 지금까지부족한연재를지켜봐주싞독자들에게감사합니다. 연재를시작하기이젂부터개인적으로많은시간을투자해서준비했던내용이지만, 엔짂자체도안정화가안된상태에서시작했던만큼아쉬움이많이남습니다. 이후부터는제블로그 (http://ryulib.tistory.com/) 를통해서게쪽은지속적으로업그레이드될예정입니다. 아울러게임의종류도점점더추가할것이니관심있는분들은자주방문해주시면감사하겠습니다.