понедельник, 2 апреля 2012 г.

Andengine. Живые обои

В сегодняшней "лекции" пойдет разбор как сделать живые обои они же (LiveWallpaper) на движке Andengine GLES2, ну т.е. на OpenGL 2. Я с трудом нашел рабочий пример на форуме andengine, да и то после того как написал свой :)

Итак, я почти уверен что сам AndEngine у Вас уже стоит, причем свежайшая версия. Потому что ветка правится каждый день. Помимо него, нужно еще расширение AndEngineWallpaperExtension. Т.е. скачиваем, распаковываем, кладем в наш workspace для Eclipse, в самом эклипсе делаем Import->Exiting Projects into Workspace, выбираем сначала один, потом второй каталог.
Возможно, придется обновить SDK до последних версий.

Теперь создаем проект под названием LiveWallpaperService, в Properties->Android->Library добавляем AndEngine и AndEngineWallpaperExtension. Проект готов. Главная активити у нас должна наследоваться от BaseLiveWallpaperService, т.е. болванка выглядит следующим образом:

public class LiveWallpaperService extends BaseLiveWallpaperService {

 @Override
 public EngineOptions onCreateEngineOptions() {
 }

 @Override
 public void onCreateResources(
   OnCreateResourcesCallback pOnCreateResourcesCallback)
   throws Exception {
 }

 @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback)
   throws Exception {
 }

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

 @Override
 public void onAccelerationAccuracyChanged(AccelerationData pAccelerationData) {
 }
}

За основу я взял проект GLES1 http://code.google.com/p/andenginelivewallpaperextensionexample/. Оттуда можно взять картинки и положить их в каталог /assets/gfx

Правим файл /res/values/strings.xml:

    Cigarette Live-Wallpaper
    Cigarette Live-Wallpaper
    A cigarette smoking on your home-screen.

Я думаю тут всё понятно. Вторая и третья строка - соответственно название и дескрипшн, которые будут отображаться в списке живых обоев.

Добавляем файл /res/xml/wallpaper.xml:


        

Здесь тоже надеюсь всё понятно.

И правим AndroidManifest.xml, приводим его к следующему виду:


    
 
    
    
    
    
        
            
                
            
   
            
        
    



Да, в каталог /res/drawable не забываем добавить превьюшку обоев, которая будет отображаться в списке :). Подготовка на этом закончена, возвращаемся к LiveWallpaperService.java.
Как обычно, (если вы уже сделали HelloWorld на AndEngine), создаем камеру и Engine в методе onCreateEngineOptions:

 @Override
 public EngineOptions onCreateEngineOptions() {
  mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);

  return new EngineOptions(
                              true
       , mScreenOrientation
       , new FillResolutionPolicy()
       , mCamera          
   );  
  
 }

