상세 컨텐츠

본문 제목

JetBoy 따라하기 #3 - 우주선 표시

프로그래밍/Android

by ryujt 2010. 12. 25. 23:34

본문

우주선 리소스 관리 클래스 추가

이제 우주선을 표시하고 움직여 보도록 하겠습니다.  일단 우주선의 이미지를 관리해 줄, 클래스를 ResourceShip이라하고 [소스 1]과 같이 작성하도록 하겠습니다.

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

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import app.main.R;

public class ResourceShip {

	private static ResourceShip _Instance = new ResourceShip();

	public static ResourceShip getInstance(){
		return _Instance;
	}
	
	private ResourceShip() {}
	
    public static final int WIDTH = 122;
    public static final int HEIGHT = 65;

    private Bitmap[] _Bitmaps = null;
	
    public Bitmap getBitmap(int index) {
    	loadBitmaps();
    	return _Bitmaps[index];
    }
    
    public int getCount() {
    	loadBitmaps();
    	return _Bitmaps.length;
    }
    
    private void loadBitmaps() {
    	// 이미 로딩되어 있으면 무시한다.
    	if (_Bitmaps != null) return;
    	
		_Bitmaps = new Bitmap[4];
		
		Resources resources = Resource.getInstance().getResources();  
		
        _Bitmaps[0] = BitmapFactory.decodeResource(resources, R.drawable.ship2_1);
        _Bitmaps[1] = BitmapFactory.decodeResource(resources, R.drawable.ship2_2);
        _Bitmaps[2] = BitmapFactory.decodeResource(resources, R.drawable.ship2_3);
        _Bitmaps[3] = BitmapFactory.decodeResource(resources, R.drawable.ship2_4);
    }
	
}
ResourceAsteroid 클래스와 동일한 소스이기 때문에 특별히 설명드릴 만한 곳은 없습니다.  41-44: 라인에서 우주선의 애니메이션을 위한 4장의 이미지를 로딩하고 있습니다.

18-19: 우주선 이미지의 가로와 세로 크기 입니다.  이미지에서 직접 얻어와도 되지만 상수로 처리했습니다.



중복 코드의 제거 - 템플릿 메소드

그런데, 여기서 한 가지 걸리는 것이 있습니다.  무엇일까요?  네!  코드가 중복되고 있습니다.  중복된 소스 코드는 언제나 심각한 문제를 유발 할 수 있기 때문에 중복을 제거 하는 것이 좋겠습니다.  여기서는 상속을 통해서 중복을 제거 하도록 하겠습니다.

중복이 되는 부분만 일단 살펴보면 21-31: 라인 부분이 중복되고 있습니다.  그리고, loadBitmaps() 메소드는 대부분 유사하지만 일부분이 변경되고 있습니다.  따라서 일단 [그림 1]과 같이 중복 코드를 제거하기위해 부모 클래스를 만들고 중복되는 메소드(코드)를 부모 클래스로 옮겨서 중복을 피하도록 하겠습니다.  부모 클래스의 이름은 BitmapList라고 하겠습니다.

[그림 1] Class Diagram - 중복 제거

그런데, 무엇인가 어색합니다.  무엇일까요?  getBitmap() 메소드와 getCount() 메소드가 loadBitmap()을 호출해야 하는데, 부모 클래스에는 해당 메소드가 없습니다.  이럴 때는, 부모 클래스에 추상 메소드만을 남겨두고, 실제 구현을 자식 클래스에서 진행해야 합니다.  이런 경우를 바로 템플릿 메소드 패턴 이라고 합니다.  (Template Method Pattern)

따라서, [그림 2]와 같이 소스를 변경하도록 하겠습니다.  그리고, 아직은 사용하지 않지만, Clear() 메소드를 추가해서 더 이상 이미지 데이터가 필요 없을 때, 메모리를 반환 할 수 있도록 하겠습니다.


[그림 2] Class Diagram - 템플릿 메소드 패턴

[그림 2]에서 #으로 표시된 것은 protected 라는 것을 뜻 합니다.  이태릭체로 기울여져 있는 메소드는 abstract 메소드 입니다.

BitmapList 클래스는 [소스 2]와 같이 작성합니다.

[소스 2] BitmapList.java
package app.resources;

import android.graphics.Bitmap;

public abstract class BitmapList {

	protected Bitmap[] _Bitmaps = null;

