안드로이드 레이아웃 xml 내용 중에 px를 모두 찾아서 dp로 변환해 주는 유틸리티 입니다.  지금 진행 중인 프로젝트에 사용하려고 급하게 만들었습니다.  소스 공개는 제가 생각한 기능과 UI가 갖춰지면 진행하겠습니다 ^^;


(프로젝트 폴더만 지정하면 스스로 알아서 density를 판단해서 처리하려고 했는데 흐음 ㅡ.ㅡa)


픽셀 단위로 계산해서 레이아웃을 작성 한 다음에 한꺼번에 dp로 변환 할 때 편합니다.


사용법은 

  • 프로그램을 실행
  • 텍스트 에디터 영역에 xml 내용을 복사
  • density 선택
  • Convert 버턴을 클릭

[그림 1] 실행 화면





Posted by 류종택

Android 4.0 이상에서는 Main thread에서 통신 관련 작업을 할 수가 없다고 합니다.

(한 동안 손 놓고 있다가, 근래 다시 안드로이드 프로젝트 중인데, 이것 때문에 엄청 고생을 ㅠ.ㅠ)


AsyncTask 와 친해지면 모든 것이 해결 된다.


new AsyncTask() {
    @Override
    protected String doInBackground(Void... params) {
        httpGetRequest("http://www.himytv.com/");
        return null;
    }
}.execute(null, null, null);




Posted by 류종택
이번에는 안드로이드 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] 완성된 결과



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



소스



Posted by 류종택
RyuLib.Game for Android의 프로젝트 명은 "게임엔진이라고 하기에는 심하게 쪽팔리지만"이며, 줄여서 "게쪽"입니다.  현재는 ver 0.1 beta 상황입니다.  게쪽은 수준 높은 게임을 목표로 하기보다, 입문자들이 비교적 쉽게 게임 실무에 익숙해 질 수 있도록 하는 것이 목표입니다.

[그림 1] Class Diagram

[그림 1]은 게쪽의 가장 중요한 클래스의 Class Diagram 입니다.  쉽게 설명하기 위해서 상당히 단순화 하였습니다.  디자인 패턴에 익숙하신 분들은 오른쪽이 컴포지트(Composite) 패턴으로 이루어 져 있다는 것을 아실 수 있을 것 입니다.

GamePlatform 클래스는 SurfaceView를 상속받아 확장하였습니다.  화면 그리기에 대한 기능을 제공하며, 게임 컨트롤을 관리합니다.

GameControlBase 클래스는 게임 컨트롤의 추상 클래스입니다.  게임 컨트롤이란, 게임에 사용되는 비행기, 미사일 등의 객체를 뜻 합니다.  애니메이션 효과 및 충돌 테스트 등의 기능이 내장되어 있습니다.

그런데, GameControlBase는 GameControlGroup과 GameControl로 나눠지고 있습니다.  컴포지트 패턴이 무엇인지 모르시는 분들은 GameControlGroup을 폴더라고 생각하시고, GameControl을 파일이라고 생각하시면 됩니다.  

즉, GameControlGroup은 GameControlGroup과 GameControl을 포함 할 수 있다는 뜻 입니다.  예를 들어 게임 케릭터가, [그림 2]와 같이, 하나의 객체가 아닌 여러 개의 객체로 구성되어 있다고 합시다.  케릭터와 아이템 주머니는 다른 컨트롤을 포함해야 하므로 GameControlGroup을 상속 받아서 확장하고, 칼, 방패, 아이템1, 아이템2는 GameControl을 상속 받아서 확장하면 됩니다.

이렇게 GameControlGroup과 GameControl로 나눠서 작업하는 이유는, 관리해야 할 객체가 많아질 때, 분류를 통해서  보다 효율적으로 처리하기 위해서 입니다.  MP3 파일 수 천 개를 하나의 폴더에 몽땅 저장해 놓은 후, 원하는 가수의 노래만 찾아야 하는 경우를 생각해보시면, 왜 컴포지트 패턴을 사용했는 지 이해하기 쉬울 것 입니다.