и подгрузим картинки в onCreateResources:

 @Override
 public void onCreateResources(
   OnCreateResourcesCallback pOnCreateResourcesCallback)
   throws Exception {
  
  // каталог, откуда берем все картинки
  BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
  
  // подготовка атласа
  mTexture = new BitmapTextureAtlas(
          getTextureManager()
    , 512
    , 128
    , TextureOptions.BILINEAR_PREMULTIPLYALPHA
     );
  
  // установка положения текстур в атласе
  this.mCigaretteTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(this.mTexture, this, "cigarette.png", 0, 0); // 400x120
  this.mSmokeTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(this.mTexture, this, "smoke.png", 381, 0); // 64x64
  
  // загрузка текстур в атлас
  mTexture.load();
  
  // включаем акселерометр
  this.enableAccelerationSensor(this);
  
  pOnCreateResourcesCallback.onCreateResourcesFinished();


Все вроде откомментировано и понятно. Единственное, наше приложение будет реагировать на датчик положения, поэтому нужно добавить интерфейс IAccelerationListener в объявление класса, чтобы наше приложение реагировало на акселерометр. И соответственно создадутся заглушки на два новых метода - onAccelerationAccuracyChanged и onAccelerationChanged.

В конце вызывается метод onCreateResourcesFinished(), что говорит приложению об окончательной загрузке ресурсов.

Переходим к созданию сцены, правим метод onCreateScene:
  @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback)
   throws Exception {
  // сцена
  mScene = new Scene();

  // координаты левого верхнего угла спрайта сигареты
        final int cigaretteX = 0;
        final int cigaretteY = CAMERA_HEIGHT - (int)this.mCigaretteTextureRegion.getHeight();
        
        // координаты левого верхнего угла спрайта дыма
        final int smokeX = cigaretteX + (int)this.mCigaretteTextureRegion.getWidth() - 100;
        final int smokeY = cigaretteY - 25;
        
        // создаем спрайты сигареты, дыма
        final Sprite sigarette = new Sprite(cigaretteX, cigaretteY, this.mCigaretteTextureRegion, this.getVertexBufferObjectManager());
  final Sprite smoke = new Sprite(smokeX, smokeY, this.mSmokeTextureRegion, this.getVertexBufferObjectManager());
  
  // добавляем слои сигареты и дыма на сцену
  mScene.attachChild(sigarette);
  mScene.attachChild(smoke);
  
  // генератор частиц
  final SpriteParticleSystem particleSystem = new SpriteParticleSystem(
      new PointParticleEmitter(smokeX, smokeY) // Particle Emitter. Координаты генератора частиц
    , 6                                        // минимальная частота
    , 8                                        // максимальная частота
    , 150                                      // максимум частиц
    , this.mSmokeTextureRegion                 // текстура дыма
    , this.getVertexBufferObjectManager()
  );  
  
  // скорость частиц
  mVelocityInitializer = new VelocityParticleInitializer(
    -20 // min X
      , 20 // max X
      , -100 // min Y
      , -120 // max Y
  ); 
  
  particleSystem.addParticleInitializer(mVelocityInitializer);
  
  // прозрачность частиц
  particleSystem.addParticleModifier( new AlphaParticleModifier(
        1f         // from time
   , 0f         // to time
   , 0f         // from alfa
   , 10f        // to alfa
     ));
   
  mScene.attachChild(particleSystem);
  
  pOnCreateSceneCallback.onCreateSceneFinished(mScene);
 }

Сначала считаем координаты спрайтов относительно экрана, аттачим их к сцене. Создаем SpriteParticleSyste - "генератор дыма". В функции обратного вызова возвращаем готовую сцену.

Осталось только написать обработчик для акселерометра. Оформляем метод следующим образом:

 @Override
 public void onAccelerationChanged(AccelerationData pAccelerationData) {
            final float minVelocityX = (pAccelerationData.getX() + 2) * 5;
            final float maxVelocityX = (pAccelerationData.getX() - 2) * 5;
        
            final float minVelocityY = (pAccelerationData.getY() - 8) * 10;
            final float maxVelocityY = (pAccelerationData.getY() - 10) * 10;
            this.mVelocityInitializer.setVelocity(minVelocityX, maxVelocityX, minVelocityY, maxVelocityY);
 }

Тут по движению акселерометра изменяется скорость частиц в mVelocityInitializer.
Общий код приложения
package com.osmsoft.lws;

import org.andengine.engine.camera.Camera;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.EngineOptions.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.FillResolutionPolicy;
import org.andengine.entity.particle.SpriteParticleSystem;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.sprite.Sprite;
import org.andengine.extension.ui.livewallpaper.BaseLiveWallpaperService;
import org.andengine.opengl.texture.TextureOptions;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlas;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlasTextureRegionFactory;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.input.sensor.acceleration.AccelerationData;
import org.andengine.input.sensor.acceleration.IAccelerationListener;
import org.andengine.entity.particle.emitter.PointParticleEmitter;
import org.andengine.entity.particle.initializer.VelocityParticleInitializer;
import org.andengine.entity.particle.modifier.AlphaParticleModifier;

public class LiveWallpaperService extends BaseLiveWallpaperService implements IAccelerationListener {

    private static final int CAMERA_WIDTH = 800;
    private static final int CAMERA_HEIGHT = 480;
    
    private static final int LAYER_SMOKE = 1;
    private static final int LAYER_CIGARETTE = 0;
    
    public static Camera mCamera;
 
    private BitmapTextureAtlas mTexture;

    private ScreenOrientation mScreenOrientation;
    private VelocityParticleInitializer mVelocityInitializer;

    private Scene mScene;
    
