Sunday, March 2, 2014

[ AndEngine Tutorial : Essential 32 ] TiledSprite 을 이용해 만든 음악 재생 버튼

이 강좌는 [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)




이번 강좌에서는 화면에 버튼이 사용자의 터치에 의해 상태가 변화하는 것은 같으나, 다음 이벤트가 버튼에 가해지지 않는 한 버튼의 상태는 변화하지 않게 하려 합니다. 그래서, 음악을 [재생 혹은 멈춤]하게 하는 버튼을 만들어보려 해요.  하지만, 이전 레시피(AndEngine Essential 31)와는 다른 점이 있는데요. ButtonSprite을 사용하지 않고 TiledSprite을 사용한다는 점입니다. 음악이 재생되거나 멈추거나 하는 상태에 따라 이미지 타일을 번갈아 사용하려고 해요. 그럼 시작해 볼까요?


타일 이미지와 사운드 파일을 준비하자

이전 레시피 처럼, 여기서도 타일 이미지를 준비할 건데요. 검은 색 화면과 좀 더 어울리는 버튼 이미지를 이용해서 아래처럼 만들어 봤습니다. 마찬가지로 다운로드 하셔서 gfx 폴더 밑으로 복사해 주시면 될거예요.


그리고, 하나 더 준비할 게 있는데, 사운드 파일이죠? 그래서 www.soundjay.com에서 그럴 듯한 음악 파일 하나를 다운로드 했습니다. 여러분도 필요하실테니 [여기]에서 다운로드 해서 이번엔 sfx 폴더를 하나 만드시고 거기에 복사해 주세요.



코드의 작성과 이해

이번 레시피에서는 음악 파일 및 텍스쳐를 다루는 법, 타일 형태의 텍스쳐 처리, 터치 이벤트의 처리를 한꺼번에 작성하게 되네요. 아마 실제 앱 개발에서도 충분히 활용 가능하고 유용한 정보일 거라 생각합니다.

1. 필요한 인스턴스 변수들을 선언합시다. 음악 파일을 처리할 거니 Music 객체를 위한 것, 타일 형태의 texture region을 위한 것, 그리고 그것을 이용할 TiledSprite 타입이 기본적으로 필요합니다.

public class MusicMenuActivity extends BaseGameActivity {

   public static int WIDTH = 800;
   public static int HEIGHT = 480;

   public static final int MUTE = 0;
   public static final int UNMUTE = 1;

   private Scene mScene;
   private Camera mCamera;

   private TiledSprite mMuteButton;
   private Music mMenuMusic;
   private ITiledTextureRegion mButtonTextureRegion;

거기에 추가해서  음악이 재생 상태인지 멈춤 상태인지를 저장하기 위한 static 변수도 두 개 정해져 있네요(Line 6,7)


2. 다음으로 onCreateResources() 메소드 내부에서는 텍스쳐와 음악 파일에 대한 처리가 함께 이루어져야 합니다. 아래 코드를 보실까요?
   @Override
   public void onCreateResources( OnCreateResourcesCallback pOnCreateResourcesCallback) {
      BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
      BuildableBitmapTextureAtlas bitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 256, 128);
      mButtonTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(bitmapTextureAtlas, getAssets(), "sound_button_tiles.png", 2, 1);

      try {
         bitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder( 0, 0, 0));
      } catch (TextureAtlasBuilderException e) {
         e.printStackTrace();
      }
      bitmapTextureAtlas.load();

      MusicFactory.setAssetBasePath("sfx/");
      try {
         mMenuMusic = MusicFactory.createMusicFromAsset(
               mEngine.getMusicManager(), this, "heart_of_the_sea_01.mp3");
      } catch (IllegalStateException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      pOnCreateResourcesCallback.onCreateResourcesFinished();
   }

12번째 라인까지는, 바로 전 레시피에서 다뤘던 부분이니 이미지의 크기만 다를 뿐 같은 방식으로 처리하시면 되고요. 14번째 부터는 Music 객체를 통해 우리가 미리 준비한 MP3 파일을 로드하는 과정입니다. 텍스쳐의 처리나 거의 비슷하죠?

