상세 컨텐츠

본문 제목

JetBoy 따라하기 #1 - 배경 이미지 처리

프로그래밍/Android

by ryujt 2010. 12. 24. 15:06

본문

이번에는 안드로이드 SDK에 포함된 예제인 JetBoy를 따라해 보도록 하겠습니다.  물론 게쪽을 사용해서 만들기 때문에 소스는 완전히 다른 스타일로 진행 됩니다.



기초 작업

우선 [그림 1]처럼 프로젝트의 메인 Activity의 Package Name은 app.main으로 정했습니다.  이 부분은 여러분들이 각자 마음에 드는 이름으로 지정하셔도 됩니다.  그리고, Main Activity Class의 이름은 Main, 배경을 담당 할 클래스의 이름은 BackGround로 지정하였습니다.  이후 RyuLib를 src 폴더에 복사해서 사용하고 있습니다.

"RyuLib.Game for Android 시작하기"에서 이미 설명한 것과 중복되지만, "Add External JARs" 등을 이용하여 외부 참조를 하는 것이 익숙하지 않은 분들에게서 질문이 올라오고 있어서, 좀 쉽게 가기로 했습니다.  모든 예제의 소스에는 해당 예제를 작성할 때의 RyuLib를 복사해서 배포 할 예정입니다.  RyuLib는 아직 안정화 단계가 아니기 때문에 서로 버전이 달라 질 수 있음을 유의하시기 바랍니다.

이미지와 관련해서는 JetBoy의 drawable 폴더에 있는 이미지를 그대로 복사 해 왔습니다.

[그림 1] 프로젝트 준비

이제 [소스 1]과 같이 AndroidManifest.xml 를 수정하여 화면을 가로로 변경하도록 하겠습니다.

[소스 1] AndroidManifest.xml
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="app.main"
      android:versionCode="1"
      android:versionName="1.0">
     <application android:icon="@drawable/icon" android:label="@string/app_name">
         <activity android:name=".Main"
                  android:label="@string/app_name"
                  android:screenOrientation="landscape">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
     </application>
 </manifest> 
9: android:screenOrientation="landscape" 를 추가해서 지금 작성하는 어플리케이션이 가로로 화면이 출력되어야 함을 알립니다.



일단 무조건 표시하고 이동 해보자

이후 Main.java와 BackGround.java의 소스를 [소스 2]와 [소스 3]과 같이 작성합니다.

[소스 2] 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;

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

        _GamePlatform = new GamePlatform(this);
        _GamePlatform.setLayoutParams(
        		new LinearLayout.LayoutParams(
        				ViewGroup.LayoutParams.FILL_PARENT, 
        				ViewGroup.LayoutParams.FILL_PARENT, 
        				0.0F
        		)
        );

        setContentView(_GamePlatform);
        
        _BackGround = new BackGround(this);
        _GamePlatform.AddControl(_BackGround);
    }
    
    private GamePlatform _GamePlatform = null;
    private BackGround _BackGround = null;
    
}
"RyuLib.Game for Android 시작하기"에서 충분히 설명한 부분이라서 설명을 생략합니다.


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

import ryulib.game.GameControl;
import ryulib.game.GamePlatformInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class BackGround extends GameControl {

	public BackGround(Context context) {
		super(null);
		
		Resources resources = context.getResources();
		
		_BackGoundBitmap = BitmapFactory.decodeResource(resources, R.drawable.background_a);
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private Bitmap _BackGoundBitmap = null;
	private double _X01 = 0;
	
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		long tick = platformInfo.getTick();
		
		// 현재 위치에 먼 거리 배경 이미지 그리기
		_Canvas.drawBitmap(_BackGoundBitmap, (int) _X01, 0, _Paint);
		
		_X01 = _X01 - (25 * tick / 1000);
	}
	
}
17: Resources는 이미지나 오디오 등의 자원들을 코드에서 사용해 줄 수 있게 합니다.  우리는 지금 이미지가 필요한 시기이므로 간절히 필요합니다.

19: BitmapFactory를 이용해서 Resources에서 이미지를 불러옵니다.  drawable 폴더에서 background_a.png 파일을 가져옵니다.

26: 배경 이미지를 시간이 갈 수록 왼쪽으로 스크롤 하기 위해서, 이미지가 표시될 왼쪽 좌표를 저장 할 변수를 _X01로 지정하였습니다. 좌표의 이름 뒤에 "01"을 붙인 이유는 배경 이미지 표시가 두 번 필요하기 때문입니다. 이는 뒤에서 설명하도록 하겠습니다.

39: 현재 좌표 (_X01, 0)에 비트맵을 그려줍니다. 배경의 Y축 좌표는 변하지 않기 때문에 0으로 지정하였습니다.

41: 시간에 따라 X축 좌표를 왼쪽으로 이동하기 위해 시간에 맞춰서 빼기 연산을 합니다. 얼마만큼 이동하느냐는 1초 단위마다 25 픽셀로 정하였습니다. tick은 이미 설명한 것과 같이 1/1000(천분의 1) 단위를 가지고 있기 때문에 1000으로 나눠서 계산하였습니다.