당분간은 GameControlGroup을 사용하지 않고 가벼운 예제를 중심으로 설명을 이어 갈 것 입니다.

[그림 2] 케릭터의 객체 구성



RyuLib 설치

우선 소스는 아래의 사이트에서 다운받으시면 됩니다.  SVN을 사용합니다.

만약 SVN 사용이 익숙하지 않으신 분들께서는 아래의 링크에서 첨부파일을 받아서 사용하시면 됩니다.

외부 라이브러리로 연결해서 사용하는 것이 어려우신 분들은 당분간 프로그램을 작성하실 때, 소ryulib 폴더 전체를 프로그램의 src 폴더 밑에 복사해서 사용하시면 됩니다.  저도 강좌를 진행하는 동안에는 ryulib 폴더를 src 폴더에 복사해서 사용하도록 하겠습니다.




간단한 도형 출력의 예

이해를 돕기 위해서 간단한 도형을 출력하는 예제를 작성해보도록 하겠습니다.  

새로운 프로젝트를 시작하시고, [소스1]과 [소스 2]와 같이 코딩을 마친 후, 실행하면 [그림 3]과 같이 빨간 박스가 화면 좌측 상단에 나타나게 됩니다.  

[그림 3] 프로그램 실행 결과

이때, CPU 사용률을 보면, 별것 아닌 프로그램이 상당히 많은 CPU를 사용하고 있는 것을 발견하게 됩니다.  화면에는 가만히 있는 것처럼 보이지만, 반복해서 빨간 박스를 그리고 있는 것이기 때문입니다.  이처럼, 게쪽은 Direct-X 프로그래밍과 같이, 전체 화면을 계속 갱신하도록 되어 있습니다.  CPU를 효율적으로 사용하는 방법에 대해서는 추후 다루도록 하겠습니다.

[소스 1] 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);
        
        _GamePlatform.AddControl(_Box);
    }
    
    private GamePlatform _GamePlatform = null;
    private Box _Box = new Box(null);
    
} 
15: 게쪽의 화면을 담당하는 GamePlatform 생성 합니다.

16-22: GamePlatform을 화면에 꽉차도록 레이아웃 설정을 합니다.

24: setContentView() 메소드를 이용하여 생성된 GamePlatform이 화면에 표시 되도록 합니다.

26: 게임 컨트롤인 Box를 GamePlatform에 추가합니다. GamePlatform은 추가된 모든 컨트롤을 차례대로 반복하면서 화면에 그려줍니다.

[소스 2] Box.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 android.graphics.Rect;

public class Box extends GameControl {

	public Box(GameControlGroup gameControlGroup) {
		super(gameControlGroup);
	}
	
	private Rect _rect = new Rect(0, 0, 32, 32);

	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		Paint paint = platformInfo.getPaint();
		Canvas canvas = platformInfo.getCanvas();
		
		paint.setARGB(255, 255, 0, 0);
		canvas.drawRect(_rect, paint);
	}
}
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: 라인에서 지정되어 있습니다.



애니메이션 효과

이번에는 애니메이션 효과를 주는 방법을 알아보겠습니다.  우선 Main.java는 [소스 1]과 유사 합니다.

[소스 3] 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);
        
        _Asteroid = new Asteroid(this, 100, 0);
        _GamePlatform.AddControl(_Asteroid);
    }
    
    private GamePlatform _GamePlatform = null;
    private Asteroid _Asteroid = null; 
    
}
달라진 부분은 31: 라인에서 Box 대신 Asteroid 클래스로 변경되었습니다.  그리고, 26: 라인에서 객체를 생성하면서 Context와 처음 표시 될 좌표 (x, y)를 넘기고 있습니다.  [소스 3]에서는 (100, 0) 좌표에 표시되도록 하고 있습니다.