	public Bitmap getBitmap(int index) {
    	if (_Bitmaps == null) loadBitmaps();
    	return _Bitmaps[index];
    }
    
    public int getCount() {
    	if (_Bitmaps == null) loadBitmaps();
    	return _Bitmaps.length;
    }
    
    abstract protected void loadBitmaps();
    
}

이제 ResourceAsteroid 클래스와 ResourceShip 클래스를 [소스 3]과 [소스 4]와 같이 중복을 제거하고 다시 작성 합니다.

[소스 3] ResourceAsteroid .java
package app.resources;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import app.main.R;

public class ResourceAsteroid extends BitmapList {

	private static ResourceAsteroid _Instance = new ResourceAsteroid();

	public static ResourceAsteroid getInstance(){
		return _Instance;
	}
	
	private ResourceAsteroid() {}
	
    public static final int SIZE = 64;
	
    @Override
    protected void loadBitmaps() {
		_Bitmaps = new Bitmap[12];
		
		Resources resources = Resource.getInstance().getResources();  
		
        _Bitmaps[11] = BitmapFactory.decodeResource(resources, R.drawable.asteroid01);
        _Bitmaps[10] = BitmapFactory.decodeResource(resources, R.drawable.asteroid02);
        _Bitmaps[ 9] = BitmapFactory.decodeResource(resources, R.drawable.asteroid03);
        _Bitmaps[ 8] = BitmapFactory.decodeResource(resources, R.drawable.asteroid04);
        _Bitmaps[ 7] = BitmapFactory.decodeResource(resources, R.drawable.asteroid05);
        _Bitmaps[ 6] = BitmapFactory.decodeResource(resources, R.drawable.asteroid06);
        _Bitmaps[ 5] = BitmapFactory.decodeResource(resources, R.drawable.asteroid07);
        _Bitmaps[ 4] = BitmapFactory.decodeResource(resources, R.drawable.asteroid08);
        _Bitmaps[ 3] = BitmapFactory.decodeResource(resources, R.drawable.asteroid09);
        _Bitmaps[ 2] = BitmapFactory.decodeResource(resources, R.drawable.asteroid10);
        _Bitmaps[ 1] = BitmapFactory.decodeResource(resources, R.drawable.asteroid11);
        _Bitmaps[ 0] = BitmapFactory.decodeResource(resources, R.drawable.asteroid12);
    }
	  
}

[소스 4] ResourceShip .java
package app.resources;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import app.main.R;

public class ResourceShip extends BitmapList {

	private static ResourceShip _Instance = new ResourceShip();

	public static ResourceShip getInstance(){
		return _Instance;
	}
	
	private ResourceShip() {}
	
    public static final int WIDTH = 122;
    public static final int HEIGHT = 65;

    @Override
    protected void loadBitmaps() {
		_Bitmaps = new Bitmap[4];
		
		Resources resources = Resource.getInstance().getResources();  
		
        _Bitmaps[0] = BitmapFactory.decodeResource(resources, R.drawable.ship2_1);
        _Bitmaps[1] = BitmapFactory.decodeResource(resources, R.drawable.ship2_2);
        _Bitmaps[2] = BitmapFactory.decodeResource(resources, R.drawable.ship2_3);
        _Bitmaps[3] = BitmapFactory.decodeResource(resources, R.drawable.ship2_4);
    }
	
}



우주선 표시

이제 우주선을 표시 할 게임 컨트롤을 작성하도록 하겠습니다.  GameControl을 상속 받아서 [소스 5]와 같이 작성합니다.

[소스 5] Ship.java
package app.main;

import ryulib.game.GameControl;
import ryulib.game.GameControlGroup;
import ryulib.game.GamePlatformInfo;
import android.graphics.Canvas;
import android.graphics.Paint;
import app.resources.ResourceShip;

public class Ship extends GameControl {

	private static final int _ANIMATIONINTERVAL = 100;
	