[그림 2]는 실행 화면입니다.  배경 이미지의 크기 이상으로 스크롤이 진행되자, 화면 오른쪽에 이미지의 잔상만을 남기면서 이상한 모양새가 되어 버렸습니다.

[그림 2] 배경 이미지 크기 이상으로 스크롤 된 화면

이미지를 무한히 크게 하지 않는 한 이런 현상을 피할 수는 없습니다.  따라서, 대개의 경우 배경 이미지를 반복적으로 사용하게 됩니다.  아예 배경 이미지를 실시간으로 랜더링하는 방법도 있긴 합니다.  하지만, 실시간 랜더링은 다소 복잡하니 나중으로 미루도록 하겠습니다.



배경 이미지 순환 반복

여기서는 이미지를 두 장 사용하여, 한 장이 화면에서 지나가면 바로 오른쪽에 이어서 표시하는 방식을 거듭해서 무한히 반복되도록 하겠습니다.  [그림 3]을 보시면 "배경 이미지 1"이 스크롤되면서 생긴 화면의 공백을 "배경 이미지 2"가 이어서 표시되도록 하는 것이 보입니다.  이후 "배경 이미지 1"이 화면영역에서 완전히 벗어나면, 이것을 다시 오른쪽에 이어 붙여서 재 활용 합니다.

[그림 3] 배경 이미지 두 장으로 연속적인 스크롤 구현


[소스 4] BackGround.java의 1차 수정
package app.main;

import ryulib.game.GameControl;
import ryulib.game.GamePlatformInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class BackGround extends GameControl {

	private static final int BACKROUND_SPEED = 25;

	public BackGround(Context context) {
		super(null);
		
		Resources resources = context.getResources();
		
		_BackGoundBitmap = BitmapFactory.decodeResource(resources, R.drawable.background_a);
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private Bitmap _BackGoundBitmap = null;
	private double _X01 = 0;
	private double _X02 = 0;
	
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		long tick = platformInfo.getTick();
		
		// 현재 위치에 먼 거리 배경 이미지 그리기
		if ((_BackGoundBitmap.getWidth()+_X01) > 0) {
			_Canvas.drawBitmap(_BackGoundBitmap, (int) _X01, 0, _Paint);
		}
		
		if ((_BackGoundBitmap.getWidth()+_X01) <= _Canvas.getWidth()) {
			_Canvas.drawBitmap(_BackGoundBitmap, (int) _X02, 0, _Paint);
		}
		
		_X01 = _X01 - (BACKROUND_SPEED * tick / 1000);
		_X02 = _X01 + _BackGoundBitmap.getWidth();
		
		if ((_BackGoundBitmap.getWidth()+_X02) <= _Canvas.getWidth()) {
			double temp = _X01;
			_X01 = _X02;
			_X02 = temp + _BackGoundBitmap.getWidth();
		}
	}
	
}
14: [소스 3]의 41: 라인에 있는 25라는 숫자는 어떤 의미로 쓰이는 지 알 수가 없기 때문에 이런 경우에는 상수로 정의해서 쓰는 것이 정석입니다.  따라서, 배경 이미지의 스크롤 속도에 맞게 상수 이름을  BACKROUND_SPEED로 정하였습니다.

29: 배경 이미지 좌표 정보가 두 개 필요하기 때문에 추가된 코드 입니다.

42: 첫 번째 이미지가 좌측 경계선을 넘어서면, 이미지를 표시 할 필요가 없습니다.

46: 두 번째 이미지는 첫 번째 이미지가 화면 전체를 덮지 못 할 경우에 표시 합니다.  첫 번째 이미지의 크기에 좌표를 더하면 현재 화면에 표시되고 있는 이미지의 넓이가 나옵니다.  여기서 좌표를 더하는 이유는 좌표가 음수 영역이기 때문입니다.

50-51: 우선 첫 번째 이미지 좌표 _X01을 이동 시키고, _X02는 _X01에 이미지의 넓이를 더해 줍니다.

53-57: 두 번째 이미지 마저 화면을 꽉 채우지 못하게 되면, 이미 사라져 버린 _X01과 자리를 바꿔서 이미지 표시를 순환 반복 합니다.


[그림 4]를 보시면 이미지 스크롤이 이미지 크기보다 더 진행되어 이미지의 앞 부분이 화면 오른쪽에 다시 반복되서 출력되는 것을 확인하실 수 있습니다.  이제 배경 이미지는 빈틈없이 순환 반복되어 출력 됩니다.

[그림 4] 배경 이미지의 순환 반복

배경 이미지 표시를 이대로 해도 큰 문제가 되지는 않지만, 좀 더 세련된 표현을 해보도록 하겠습니다.  게임에서 자주 활용되는 기법인데, 배경 이미지를 여러 개 사용하고, 모든 배경 이미지의 속도를 다르게 함으로써 입체적인 표시를 하는 것 입니다.  느린 속도로 스크롤하는 배경은 멀리 있어 보이고, 빠를 수록 가까운 거리로 느껴지게 됩니다.