[소스 4]
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 Asteroid extends GameControl {

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

	private static final int _ANIMATIONINTERVAL = 100;
	private static final int _SPEED = 100;
	
	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	private int _X = 0;
	private int _Y = 0;
	private double _DY = 0;
	
    private Bitmap[] _Bitmaps = new Bitmap[12];
    private int _BitmapIndex = 0;
    private long _TickCount = 0;
    
    private void init(Context context) {
        Resources resources = context.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);
    }
    
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		_Canvas.drawBitmap(_Bitmaps[_BitmapIndex], _X, _Y, _Paint);
		
		long tick = platformInfo.getTick();
		
		_TickCount = _TickCount + tick;
		if (_TickCount >= _ANIMATIONINTERVAL) {
			_TickCount = 0;
			_BitmapIndex++;
			if (_BitmapIndex >= _Bitmaps.length) {
				_BitmapIndex = 0;
			}
		}
		
		// 초당 _SPEED 만큼의 속도로 운석을 이동 한다.
		_DY = _DY + (_SPEED * tick / 1000);
		if (_DY >= 1) {
			_Y = _Y + ((int) _DY);
			_DY = _DY - ((int) _DY);
			
			if (_Y > _Canvas.getHeight()) _Y = 0;
		}
	}
	
}

갑자기 소스가 복잡해져 보입니다.  그러나 사실 별로 어려울 만한 코드는 없으니 천천히 살펴보도록 하겠습니다.  우선 Asteroid 클래스는 크게 아래와 같이 구성되어 있습니다.
  • public Asteroid(Context context, int x, int y) 
    • 생성자 
  • private void init(Context context) 
    • 초기화 메소드
  • protected void onStart(GamePlatformInfo platformInfo) 
    • 게임 컨트롤이 처음 가동되기 시작 할 때 한 번 실행 됩니다.
  • protected void onDraw(GamePlatformInfo platformInfo)
    • GamePlatform을 통해서 주기적으로 반복 호출 됩니다.
    • 게임 컨트롤을 그리는 코드를 구현하면 됩니다.
14-21: 생성자를 구현하고 있습니다.  Context 객체에 대한 레퍼런스와 처음 표시되어야 할 좌표를 지정하기 위하여 파라메터 선언 부분이 변경되었습니다.

37-52: 리소스에 있는 소행성 이미지를 배열로 정의하는 곳 입니다.  이미지는 Android SDK에 포함된 예제인 JeyBoy에서 복사해서 사용하고 있습니다.

55-58: 게임 컨트롤이 처음 가동 될 때, Canvas와 Paint 객체의 레퍼런스를 미리 저장해 두고 있습니다.  매번 레퍼런스를 가져오는 것보다 조금은 성능 향상을 기대 할 수 있습니다.  [소스 2]와 같이 매번 가져오는 방식을 취해도 문제 없습니다.

64: platformInfo.getTick()은 onTick(), onDraw() 등이 반복 될 때, 이전 반복과 현재 사이의 시간을 ms(밀리 세컨드) 단위로 알려주게 됩니다.  이처럼, platformInfo은 GamePlatform을 사용하는 데 필요한 여러 가지 정보가 담겨져 있습니다.

66-69: _TickCount에 계속 tick을 더해서 _ANIMATIONINTERVAL과 같아지거나 커지면 _BitmapIndex를 하나씩 증가 시키고 있습니다.  여기서 _ANIMATIONINTERVAL은 100이기 때문에 100ms 즉, 최대 1초에 10번씩 _BitmapIndex가 증가하게 됩니다.  그리고, 70-71: 라인에서 그 값이 _Bitmaps.length 보다 같거나 커지면 다시 0으로 순환 반복하도록 되어 있습니다.  이를 통해서 소행성 이미지를 100ms 마다 차례로 표시하면서 애니메이션 효과를 주게 됩니다.