3. 이전 레시피에서 mScene.setTouchAreaBindingOnActionDownEnabled(true)를 호출했을 때와 그렇지 않았을 때의 차이점에 대해 얘기했던 것 기억나세요? 여기서는 버튼이 눌러져도 다시 원래 상태로 돌아오지 않아야 하잖아요? 그러므로 여기에선 아래처럼 그 메소드를 호출해 주지 않습니다
   @Override
   public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) {
      mScene = new Scene();

      pOnCreateSceneCallback.onCreateSceneFinished(mScene);
   }



4. 다음은 바로 onPopulateScene()으로 넘어 갑시다. 여기선 TiledSprite 클래스를 이용해서 화면의 중앙에 나타날 이미지(버튼)을 생성해 주시면 됩니다. 이전 레시피의 버튼의 동작 처리처럼 여기서도 onAreaTouched()메소드를 오버라이딩 해서 음악 파일을 재생 혹은 멈추게 하시면 되요.
   @Override
   public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) {

      final float buttonX = WIDTH * 0.5f;
      final float buttonY = HEIGHT * 0.5f;

      mMuteButton = new TiledSprite(buttonX, buttonY, mButtonTextureRegion, mEngine.getVertexBufferObjectManager()) {

         @Override
         public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float pTouchAreaLocalX, float pTouchAreaLocalY) {
            if (pSceneTouchEvent.isActionDown()) {
               if (mMenuMusic.isPlaying()) {
                  this.setCurrentTileIndex(MUTE);
                  mMenuMusic.pause();
               } else {
                  this.setCurrentTileIndex(UNMUTE);
                  mMenuMusic.play();
               }
               return true;
            }
            return super.onAreaTouched(pSceneTouchEvent, pTouchAreaLocalX,
                  pTouchAreaLocalY);
         }
      };

      mMuteButton.setCurrentTileIndex(UNMUTE);

      mScene.registerTouchArea(mMuteButton);
      mScene.attachChild(mMuteButton);

      mMenuMusic.setLooping(true);
      mMenuMusic.play();

      pOnPopulateSceneCallback.onPopulateSceneFinished();
   }

Line 4,5 : 화면에 나타날 위치를 잡기 위해 두개의 지역변수를 선언하고 값을 지정해 줍니다.
Line 7 : TiledSprite 객체를 생성하는데, 이전에 했던 것처럼 inner 클래스 형태로 선언도 해줍니다.  onAreaTouched() 내부의 if문에서는 눌려진 상태인지 아닌지를 구분해서 눌려진 상태라면 음악을 재생하고, 그렇지 않으면 음악을 멈추게 하고 있습니다.
Line 25 : 버튼의 기본 설정을 UNMUTE로 지정해 주고, 그 다음으로 터치 영역을 등록해주는 작업, scene에 tiled sprite를 부착하는 작업, 액티비티가 시작하면서 바로 음악이 반복해서 재생되도록 설정하는 작업등이 이루어지고 있습니다.

* setCurrentTileIndex()메소드의 경우 미리 선언된 MUTE, UNMUTE 변수에 할당되어진 0, 1 값을 이용하는데요. ButtonSprite클래스는 내부의 enum 값으로 State.NORMAL, State.PRESSED 등이 미리 선언되어 있지만 TiledSprite에는 상태에 대한 정보를 담을 수 있는 다른 변수가 없어서 여기서 
미리 선언해 두고 그 값을 이용해 처리해 주는 것입니다

5. 마지막으로 작성해 줄 부분이 onResumeGame()과 onPauseGame()입니다. 우리 앱이 최소화 되어야 하는 경우, 그러다 다시 실행되어질 경우 이전의 상태에 맞게 버튼도 표시되고 (최소화 되면서 음악이 멈췄다면) 음악도 이어서 재생하게 하도록 해야하잖아요? 그 처리를 해주는 부분입니다
   @Override
   public synchronized void onResumeGame() {
      super.onResumeGame();
      
      //버튼도 이미 생성되어진 상태이고 음악도 로드되어진 상태라면 이전에 앱이 pause 되었다는 거죠.
      //그러므로, pause 되기전의 앱 상태값에 따라 다시 동작하도록 설정해 주어야 합니다.
      if (mMenuMusic != null && mMuteButton != null) {
         if(mMuteButton.getCurrentTileIndex() == UNMUTE){
            mMenuMusic.play();
         }
      }
   }

   @Override
   public synchronized void onPauseGame() {
      super.onPauseGame();
      
      //음악이 재생중이라면 pause 되기전에 재생을 멈추게 하는 거죠
      if(mMenuMusic != null && mMenuMusic.isPlaying()){
         mMenuMusic.pause();
      }
   }