자 그럼 어떻게 하면 됩니까?  이미 배경 이미지 표시는 해봤으니까, 속도만 변경해서 Bitmap과 좌표를 저장 할 변수를 선언하여 코딩하면 쉽게 해결 될 것 입니다.  그리고, 이제 바로 코딩에 들어 가면 될 까요?

No!!  틀렸지만, 참 잘 했어요 ^^



배경 이미지 처리를 캡슐화하여 코드를 간결하게

그런건 아마추어들이나 하는 짓이고, 우리는 프로답게 "프로그래밍 7거지악" 중에 하나인 중복 코드도 피해가고, 배경 이미지의 위치를 계산하는 등의 전체 흐름과 무관한 코드를 캡슐화 해보록 하겠습니다.  우선 새로운 클래스인 BackGroundImage.java를 생성하였고, 그로 인해서 BackGround.java는 [소스 5]와 같이 간결해 졌습니다.

[소스 5] BackGround.java의 2차 수정
package app.main;

import ryulib.game.GameControl;
import ryulib.game.GamePlatformInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class BackGround extends GameControl {

	private static final int BACKROUND_SPEED_SLOW = 25;
	private static final int BACKROUND_SPEED_FAST = 50;

	public BackGround(Context context) {
		super(null);
		
		Resources resources = context.getResources();
		
		_BackGround_A.setBitmap(
				BitmapFactory.decodeResource(resources, R.drawable.background_a)
		);
		_BackGround_A.setSpeed(BACKROUND_SPEED_SLOW);

		_BackGround_B.setBitmap(
				BitmapFactory.decodeResource(resources, R.drawable.background_b)
		);
		_BackGround_B.setSpeed(BACKROUND_SPEED_FAST);
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private BackGroundImage _BackGround_A = new BackGroundImage();
	private BackGroundImage _BackGround_B = new BackGroundImage();
	
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		long tick = platformInfo.getTick();
		
		_BackGround_A.Draw(_Canvas, _Paint, tick);
		_BackGround_B.Draw(_Canvas, _Paint, tick);
	}
	
}
전반적으로 수정되어 수정된 부분을 표시하지 않았습니다.

14: 새로운 배경 이미지는 속도를 두 배 빠르게 하여 가까운 배경으로 지정할 예정입니다. 

35-36: 배경 이미지를 처리해 주는 BackGroundImage의 객체를 두 개 생성합니다.

21, 26: Resources에서 각각 background_a와 background_b를 불러 들여 BackGroundImage.setBitmap()으로 지정 합니다.

24, 29: 각각 BackGroundImage의 객체의 속도를 지정해 줍니다.  하나는 느리고, 하나는 빠르게 설정하였습니다.

48-49: 배경 이미지의 그리기와 스크롤 처리를 BackGroundImage 클래스에게 위임하여 캡슐화 하였기 때문에, onDraw() 메소드 구현이 상당히 간결해 졌습니다.


[소스 6]는 BackGroundImage의 내부 구현 입니다.

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

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;

public class BackGroundImage {

	private double _X01 = 0;
	private double _X02 = 0;

	// Property 
	private Bitmap _Bitmap = null;
	private int _Speed = 0;
	
	public void Draw(Canvas canvas, Paint paint, long tick) {
		// 현재 위치에 먼 거리 배경 이미지 그리기
		if ((_Bitmap.getWidth()+_X01) > 0) {
			canvas.drawBitmap(_Bitmap, (int) _X01, 0, paint);
		}
		
		if ((_Bitmap.getWidth()+_X01) <= canvas.getWidth()) {
			canvas.drawBitmap(_Bitmap, (int) _X02, 0, paint);
		}
		
		_X01 = _X01 - (_Speed * tick / 1000);
		_X02 = _X01 + _Bitmap.getWidth();
		
		if ((_Bitmap.getWidth()+_X02) <= canvas.getWidth()) {
			double temp = _X01;
			_X01 = _X02;
			_X02 = temp + _Bitmap.getWidth();
		}
	}
	
	public void setBitmap(Bitmap value) {
		_Bitmap = value;
	}
	public Bitmap getBitmap() {
		return _Bitmap;
	}

	public void setSpeed(int value) {
		_Speed = value;
	}

	public int getSpeed() {
		return _Speed;
	}

}
대분의 코드는 BackGround.java에서 구현했던 것과 동일 합니다.  특별히 다시 설명해야 할 만한 곳이 보이지 않기 때문에 설명을 생략하도록 하겠습니다.

이제와서 느낀건데 왜 BackGround가 Background보다 자연스럽게 느껴졌던 걸까요?  아놔!!


[그림 5] 완성된 결과



정리
  • 배경 이미지를 표시하고 스크롤 하기 (배경 이미지 순환 반복)
  • 배경 이미지를 여러 개를 준비하고, 속도를 달리하여 입체감 살리기



소스


관련글 더보기