73-80: 소행성을 회전시키면서 아래쪽으로 떨어트리는 효과를 주기 위해서 _Y 좌표를 _SPEED 만큼씩 전진 시키고 있습니다.  여기서 _Y를 직접 계산하지 않고 _DY라는 보조 변수를 통해서 실수 연산을 하다가 1보다 커졌을 경우만 적용시키고 있습니다.  이것은 반복하는 간격이 너무 짧아서 움직이는 값이 1 픽섹보다 작은 소수점 미만의 숫자가 되서 결국 0으로 취급되어 제 자리에 멈추는 것을 방지하기 위함 입니다.  아래와 같이 변경해서 실행 해 보시면 이해가 쉬울 것 입니다.
_Y = _Y + ((int) (_SPEED * tick / 1000));
81: 소행성이 화면을 벗어나면 다시 위에서 부터 떨어지도록 _Y 좌표 값을 0으로 지정하고 있습니다.


[그림 4]는 실행 결과 화면 입니다.  한 가지 문제가 있네요.  소행성이 움직이는 과정이 지워지지 않아서 흔적이 남고 있습니다.


[그림 4] 프로그램 실행 결과

 이제 흔적을 지우기 위해서 [소스 6]와 같이 BackGround 클래스를 추가하도록 하겠습니다.  [소스 5]의 Main.java 는 아래와 같이 수정해야 합니다.


[소스 5] 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(null);
        _Asteroid = new Asteroid(this, 100, 0);
        
        _GamePlatform.AddControl(_BackGround);
        _GamePlatform.AddControl(_Asteroid);
    }
    
    private GamePlatform _GamePlatform = null;
    private BackGround _BackGround = null;
    private Asteroid _Asteroid = null; 
    
}
수정된 부분은 다른 색으로 표시되어 있습니다.  여기서 중요한 것은 _BackGround가 _Asteroid 먼저 _GamePlatform에 Add 되어야 한다는 것 입니다.  그래야 먼저 바탕이 그려지게 됩니다.  순서를 뒤바꾸는 방법도 있지만, 그것은 나중에 설명하도록 하겠습니다.


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

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

public class BackGround extends GameControl {

	public BackGround(GameControlGroup gameControlGroup) {
		super(gameControlGroup);
	}

	private Canvas _Canvas = null;
	private Paint _Paint = null;
	
	@Override
	protected void onStart(GamePlatformInfo platformInfo) {
		_Canvas = platformInfo.getCanvas();
		_Paint = platformInfo.getPaint();
	}
	
	@Override
	protected void onDraw(GamePlatformInfo platformInfo) {
		_Paint.setARGB(255, 0, 0, 0);
		_Canvas.drawRect(0, 0, _Canvas.getWidth(), _Canvas.getHeight(), _Paint);
	}
	
}

27: BackGound는 현재 화면을 26: 라인에서 지정한 검은 색으로 전체 화면을 덮어서 그려주고 있습니다.  따라서 방금 전에 그려졌던 소행성의 이미지가 사라지게 됩니다.  이후 소행성의 위치가 변경된 것이 BackGround 위에 표시돕니다.  BackGround에 배경 이미지를 스크롤 하는 방법은 나중에 다뤄보도록 하겠습니다.

[그림 5]는 변경된 내용이 적용된 프로그램의 실행 장면 입니다.

[그림 5] BackGround 적용 후 



KeyEvent 처리

스마트 폰 게임에서 방향키를 이용해서 케릭터를 움직이는 경우는 드물겠지만, 키보드의 방향키를 이용해서 떨어지는 소행성을 조종해보도록 하겠습니다.

[소스 7]은 [소스 6]의 Main.java를 수정한 내용입니다.  수정한 내용은 단 한 줄 입니다.


[소스 7] Main.java 2차 수정
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
        		)
        );
        _GamePlatform.setUseKeyEvent(true);
        
        setContentView(_GamePlatform);
        
        _BackGround = new BackGround(null);
        _Asteroid = new Asteroid(this, 100, 0);
        
        _GamePlatform.AddControl(_BackGround);
        _GamePlatform.AddControl(_Asteroid);
    }
    
    private GamePlatform _GamePlatform = null;
    private BackGround _BackGround = null;
    private Asteroid _Asteroid = null; 
    
}
23: 키보드 이벤트를 사용하려면 _GamePlatform.setUseKeyEvent(true)를 통해서 이벤트를 사용하겠다고 알려야 합니다.  기본으로는 _GamePlatform.setUseKeyEvent(false)로 되어 있어서 키보드 이벤트를 무시하도록 되어 있습니다.