	public Ship(GameControlGroup gameControlGroup) {
		super(gameControlGroup);
	
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private int _Y = 0;
	
    private int _BitmapIndex = 0;
    private long _TickCount = 0;
    
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onTick(GamePlatformInfo platformInfo) {
		long tick = platformInfo.getTick();
		
		_TickCount = _TickCount + tick;
		if (_TickCount >= _ANIMATIONINTERVAL) {
			_TickCount = 0;
			_BitmapIndex++;
			if (_BitmapIndex >= ResourceShip.getInstance().getCount()) {
				_BitmapIndex = 0;
			}
		}
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		_Canvas.drawBitmap(
				ResourceShip.getInstance().getBitmap(_BitmapIndex), 
				0, _Y, 
				_Paint
		);
	}
	
}
22: 우주선은 위 아래로만 움직 일 수 있도록 할 것 입니다.  따라서, Y축 좌표만 사용합니다.



중복 코드의 제거 - 위임

아악!!  [소스 5]의 37-44: 라인에서, 또 다시 중복 코드가 발견되었습니다.  소행성의 애니메이션 코드와 유사한 코드가 작성되어 있습니다.  그런데, 이미 GameControl에서 상속을 받은 상태이기 때문에, 다중 상속을 지원하지 않는 자바에서는 상속을 통해서 코드의 중복을 제거할 수가 없습니다.

따라서, 위임을 통해서 중복 코드를 제거하겠습니다.  새로운 클래스 AnimationCounter를 만들어서 애니메이션에 필요한 코드를 [소스 6]과 같이 위임하도록 하겠습니다.

[소스 6] AnimationCounter.java
package app.main;

public class AnimationCounter {

	public AnimationCounter() {
		super();
	}

	public AnimationCounter(int interval, int size) {
		super();
		
		_Interval = interval;
		_Size = size;
		
		Clear();
	}

	private long _TickCount = 0;
	
	// Property 
	private int _Interval = 0;
	private int _Size = 0;
	private int _Index =0;
	private boolean _AutoRewind = true;
	
	public void Clear() {
		_Index = 0;
		_TickCount = 0;
	}

	public void Tick(long tick) {
		_TickCount = _TickCount + tick;
		
		if (_TickCount >= _Interval) {
			_TickCount = _TickCount - _Interval;
			_Index++;

			if ((_AutoRewind) && (_Index >= _Size)) {
				_Index = 0;
			}
		}
	}
	
	public void setInterval(int value) {
		_Interval = value;
	}
	
	public int getInterval() {
		return _Interval;
	}
	
	public void setSize(int value) {
		_Size = value;
	}

	public int getSize() {
		return _Size;
	}

	public int getIndex() {
		return _Index;
	}

	public void setAutoRewind(boolean value) {
		_AutoRewind = value;
	}

	public boolean isAutoRewind() {
		return _AutoRewind;
	}

}
9: interval과 size를 함께 객체를 생성 할 수 있도록 생성자를 추가하였습니다.

21: _Interval은 얼마의 시간 간격으로 이미지를 바꾸면서 애니메이션 할 것 인가를 결정 합니다.  1/1000 초 단위 입니다.

22: _Size는 애니메이션을 위한 이미지의 개수 입니다.

23: _Index는 현재 표시해야 할 이미지의 순서 입니다.

24: _AutoRewind는 순환 반복해서 이미지를 표시 할 것인지 아니면, 처음부터 한 번 표시하고 말 것인지를 결정 합니다.

26: Clear() 메소드를 호출하면 처음 이미지부터 다시 계산하게 됩니다.

31: Tick() 메소드는 이전 호출과 현재 호출 간의 시간 간격을 입력해서, 몇 번째 이미지가 표시되어야 하는 지를 결정 합니다.  이미지는 순환 반복되면서 표시되도록 _Index를 계산 합니다.


이제, AnimationCounter 클래스를 사용하여, [소스 7]과 같이 Ship.java를 수정하도록 하겠습니다.

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

import ryulib.game.GameControl;
import ryulib.game.GameControlGroup;
import ryulib.game.GamePlatformInfo;
import android.graphics.Canvas;
import android.graphics.Paint;
import app.resources.ResourceShip;

public class Ship extends GameControl {

	private static final int _ANIMATIONINTERVAL = 100;
	
