вторник, 12 июня 2012 г.

Новая версия AndEngine GLES2: продолжаем делать игры для Android


Раньше я писал о том, как легко и быстро можно сделать игру для Android c помощью замечательного 2D игрового движка AndEngine и его "физического" расширения box2D. С тех пор многое изменилось в нашем непостоянном мире. AndEngine теперь поддерживает OpenGL ES2.0, и в связи с этим существенно изменилось его API. Можно, конечно продолжать использовать предидущую версию, но если учесть внушительный список изменений, то такой путь выглядит тупиковым.
Как же быть начинающему разработчику Android-игр, если прочитав несколько руководств и создав своё BaseGameActivity он не увидит там больше описанных везде абстрактных методов onLoadEngine, onLoadResources, onLoadScene, onLoadComplete? Ответ простой: читать этот пост и погружаться в разработку игр с использованием новой версии AndEngine GLES2 :)

Новый репозиторий AndEngine



Cо времени последней моей статьи о движке, он переехал на GitHub. Не думаю, что у кого-то возникнут сложности с поиском исходников движка, но для "целостности повествования" ссылка тут. Кстати, бранч версии GLES1 уже удалён из репозитория.


Новые имена методов

Итак, скачали исходники, подключили к проекту как Android library project, создали главное Activity.
Как я уже писал выше, вам теперь не прийдётся переопрделять методы onLoadEngine, onLoadResources, onLoadScene, onLoadComplete из BaseGameActivity. Теперь начать разработку вам нужно с реализации других методов и из другого базового Activity: SimpleBaseGameActivity.
Вот примерная схема соответствия старых и новых абстрактных методов:

onLoadEngine (GLES1) -> onCreateEngineOptions (GLES2)
Старый метод возвращал объект Engine, чтобы создать который, нужно было определить EngineOptions, а "по пути" создать объект Camera. Новый метод возвращает непосредственно EngineOptions.

onLoadResources (GLES1) -> onCreateResources (GLES2)
Делаем практически то же самое: загружаем ресурсы

onLoadScene (GLES1) и onLoadComplete (GLES1) -> onCreateScene (GLES2) Тут вместо двух методов реализуем один. Смысл его тот же: нужно разместить наши объекты и проинициализировать PhysicsWorld если у нас игра с физикой.

Также изменились имена некоторых методов для работы с устройствами. Например, onAccelerometerChanged из версии GLES1 превратился в onAccelerationChanged в версии GLES2. Кроме имени метода тут ничего не изменилось. Он по-прежнему получает объект данных акселерометра, который можно использовать для установки направления гравитации в нашем виртуальном мире.

Новая игра на новом движке

Чтобы продемонстрировать вышеописанные особенности в деле, сделаем небольшую игру на движке AndEngine GLES2 с расширением Physics Box2D. Отправим маленький космический кораблик в путь по лабиринту. Управять полётом кораблика будем, наклоняя наш смартфон в разные стороны.  

import org.andengine.engine.Engine;
import org.andengine.engine.camera.Camera;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy;
import org.andengine.entity.primitive.Rectangle;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.scene.background.Background;
import org.andengine.entity.shape.IAreaShape;
import org.andengine.entity.sprite.Sprite;
import org.andengine.extension.physics.box2d.FixedStepPhysicsWorld;
import org.andengine.extension.physics.box2d.PhysicsConnector;
import org.andengine.extension.physics.box2d.PhysicsFactory;
import org.andengine.extension.physics.box2d.PhysicsWorld;
import org.andengine.extension.physics.box2d.util.Vector2Pool;
import org.andengine.input.sensor.acceleration.AccelerationData;
import org.andengine.input.sensor.acceleration.IAccelerationListener;
import org.andengine.opengl.texture.TextureManager;
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.TextureRegion;
import org.andengine.opengl.vbo.VertexBufferObjectManager;
import org.andengine.ui.activity.SimpleBaseGameActivity;
 
import android.content.res.Resources;
import android.hardware.SensorManager;
 
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.FixtureDef;
 
