상세 컨텐츠

본문 제목

JetBoy 따라하기 #6 - 레이저 발사

프로그래밍/Android

by ryujt 2010. 12. 28. 15:58

본문

이번에는 우주선에서 레이저를 발사하여 소행성을 폭발시켜 보겠습니다.  다만, 소행성의 폭발 애니메이션은 잠시 미루기로 하고, 우주선과의 충돌 테스트와 마찬가지로 충돌하면 바로 소행성이 사라지도록 하겠습니다.



미사일 리소스 관리 클래스 추가

[소스 1] ResourceLaser.java
package app.resources;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import app.main.R;

public class ResourceLaser {

	private static ResourceLaser _Instance = new ResourceLaser();

	public static ResourceLaser getInstance(){
		return _Instance;
	}
	
	private ResourceLaser() {}
	  
	private Bitmap _Bitmap = null;
	
    public static final int WIDTH = 27;
    public static final int HEIGHT = 12;
    
	public Bitmap getBitmap() {
		if (_Bitmap == null) {
			_Bitmap = 
				BitmapFactory.decodeResource(
					Resource.getInstance().getResources(), R.drawable.laser
				);
		}
		
		return _Bitmap;
	}

}
레이저의 경우에는 이미지가 한 장이기 때문에 배경과 유사하게 처리합니다.  즉, Bitmap의 배열이 형태가 아니기 때문에, BitmapList에서 상속 받을 이유가 없습니다.

레이저의 이미지 크기는 가로 27 픽셀이며, 세로는 12픽셀입니다.



레이저 관리 클래스

당연하다고 생각이 되어 소행성의 경우 설명을 생략했는데, 같은 또는 유사한 객체가 여러 개 등장하는 경우 이들을 하나로 묶어주는 클래스가 있는 것이 효율적일 경우가 많습니다.  불특정 다수를 다루기 보다, 하나의 객체에게 기능을 요구하고, 해당 관리 객체가 다수의 객체를 대신 관리해주는 경우 등 입니다.

간혹, 레이저 또는 소행성과 같은 경우 프로그램 상위 계층에 배열로 처리하는 경우가 있는데, 이렇게 되면 논리적으로 다른 코드가 서로 섞이게 될 수 있습니다.

따라서, 소행성 때와 마찬가지로, [그림 1]과 같이 레이저를 관리해주는 클래스를 따로 두어서 레이저 내부 코드를 캡슐화하도록 하겠습니다.

[그림 1] Class Diagram

Lasers 객체가 Laser 객체들을 생성만 해주고, 그 이후에는 더 이상 관리 할 필요가 없기 때문에, 목록에 대한 레퍼런스를 저장 할 멤버를 가지고 있지 않습니다.  때로는, 지속적인 관리가 필요한 경우가 있을 수도 있습니다.  그때는 배열이나 ArrayList 등을 레퍼런스를 관리해주시면 됩니다.

[소스 2] Lasers.java
package app.main;

import ryulib.game.GameControl;
import ryulib.game.GameControlGroup;

public class Lasers extends GameControl {

	private static final int _FIRE_INTERVAL = 250;

	public Lasers(GameControlGroup gameControlGroup) {
		super(gameControlGroup);

	}
	
	private long _OldTick = 0;
	
	public void Fire(int x, int y) {
		long tick = System.currentTimeMillis();

		if ((_OldTick == 0) || ((tick-_OldTick) >= _FIRE_INTERVAL)) {
			Laser laser = new Laser(null);
			laser.Init(x, y);
			this.getGameControlGroup().AddControl(laser);

			_OldTick = tick;
		}
	}

}
Lasers에는 Fire() 메소드 하나만 제공합니다.  즉, "레이저를 발사하라"라는 요구사항이 발생하면 거기에 맞춰서 레이저를 발사만 하면 그만인 것 입니다.  레이저가 어떻게 나가고 언제 폭발하는 지 등에 대해서는 알필요가 없는 것 입니다.

또한, 이전 발사 시간을 기록해 뒀다가, 지정된 간격 이내의 발사 요청은 무시하도록 되어 있습니다.


[소스 3] Laser.java
package app.main;

import ryulib.game.GameControl;
import ryulib.game.GameControlGroup;
import ryulib.game.GamePlatformInfo;
import ryulib.game.HitArea;
import ryulib.graphic.Boundary;
import android.graphics.Canvas;
import android.graphics.Paint;
import app.resources.ResourceLaser;

public class Laser extends GameControl {

	private static final int _SPEED = 200;
	private static final int _HORIZONTAL_MARGINE = 0;
	private static final int _VERTICAL_MARGINE   = 0;
	
	public Laser(GameControlGroup gameControlGroup) {
		super(gameControlGroup);
		
		_HitArea.Add(_HitBoundary);
	}
	
	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private int _X = 0;
	private int _Y = 0;
	private Scroll _Scroll = new Scroll(_SPEED);
	
	// 충돌 검사 할 때 자신이 차지하고 있는 영역을 알려준다.  
	private HitArea _HitArea = new HitArea();

	// Boundary는 Rect와 유사한 기능을 한다.  
	private Boundary _HitBoundary = new Boundary(0, 0, 0, 0);
	
	@Override
	protected HitArea getHitArea() {
		_HitBoundary.setBoundary(
				_X + _HORIZONTAL_MARGINE, 
				_Y + _VERTICAL_MARGINE, 
				_X + ResourceLaser.WIDTH - _HORIZONTAL_MARGINE*2, 
				_Y + ResourceLaser.HEIGHT - _VERTICAL_MARGINE*2
		);		
		
		return _HitArea;
	}
	