    private ITextureRegion mSmokeTextureRegion;
    private ITextureRegion mCigaretteTextureRegion;
    
    
 @Override
 public EngineOptions onCreateEngineOptions() {
  mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);

  return new EngineOptions(
      true
       , mScreenOrientation
       , new FillResolutionPolicy()
       , mCamera          
   );  
  
 }

 @Override
 public void onCreateResources(
   OnCreateResourcesCallback pOnCreateResourcesCallback)
   throws Exception {
  
  // каталог, откуда берем все картинки
  BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
  
  // подготовка атласа
  mTexture = new BitmapTextureAtlas(
          getTextureManager()
    , 512
    , 128
    , TextureOptions.BILINEAR_PREMULTIPLYALPHA
     );
  
  // установка положения текстур в атласе
  this.mCigaretteTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(this.mTexture, this, "cigarette.png", 0, 0); // 400x120
  this.mSmokeTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(this.mTexture, this, "smoke.png", 381, 0); // 64x64
  
  // загрузка текстур в атлас
  mTexture.load();
  
  // включаем акселерометр
  this.enableAccelerationSensor(this);
  
  pOnCreateResourcesCallback.onCreateResourcesFinished();
 }

 @Override
 public void onCreateScene(OnCreateSceneCallback pOnCreateSceneCallback)
   throws Exception {
  // сцена
  mScene = new Scene();

  // координаты левого верхнего угла спрайта сигареты
        final int cigaretteX = 0;
        final int cigaretteY = CAMERA_HEIGHT - (int)this.mCigaretteTextureRegion.getHeight();
        
        // координаты левого верхнего угла спрайта дыма
        final int smokeX = cigaretteX + (int)this.mCigaretteTextureRegion.getWidth() - 100;
        final int smokeY = cigaretteY - 25;
        
        // создаем спрайты сигареты, дыма
        final Sprite sigarette = new Sprite(cigaretteX, cigaretteY, this.mCigaretteTextureRegion, this.getVertexBufferObjectManager());
  final Sprite smoke = new Sprite(smokeX, smokeY, this.mSmokeTextureRegion, this.getVertexBufferObjectManager());
  
  // добавляем слои сигареты и дыма на сцену
  mScene.attachChild(sigarette);
  mScene.attachChild(smoke);
  
  // генератор частиц
  final SpriteParticleSystem particleSystem = new SpriteParticleSystem(
      new PointParticleEmitter(smokeX, smokeY) // Particle Emitter. Координаты генератора частиц
    , 6                                        // минимальная частота
    , 8                                        // максимальная частота
    , 150                                      // максимум частиц
    , this.mSmokeTextureRegion                 // текстура дыма
    , this.getVertexBufferObjectManager()
  );  
  
  // скорость частиц
  mVelocityInitializer = new VelocityParticleInitializer(
    -20 // min X
      , 20 // max X
      , -100 // min Y
      , -120 // max Y
  ); 
  
  particleSystem.addParticleInitializer(mVelocityInitializer);
  
  // прозрачность частиц
  particleSystem.addParticleModifier( new AlphaParticleModifier(
        1f         // from time
   , 0f         // to time
   , 0f         // from alfa
   , 10f        // to alfa
     ));
   
  mScene.attachChild(particleSystem);
  
  pOnCreateSceneCallback.onCreateSceneFinished(mScene);
 }

 @Override
 public void onPopulateScene(Scene pScene,
   OnPopulateSceneCallback pOnPopulateSceneCallback) throws Exception {
  pOnPopulateSceneCallback.onPopulateSceneFinished();
 }

 @Override
 public void onAccelerationAccuracyChanged(AccelerationData pAccelerationData) {
  // TODO Auto-generated method stub
  
 }

 @Override
 public void onAccelerationChanged(AccelerationData pAccelerationData) {
        final float minVelocityX = (pAccelerationData.getX() + 2) * 5;
        final float maxVelocityX = (pAccelerationData.getX() - 2) * 5;
        
        final float minVelocityY = (pAccelerationData.getY() - 8) * 10;
        final float maxVelocityY = (pAccelerationData.getY() - 10) * 10;
        this.mVelocityInitializer.setVelocity(minVelocityX, maxVelocityX, minVelocityY, maxVelocityY);
 }
}