public class LExampleActivity extends SimpleBaseGameActivity implements
  IAccelerationListener {
 
 private static int CAMERA_WIDTH;
 private static int CAMERA_HEIGHT;
 private static final int MAX_STEPS_PER_UPDATE = 60;
 private static final int PHYSICS_STEPS_PER_SECOND = 60;
 private static final int PHYSICS_VELOCITY_ITERATIONS = 5;
 private static final int PHYSICS_POSITION_ITERATIONS = 5;
 private TextureRegion mShip;
 private Scene mScene;
 private PhysicsWorld mPhysicsWorld;
 private final FixtureDef FIXTURE_DEF = PhysicsFactory.createFixtureDef(0,
   0.5f, 0.5f);
 
 /**
  * Определяем размеры экрана, создаём камеру и определяем опции движка
  */
 @Override
 public EngineOptions onCreateEngineOptions() {
  Resources res = getResources();
  CAMERA_HEIGHT = res.getDisplayMetrics().heightPixels;
  CAMERA_WIDTH = res.getDisplayMetrics().widthPixels;
  Camera mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
  return new EngineOptions(true, ScreenOrientation.PORTRAIT_FIXED,
    new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), mCamera);
 }
 
 /**
  * Загружаем ресурсы
  */
 @Override
 protected void onCreateResources() {
  BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
  Engine engine = getEngine();
  TextureManager tm = engine.getTextureManager();
  BitmapTextureAtlas mTexture = new BitmapTextureAtlas(tm, 1024, 768,
    TextureOptions.NEAREST_PREMULTIPLYALPHA);
  mShip = BitmapTextureAtlasTextureRegionFactory.createFromAsset(
    mTexture, this, "ship.png", 0, 0);
  tm.loadTexture(mTexture);
 }
 
 /**
  * Создаём сцену, инициализируем физику и выводим на сцену
  * "действующее лицо"
  */
 @Override
 protected Scene onCreateScene() {
  mScene = new Scene();
  mScene.setBackground(new Background(0.09804f, 0.7274f, 0.8f));
  mPhysicsWorld = createPhysicBox(mScene);
  Sprite sprite = new Sprite(CAMERA_WIDTH / 2 - mShip.getWidth() / 2,
    CAMERA_HEIGHT - mShip.getHeight(), mShip,
    getVertexBufferObjectManager());
  mScene.attachChild(sprite);
  Body body = PhysicsFactory.createBoxBody(mPhysicsWorld, sprite,
    BodyType.DynamicBody, FIXTURE_DEF);
  mPhysicsWorld.registerPhysicsConnector(new PhysicsConnector(sprite,
    body, true, true));
  return mScene;
 }
 
 /**
  * Инициализация физики в игре. Также устанавливаем статические объекты -
  * "стены"
  * 
  * @param mScene
  * @return PhysicsWorld
  */
 private PhysicsWorld createPhysicBox(Scene mScene) {
  PhysicsWorld mPhysicsWorld = new FixedStepPhysicsWorld(
    PHYSICS_STEPS_PER_SECOND, MAX_STEPS_PER_UPDATE, new Vector2(0,
      SensorManager.GRAVITY_EARTH), false,
    PHYSICS_VELOCITY_ITERATIONS, PHYSICS_POSITION_ITERATIONS);
  VertexBufferObjectManager vbo = this.getVertexBufferObjectManager();
 
  final IAreaShape ground = new Rectangle(0, CAMERA_HEIGHT - 2,
    CAMERA_WIDTH, 2, vbo);
  final IAreaShape roof = new Rectangle(0, 0, CAMERA_WIDTH, 2, vbo);
  final IAreaShape left = new Rectangle(0, 0, 2, CAMERA_HEIGHT, vbo);
  final IAreaShape right = new Rectangle(CAMERA_WIDTH - 2, 0, 2,
    CAMERA_HEIGHT, vbo);
 
  final IAreaShape br1 = new Rectangle(0, CAMERA_HEIGHT / 3, CAMERA_WIDTH
    - CAMERA_WIDTH / 3, 2, vbo);
  final IAreaShape br2 = new Rectangle(CAMERA_WIDTH - CAMERA_WIDTH / 3,
    2 * CAMERA_HEIGHT / 3, 4 * CAMERA_WIDTH - CAMERA_WIDTH / 3, 2,
    vbo);
 
  PhysicsFactory.createBoxBody(mPhysicsWorld, ground,
    BodyType.StaticBody, FIXTURE_DEF);
  PhysicsFactory.createBoxBody(mPhysicsWorld, roof, BodyType.StaticBody,
    FIXTURE_DEF);
  PhysicsFactory.createBoxBody(mPhysicsWorld, left, BodyType.StaticBody,
    FIXTURE_DEF);
  PhysicsFactory.createBoxBody(mPhysicsWorld, right, BodyType.StaticBody,
    FIXTURE_DEF);
 
  PhysicsFactory.createBoxBody(mPhysicsWorld, br1, BodyType.StaticBody,
    FIXTURE_DEF);
  PhysicsFactory.createBoxBody(mPhysicsWorld, br2, BodyType.StaticBody,
    FIXTURE_DEF);
 
  mScene.attachChild(ground);
  mScene.attachChild(roof);
  mScene.attachChild(left);
  mScene.attachChild(right);
 
  mScene.attachChild(br1);
  mScene.attachChild(br2);
 
  mScene.registerUpdateHandler(mPhysicsWorld);
  return mPhysicsWorld;
 }
 
 /**
  * Регистрируем слушатель акселерометра
  */
 @Override
 protected void onResume() {
  super.onResume();
  this.enableAccelerationSensor(this);
 }
 
 /**
  * Удаляем слушатель акселерометра
  */
 @Override
 protected void onPause() {
  this.disableAccelerationSensor();
  super.onPause();
 }
 
 @Override
 public void onAccelerationAccuracyChanged(AccelerationData pAccelerationData) {
 }
 
 /**
  * При изменении вектора гравитации меняем его в виртуальном мире
  */
 @Override
 public void onAccelerationChanged(AccelerationData pData) {
  if (mPhysicsWorld != null) {
   final Vector2 gravity = Vector2Pool.obtain(pData.getX(),
     pData.getY());
   mPhysicsWorld.setGravity(gravity);
   Vector2Pool.recycle(gravity);
  }
 }
}



3 комментария:

  1. Спасибо большое. Будем разбираться))

    ОтветитьУдалить
  2. Вот уж, действительно, находка для программиста. Спасибо огромное! =)

    ОтветитьУдалить
  3. Спасибо, за полезную информацию!!!

    ОтветитьУдалить