	public void Init(int x, int y) {
		_X = x - ResourceLaser.WIDTH;
		_Y = y - (ResourceLaser.HEIGHT / 2);
	}

	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		_Canvas.drawBitmap(ResourceLaser.getInstance().getBitmap(), _X, _Y, _Paint);
		
		GameControl GameControl = this.CheckCollision(this);		
		if ((GameControl != null) && (GameControl instanceof Asteroid)) {
			this.Delete();
			GameControl.Delete();
		}
		
		_X = _X + _Scroll.Move(platformInfo.getTick());
		
		if (_X > _Canvas.getWidth()) this.Delete();
	}
}
소행성과 거의 유사한 코드입니다.  다만, 움직이는 방향과 속도가 다르며, 충돌을 검사하는 코드가 있다는 점도 다릅니다.  

그런데, 왜 소행성에서 테스트 안하고, 우주선과 레이저 두 곳에서 충돌 테스트를 하는 걸까요?  그것은 소행성 갯수만큼 테스트를 하는 것보다, 비교적으로 숫자가 적은 객체에서 테스트를 하는 편이 유리하기 때문입니다.

64-68: 충돌을 검사하고, 충돌이 감지되면 미사일과 소행성 모두가 사라집니다.



레이저 발사 이벤트 구현

레이저 발사 이벤트를 감지하는 곳을 Lasers로 해도 문제없으나, 의미상 우주선에서 발사하는 것이 자연스럽다고 생각되어, Ship.java를 [소스 4]와 같이 수정합니다.

[소스 4] Ship.java 수정
package app.main;

import ryulib.game.GameControl;
import ryulib.game.GameControlGroup;
import ryulib.game.GamePlatformInfo;
import ryulib.game.HitArea;
import ryulib.graphic.Boundary;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.KeyEvent;
import android.view.MotionEvent;
import app.resources.Resource;
import app.resources.ResourceShip;

public class Ship extends GameControl {

	private static final int _ANIMATIONINTERVAL = 100;
	private static final int _SPEED = 150;
	private static final int _HORIZONTAL_MARGINE = 20;
	private static final int _VERTICAL_MARGINE   = 15;
	
	public Ship(GameControlGroup gameControlGroup) {
		super(gameControlGroup);
	
		_HitArea.Add(_HitBoundary);
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private JoyStick _JoyStick = new JoyStick(_SPEED);
	
	private AnimationCounter _AnimationCounter = 
		new AnimationCounter(
				_ANIMATIONINTERVAL, 
				ResourceShip.getInstance().getCount()
		);
	
	private Lasers _Lasers = new Lasers(null);
	
	// 충돌 검사 할 때 자신이 차지하고 있는 영역을 알려준다.  
	private HitArea _HitArea = new HitArea();

	// Boundary는 Rect와 유사한 기능을 한다.  
	private Boundary _HitBoundary = new Boundary(0, 0, 0, 0);
	
	@Override
	protected HitArea getHitArea() {
		// 비행기의 가상자리에서는 충돌이 일어나지 않도록 마진을 주고 있다.
		_HitBoundary.setBoundary(
				_JoyStick.getX() + _HORIZONTAL_MARGINE, 
				_JoyStick.getY() + _VERTICAL_MARGINE, 
				_JoyStick.getX() + ResourceShip.WIDTH - _HORIZONTAL_MARGINE*2, 
				_JoyStick.getY() + ResourceShip.HEIGHT - _VERTICAL_MARGINE*2
		);
		
		return _HitArea;
	}
	
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
		
		getGameControlGroup().AddControl(_Lasers);		
		
		_JoyStick.PrepareOrientationSensor(Resource.getInstance().getContext());

		_JoyStick.setBoundaryLimit(
				true, 
				0, 0, _Canvas.getWidth(), _Canvas.getHeight(),
				ResourceShip.WIDTH, ResourceShip.HEIGHT
		);
	}
	
	@Override
	protected void onTick(GamePlatformInfo platformInfo) {
		long tick = platformInfo.getTick();
		
		_AnimationCounter.Tick(tick);
		_JoyStick.Tick(tick);
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		_Canvas.drawBitmap(
				ResourceShip.getInstance().getBitmap(_AnimationCounter.getIndex()), 
				_JoyStick.getX(), _JoyStick.getY(), 
				_Paint
		);
		
		GameControl GameControl = this.CheckCollision(this);
		if ((GameControl != null) && (GameControl instanceof Asteroid)) {
			this.Delete();
			GameControl.Delete();
		}
	}
	
	@Override
    protected boolean onTouchEvent(GamePlatformInfo platformInfo, MotionEvent event) {
		_Lasers.Fire(
				_JoyStick.getX() + ResourceShip.WIDTH, 
				_JoyStick.getY() + (ResourceShip.HEIGHT / 2));
		
		return true;
    }
    
}
39: Lasers 객체를 생성합니다.

65: Lasers 객체를 Ship 객체와 같은 레벨의 그룹으로 묶습니다.  그룹이 서로 다르면 충돌이 일어나지 않습니다.

99-106: 화면이 눌러지면 바로 레이저를 발사하도록 합니다.  내부적으로는 일정 간격 이하의 요청은 무시합니다.

103: 우주선의 중간에서 레이저가 발사되도록 좌표를 조정합니다.



정리

지금까지 레이저 발사에 대한 구현을 알아보았습니다.  다음에는 소행성이 단순하게 사라지지 않고, 폭발하는 애니메이션 동작과 함께 사라지는 방법에 대해서 알아보겠습니다.



소스

관련글 더보기