	public Ship(GameControlGroup gameControlGroup) {
		super(gameControlGroup);
	
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private int _Y = 0;
	
	private AnimationCounter _AnimationCounter = 
		new AnimationCounter(
				_ANIMATIONINTERVAL, 
				ResourceShip.getInstance().getCount()
		);
	
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onTick(GamePlatformInfo platformInfo) {
		long tick = platformInfo.getTick();
		
		_AnimationCounter.Tick(tick);
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		_Canvas.drawBitmap(
				ResourceShip.getInstance().getBitmap(_AnimationCounter.getIndex()), 
				0, _Y, 
				_Paint
		);
	}
	
}
24-28: AnimationCounter를 생성합니다.  이미지 교체 간격은 _ANIMATIONINTERVAL(100ms)이고, 이미지의 개수는 ResourceShip.getInstance().getCount()로 지정하였습니다.

40: 이전 프레임 표시와의 간격 tick을 AnimationCounter 객체에 전달 합니다.

46: _AnimationCounter.getIndex()는 현재 표시되어야 할 이미지의 인덱스를 리턴합니다.  어떻게 계산했는 지는 캡슐화 되어서 코드가 간결해 보입니다.  애니메이션의 정책이 변경되더라도, 더 이상 신경 쓸 필요가 없어졌습니다.



우주선 테스트

제대로 동작하는 지 확인하기 위해서 Main.java를 수정하여 실행하도록 하겠습니다.

[소스 8] Main.java 수정
package app.main;

import ryulib.game.GamePlatform;
import android.app.Activity;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import app.resources.Resource;

public class Main extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        Resource.getInstance().setContext(this);

        _GamePlatform = new GamePlatform(this);
        _GamePlatform.setLayoutParams(
        		new LinearLayout.LayoutParams(
        				ViewGroup.LayoutParams.FILL_PARENT, 
        				ViewGroup.LayoutParams.FILL_PARENT, 
        				0.0F
        		)
        );
        
        setContentView(_GamePlatform);
        
        _Background = new Background(null);
        _GamePlatform.AddControl(_Background);
        
        _Asteroids = new Asteroids(null);
        _GamePlatform.AddControl(_Asteroids);        

        _Ship = new Ship(null);
        _GamePlatform.AddControl(_Ship);
    }
    
    private GamePlatform _GamePlatform = null;
    private Background _Background = null;
    private Asteroids _Asteroids = null;
    private Ship _Ship = null;
    
}
35: 우주선을 표시할 Ship 객체를 생성하고, 
36: GamePlatform 객체에 등록 합니다.


실행 결과 화면은 [그림 3]과 같습니다.

[그림 3] 우주선 처리 결과 화면

이제 마지막으로 Asteroid 클래스의 애니메이션 처리도 AnimationCounter를 이용하여 사용 할 수 있도록 변경합니다.

[소스 9] Asteroid .java 수정
package app.main;

import ryulib.game.GameControl;
import ryulib.game.GamePlatformInfo;
import android.graphics.Canvas;
import android.graphics.Paint;
import app.resources.ResourceAsteroid;

public class Asteroid extends GameControl {

	private static final int _ANIMATIONINTERVAL = 100;
	private static final int _SPEED = 100;

	public Asteroid(int x, int y) {
		super(null);
	
		_X = x;
		_Y = y;
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private int _X = 0;
	private int _Y = 0;
	private Scroll _Scroll = new Scroll(_SPEED);
	
	private AnimationCounter _AnimationCounter = 
		new AnimationCounter(
				_ANIMATIONINTERVAL, 
				ResourceAsteroid.getInstance().getCount()
		);
	
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onTick(GamePlatformInfo platformInfo) {
		long tick = platformInfo.getTick();
		
		_AnimationCounter.Tick(tick);
		
		// 초당 _Scroll.getSpeed() 만큼의 속도로 운석을 이동 한다.
		_X = _X - _Scroll.Move(tick);
		
		// 화면에서 사라지면 삭제 한다.
		if (_X < (-ResourceAsteroid.SIZE)) this.Delete(); 
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		_Canvas.drawBitmap(
				ResourceAsteroid.getInstance().getBitmap(
						_AnimationCounter.getIndex()
				),
				_X,	_Y, 
				_Paint
		);
	}
	
}



정리

이번에는 우주선을 표시하는 과정 중에서 중복을 제거하는 두 가지 방법에 대해서 설명하였습니다.  하나는 상속을 통해서 중복을 제거하는 방법을 설명하였고, 다른 하나는 새로운 클래스에 기능을 위임하는 방식이었습니다.  특히, 상속의 경우에는 중복되는 부분이 중복되지 않는 부분을 의존하고 있어서, 템플릿 메소드 패턴을 이용하여 해결 하는 과정을 설명하였습니다.

다음에는 우주선을 이동하는 기능을 추가하도록 하겠습니다.

고관절 때문에 하루 종일 누워있다가, 베타 테스트 중인 도영군 ^^*
충돌 테스트를 적용하여 소행성을 피하느라 열중하고 있습니다.



소스



관련글 더보기