전체 소스 코드


package chapter3;

import java.io.IOException;

import org.andengine.audio.music.Music;
import org.andengine.audio.music.MusicFactory;
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.TiledSprite;
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;

public class MusicMenuActivity extends BaseGameActivity {

   public static int WIDTH = 800;
   public static int HEIGHT = 480;

   public static final int MUTE = 0;
   public static final int UNMUTE = 1;

   private Scene mScene;
   private Camera mCamera;

   private TiledSprite mMuteButton;

   /* Music object containing a sound file of the music to be played */
   private Music mMenuMusic;

   private ITiledTextureRegion mButtonTextureRegion;

   @Override
   public EngineOptions onCreateEngineOptions() {

      mCamera = new Camera(0, 0, WIDTH, HEIGHT);

      EngineOptions engineOptions = new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED, new FillResolutionPolicy(), mCamera);

      /* Tell the engineOptions that we want to play music */
      engineOptions.getAudioOptions().setNeedsMusic(true);

      return engineOptions;
   }

   @Override
   public void onCreateResources(
         OnCreateResourcesCallback pOnCreateResourcesCallback) {
      BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");

      BuildableBitmapTextureAtlas bitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 256, 128);
      mButtonTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(bitmapTextureAtlas, getAssets(), "sound_button_tiles.png", 2, 1);

      try {
         bitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder( 0, 0, 0));
      } catch (TextureAtlasBuilderException e) {
         e.printStackTrace();
      }
      bitmapTextureAtlas.load();

      MusicFactory.setAssetBasePath("sfx/");
      try {
         mMenuMusic = MusicFactory.createMusicFromAsset(
               mEngine.getMusicManager(), this, "heart_of_the_sea_01.mp3");
      } catch (IllegalStateException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      pOnCreateResourcesCallback.onCreateResourcesFinished();
   }

   @Override
   public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback) {
      mScene = new Scene();

      pOnCreateSceneCallback.onCreateSceneFinished(mScene);
   }

   @Override
   public void onPopulateScene(Scene pScene, OnPopulateSceneCallback pOnPopulateSceneCallback) {

      final float buttonX = WIDTH * 0.5f;
      final float buttonY = HEIGHT * 0.5f;

      mMuteButton = new TiledSprite(buttonX, buttonY, mButtonTextureRegion, mEngine.getVertexBufferObjectManager()) {

         @Override
         public boolean onAreaTouched(TouchEvent pSceneTouchEvent, float pTouchAreaLocalX, float pTouchAreaLocalY) {
            if (pSceneTouchEvent.isActionDown()) {
               if (mMenuMusic.isPlaying()) {
                  this.setCurrentTileIndex(MUTE);
                  mMenuMusic.pause();
               } else {
                  this.setCurrentTileIndex(UNMUTE);
                  mMenuMusic.play();
               }
               return true;
            }
            return super.onAreaTouched(pSceneTouchEvent, pTouchAreaLocalX,
                  pTouchAreaLocalY);
         }
      };

      mMuteButton.setCurrentTileIndex(UNMUTE);

      mScene.registerTouchArea(mMuteButton);
      mScene.attachChild(mMuteButton);

      mMenuMusic.setLooping(true);
      mMenuMusic.play();

      pOnPopulateSceneCallback.onPopulateSceneFinished();
   }

   @Override
   public synchronized void onResumeGame() {
      super.onResumeGame();
      
      if (mMenuMusic != null && mMuteButton != null) {
         if(mMuteButton.getCurrentTileIndex() == UNMUTE){
            mMenuMusic.play();
         }
      }
   }

   @Override
   public synchronized void onPauseGame() {
      super.onPauseGame();
      
      if(mMenuMusic != null && mMenuMusic.isPlaying()){
         mMenuMusic.pause();
      }
   }
}




실행 화면 보기













No comments:

Post a Comment