Saturday, March 1, 2014

[ AndEngine Tutorial : Essential 31 ] 누르면 반응하는 버튼 만들기

이 강좌는 [AndEngine for Android Game Development Cookbook by Jayme Schroeder, Brian Broyles (2013)]을 토대로 작성되었습니다. 소스코드는 책의 예제를 그대로 사용하였으며, 해설 또한 책에 근거하여 작성하였지만, 간략함과 이해를 위해 많은 부분이 제거 되거나 작성자의 의견이 첨가 되었습니다.


Mac OS X Maverick
Java 1.6+
Eclipse Kelper
Genymotion
AndEngine GLES 2 (Anchor Center branch)




ButtonSprite & ITiledTextureRegion
누르면 반응하는 버튼 만들기

메뉴를 구성하기 위해서 가장 흔한 방법 중 하나가 버튼을 사용하는 것이죠. 이 버튼이란 게 사용자의 터치에 적절하게 반응을 해야 사용자는 '아 눌러졌구나'하며 다음 상황을 기대할 것입니다. 그래서, 여기서는 ButtonSprite 클래스와  ITiledTextureRegion 이라는 인터페이스 타입(실제 사용되는 객체는 TiledTextureRegion 클래스 타입)을 이용해서 그와 같은 버튼을 생성해 보려 합니다.

우선 눌러서 반응하게 되는 버튼을 위해 아래와 같은 이미지가 필요합니다. 하나의 이미지 파일에 버튼이 보통(Normal) 상태와 눌러졌을 때(Pressed)를 표현하는 이미지들을 함께 집어 넣었습니다. (예제를 함께 따라해 보실 거면, 이 파일을 다운로드 하시면 됩니다.)


ButtonSprite 클래스의 경우 버튼의 상태(ButtonSprite.State.NORMAL, ButtonSprite.State.PRESSED)에 따라 타일화된(tiled) 텍스쳐 이미지를 처리하기 쉽게 만들어졌고, ITiledTextureRegion 인터페이스는  타일화하여 텍스쳐를 처리하기 위해 만들어진  타입이니 둘이 딱 어울리는 조합이죠?

지금부터 만들 예제는 아주 간단한 기능을 하는 앱입니다. 버튼을 누르면 위 그림의 두번째 그림으로 바뀌면서 버튼이 눌러진 효과를 보여주고, 간단한 Toast 메시지가 화면에 출력되죠. ButtonSprite, ITiledTextureRegion은 물론이고 사용자의 이벤트를 처리하는 간단한 형식도 볼 수 있으니 이 부분에 집중해서 보도록 하세요.

그럼 이제 예제 어플리케이션을 만들어 볼까요?


1. 인스턴스 변수에 ITiledTextureRegion 타입을 추가해 주고(Line 4)
   private Scene mScene;
   private Camera mCamera;
   
   private ITiledTextureRegion mButtonTextureRegion;



2. onCreateResources() 메소드 내에서 로드할 이미지 크기에 맞게 BuildableBitmapTextureAtlas 객체도 생성해 줍니다(Line 4-5). 그 다음엔 texture region과 texture atlas를 빌드하고 로드해 줍니다(Line 6-13). 나중에 ButtonSprite 객체를 생성할 때 쓸 수 있도록요.
   @Override
   public void onCreateResources( OnCreateResourcesCallback pOnCreateResourcesCallback) throws IOException {
      
      BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
      BuildableBitmapTextureAtlas bitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 300, 50);
      mButtonTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(bitmapTextureAtlas, getAssets(), "button_tiles.png", 2, 1);
      try{
         bitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder(0,0,0));
      }catch(TextureAtlasBuilderException ex){
         ex.printStackTrace();
      }
      
      bitmapTextureAtlas.load();
      
      pOnCreateResourcesCallback.onCreateResourcesFinished();
   }

ButtonSprite는 자신의 상태(ButtonSprite클래스의 소스코드를 보면 내부에 State이라는 enum이 선언되어 있는데요. 그 중 두개가 ButtonSprite.State.BUTTON_NORMAL, ButtonSprite.State.BUTTON_PRESSED 입니다)를 표시하기 위해 두개의 타일로 구성된 ITiledTextureRegion 객체를 필요로 합니다. 첫 번째 타일의 인덱스가 0, 두 번째 타일의 인덱스가 1로 지정되고 각각 BUTTON_NORMAL, BUTTON_PRESSED와 연결이되요. 그래서, 이 메소드 내에서 미리 ITiledTextureRegion객체를, 미리 준비한 이미지를 이용해서 생성하는 것입니다. 


3. 이제 onCreateScene() 메소드를 작성해 줄 때인데요. 여기서는 scene을 준비하고, 그 scene이 사용자의 터치 이벤트(버튼에 대한 이벤트)를 받아 처리할 수 있도록 해줘야 합니다.
   @Override
   public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) throws IOException {
      mScene = new Scene();
      mScene.setTouchAreaBindingOnActionDownEnabled(true);
      
      pOnCreateSceneCallback.onCreateSceneFinished(mScene);
   }

ButtonSprite가 의도한데로 제대로 동작하게 하기 위해서는  먼저 'Down Action'을 받을 수 있도록   mScene내에 터치 영역을 활성화 해줘야 합니다(Line 2). 그래야 mScene이 먼저 해당 이벤트를 잡아낸 후 그것을 그 자식 노드들에게 전달해 주거든요.  만약 Line 2 과정을 빼먹으면, 사용자가 버튼을 터치해서 '누름'상태로 넘어간 후 드래그 하거나 해서 버튼 밖으로 터치 위치가 빠져나갈 경우에 버튼이 '누름'상태로 유지되어 버립니다. 다시 '보통' 상태로 안돌아 와요.