Ссылки по теме:
http://www.andengine.org/forums/live-wallpaper-extension/ - форум по расширению
http://www.andengine.org/forums/post32197.html - болванка для livewallpaper
http://www.andengine.org/forums/gles2/no-particle-system-examples-t6159.html?p27526 - Еще какие-то обои

воскресенье, 1 апреля 2012 г.

AndEngine. Движение спрайта по определенному маршруту

Ссылка на пример из документации http://code.google.com/p/andengineexamples/source/browse/src/org/anddev/andengine/examples/PathModifierExample.java.

Согласитесь, весьма распространенная задача - заставить спрайт двигаться через определенные чекпойнты. Делается это довольно просто, в onCreateScene (а может быть, где-нибудь еще) создается объект Path из пакета org.andengine.entity.modifier.*. В качестве аргумента конструктора указывается количество точек, через которые будет проходить спрайт. Если точек менее двух - вывалится Exception:

final Path path = new Path( route.size()+1 );

Заполняется массив точек:

path.to( mCurrentSprite.getX(), mCurrentSprite.getY() );            // от текущей позиции спрайта
path.to(100,100);
path.to(100,250);
....

Где mCurrentSprite - объект, наследованный от Sprite.

Создаем модификатор типа PathModifier:

  mEntityModifier = new PathModifier(SceneGame.DEFAULT_TIME_MOVING_OF_SHIP, path, null, new IPathModifierListener() {

   @Override
   public void onPathStarted(PathModifier pPathModifier,
     IEntity pEntity) {
    // TODO Auto-generated method stub
                                // код, который должен исполняться при старте маршрута
    
   }
 
   @Override
   public void onPathWaypointStarted(PathModifier pPathModifier,
     IEntity pEntity, int pWaypointIndex) {
                              // TODO Auto-generated method stub
                              // код, который выполняется при старте нового чекпойнта   
                        }

   
   
   @Override
   public void onPathWaypointFinished(PathModifier pPathModifier,
     IEntity pEntity, int pWaypointIndex) {
    // TODO Auto-generated method stub
                                // код, который выполняется при финише прохождения чекпойнта   
   }

   @Override
   public void onPathFinished(PathModifier pPathModifier,
     IEntity pEntity) {
                              // TODO Auto-generated method stub   
                              // код, который должен исполняться при финише маршрута
   }
  }); 

В примере из документации эта конструкция оборачивается в LoopEntityModifier(). Если это сделать, спрайт будет возвращаться в точку старта. Я когда в это дело только въезжал, очень долго не мог понять почему мой спрайт на сцене после анимации не меняет своего положения :)

И конечно, вешаем его на наш спрайт:

mCurrentSprite.registerEntityModifier(mEntityModifier);

В-принципе, на этом уже можно поставить точку. Если исполнить данный код, спрайт будет двигаться по заданному маршруту
В методы mEntityModifier можно записать всё, что нам необходимо для анимации спрайта. У меня например спрайт - кораблик поворачивается носом к следующему чекпойнту:
 @Override
 public void onPathWaypointStarted(PathModifier pPathModifier,
   IEntity pEntity, int pWaypointIndex) {
  if( pWaypointIndex < mRoute.size()-1 ){
   Point point1 = mRoute.get( pWaypointIndex );
   Point point2 = mRoute.get( pWaypointIndex+1 );
   float angle = (float) getAngleBetweenPoints(point1, point2);
   mCurrentSprite.setRotation(angle);
  } 
 }

 private double getAngleBetweenPoints(Point point1, Point point2){
  return Math.toDegrees( Math.atan2(point2.getY() - point1.getY(), point2.getX() - point1.getX()) );
 }

А в примерах из документации AndEngine он изменяет анимацию:
    switch(pWaypointIndex) {
     case 0:
      player.animate(new long[]{200, 200, 200}, 6, 8, true);
      break;
     case 1:
      player.animate(new long[]{200, 200, 200}, 3, 5, true);
      break;
     case 2:
      player.animate(new long[]{200, 200, 200}, 0, 2, true);
      break;
     case 3:
      player.animate(new long[]{200, 200, 200}, 9, 11, true);
      break;
    }