이후 Asteroid.java에는 다음과 같이 onKeyDown() 메소드를 추가로 구현해 주면 키보드를 통한 수평 이동이 완성됩니다.

	@Override
	protected boolean onKeyDown(GamePlatformInfo platformInfo, int keyCode, KeyEvent msg) {
		switch (keyCode) {
			case KeyEvent.KEYCODE_DPAD_LEFT: _X--; break;
			case KeyEvent.KEYCODE_DPAD_RIGHT: _X++; break;
		}
		
		return true;
    }
3-6: 키보드 이벤트의 keyCode에 따라서 _X 좌표를 왼쪽 또는 오른쪽으로 이동하고 있습니다.  

실제 게임에서는 좀 더 부드러운 방법을 사용해야 합니다.  
키보드는 눌러지는 당시와 계속 누르고 있을 때의 반응 속도가 다르기 때문입니다.

8: true를 리턴하게 되면, 키보드 이벤트 처리가 완료되었음을 GamePlatform에게 알리게 됩니다.  따라서, 뒤에 실행되는 게임 컨트롤의 해당 이벤트 처리를 무시하게 됩니다.



MotionEvent 처리

이번에는 MotionEvent 처리에 대한 예제를 살펴보도록 하겠습니다.  사용자가 소행성을 터치하면 소행성을 사라지도록 하겠습니다.

이번에도 Main.java를 [소스 8]과 같이 한 줄 수정 합니다.


[소스 8] Main.java 3차 수정
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
        		)
        );
        _GamePlatform.setUseKeyEvent(true);
        _GamePlatform.setUseMotionEvent(true);
        
        setContentView(_GamePlatform);
        
        _BackGround = new BackGround(null);
        _Asteroid = new Asteroid(this, 100, 0);
        
        _GamePlatform.AddControl(_BackGround);
        _GamePlatform.AddControl(_Asteroid);
    }
    
    private GamePlatform _GamePlatform = null;
    private BackGround _BackGround = null;
    private Asteroid _Asteroid = null; 
    
}
24: 키보드 이벤트와 마찬가지로 기본으로는 MotionEvent 처리가 꺼져 있습니다.  setUseMotionEvent(true)을 통해서 사용하겠다고 GamePlatform에게 알려줘야 합니다.
Asteroid.java에는 다음과 같이 onTouchEvent() 메소드를 추가 합니다.
	@Override
    protected boolean onTouchEvent(GamePlatformInfo platformInfo, MotionEvent event) {
		int x = (int) event.getX();
		int y = (int) event.getY();
		
		boolean isHori = (x >= _X) && (x <= (_X+_AsreroidSize));
		boolean isVert = (y >= _Y) && (y <= (_Y+_AsreroidSize));		
		if (isHori && isVert) this.Delete();
		
		return true;
    }
3-4: 터치가 일어난 (x, y) 좌표를 구해오고 있습니다.

6-8: (x, y) 좌표가 소행성의 (_X, _Y) 좌표에서 시작되는 소행성 크기만큼의 사각형 영역 안에 있으면 this.Delete()를 통해서 소행성을 삭제하고 있습니다.



정리

지금까지 게쪽의 기본적인 사용법을 살펴보았습니다.  이후 충돌 테스트 등의 다양한 내용은 실제 게임을 작성해 가면서 알아보도록 하겠습니다.



소스

Add External JARs를 통해서 RyuLib 설치를 못하셔서 질문하시는 분들이 계셔서 소스에 RyuLib를 중복해서 포함 시키고 있습니다.  간단한 몇 개를 만들어 보면서 게임 쪽 라이브러리를 테스트 해왔지만, 앞으로도 계속 변경 될 수 있으니 이점 유의하시기 바랍니다.  어느 정도 안정화가 되면 SVN을 통해서 소스를 공식 배포하도록 하겠습니다.



Posted by 류종택


티스토리 툴바