4. 다음으로, onPopulateScene()에서는, 미리 준비한 mButtonTextureResion 객체를 이용해서 ButtonSprite 객체를 생성하고, 이 객체가 터치 이벤트를 받을 수 있도록 해주면 됩니다.
   @Override
   public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) throws IOException {
      
      ButtonSprite buttonSprite = new ButtonSprite(CAMERA_WIDTH*0.5F, CAMERA_HEIGHT*0.5F, mButtonTextureRegion, mEngine.getVertexBufferObjectManager()){

         @Override
         public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float pTouchAreaLocalX, float pTouchAreaLocalY) {
            if(pSceneTouchEvent.isActionDown()){
               CreatingButtons.this.runOnUiThread(new Runnable(){

                  @Override
                  public void run() {
                     Toast.makeText(getApplicationContext(), "Button Pressed", Toast.LENGTH_LONG).show();
                  }
                  
               });
            }
            return super.onAreaTouched(pSceneTouchEvent, pTouchAreaLocalX, pTouchAreaLocalY);
         }
         
      };
      
      mScene.registerTouchArea(buttonSprite);
      mScene.attachChild(buttonSprite);
      pOnPopulateSceneCallback.onPopulateSceneFinished();
   }

Inner 클래스의 형태로 생성 단계에서 버튼의 동작(눌러졌을 때)까지 정의하며 ButtonSprite 인스턴스를 생성합니다(Line 4-19). 그 내부에서는 Toast 메시지를 화면에 출력하기 위해 (안드로이드 UI스레드에서 실행되어야 할 ) Runnable 객체를 역시 Inner클래스 형태로 정의해서 인자로 넘기고 있네요.(Line 9)
Line 8에서 사용된 isActionDown() 말고도, isAcitonMove(), isActionUp() 등으로 이벤트의 상태를 확인해서 거기에 맞게 동작을 정의해 줄 수도 있습니다.

그리고, Line 23-24는 터치 영역을 mScene객체에 등록하고, 자식 노드로 부착하는 코드입니다. 버튼의 onAreaTouched()메소드가 호출되게 하려면 반드시 처리해 줘야 하는 부분이니 꼭 기억하세요.



전체 소스코드

package chapter3;

import java.io.IOException;

import org.andengine.engine.camera.Camera;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.FillResolutionPolicy;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.sprite.ButtonSprite;
import org.andengine.input.touch.TouchEvent;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlas;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlasTextureRegionFactory;
import org.andengine.opengl.texture.atlas.bitmap.BuildableBitmapTextureAtlas;
import org.andengine.opengl.texture.atlas.bitmap.source.IBitmapTextureAtlasSource;
import org.andengine.opengl.texture.atlas.buildable.builder.BlackPawnTextureAtlasBuilder;
import org.andengine.opengl.texture.atlas.buildable.builder.ITextureAtlasBuilder.TextureAtlasBuilderException;
import org.andengine.opengl.texture.region.ITiledTextureRegion;
import org.andengine.ui.activity.BaseGameActivity;

import android.widget.Toast;

public class CreatingButtons extends BaseGameActivity {
   
   public static int CAMERA_WIDTH=800;
   public static int CAMERA_HEIGHT=480;
   
   private Scene mScene;
   private Camera mCamera;
   
   private ITiledTextureRegion mButtonTextureRegion;

   @Override
   public EngineOptions onCreateEngineOptions() {
      mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
      EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(), mCamera);
      
      return engineOptions;
   }

   @Override
   public void onCreateResources( OnCreateResourcesCallback pOnCreateResourcesCallback) throws IOException {
      
      BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
      BuildableBitmapTextureAtlas bitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 300, 50);
      mButtonTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(bitmapTextureAtlas, getAssets(), "button_tiles.png", 2, 1);
      try{
         bitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder(0,0,0));
      }catch(TextureAtlasBuilderException ex){
         ex.printStackTrace();
      }
      
      bitmapTextureAtlas.load();
      
      pOnCreateResourcesCallback.onCreateResourcesFinished();
   }

   @Override
   public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) throws IOException {
      mScene = new Scene();
      mScene.setTouchAreaBindingOnActionDownEnabled(true);
      
      pOnCreateSceneCallback.onCreateSceneFinished(mScene);
   }

   @Override
   public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) throws IOException {
      
      ButtonSprite buttonSprite = new ButtonSprite(CAMERA_WIDTH*0.5F, CAMERA_HEIGHT*0.5F, mButtonTextureRegion, mEngine.getVertexBufferObjectManager()){

         @Override
         public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float pTouchAreaLocalX, float pTouchAreaLocalY) {
            if(pSceneTouchEvent.isActionDown()){
               CreatingButtons.this.runOnUiThread(new Runnable(){

                  @Override
                  public void run() {
                     Toast.makeText(getApplicationContext(), "Button Pressed", Toast.LENGTH_LONG).show();
                  }
                  
               });
            }
            return super.onAreaTouched(pSceneTouchEvent, pTouchAreaLocalX, pTouchAreaLocalY);
         }
         
      };
      
      mScene.registerTouchArea(buttonSprite);
      mScene.attachChild(buttonSprite);
      pOnPopulateSceneCallback.onPopulateSceneFinished();
   }

}














No comments:

Post a Comment