воскресенье, 17 февраля 2013 г.

Менеджер ресурсов

Привет! В этом сообщении я освещу некоторые вопросы связанные с загрузкой и освобождением ресурсов игры и на итог мы напишем шаблон класса который будет организовывать работу с этими ресурсами.

Ресурсами игры считается вся используемая графика, музыка, звуки и шрифты.
Многие из вас, только начиная работать с AndEngine, действовали по шаблону взятому из примеров: создавали активити, прописывали в ней при необходимости поля текстур, регионов и спрайтов; заполняли поля в onCreateResources(); собирали сцену в onCreateScene() и радовались жизни =) Проблемы начинались тогда, когда объект игры представлял собой не просто спрайт, а комплексный объект использующий свои дополнительные свойства и методы, содержащий более одного спрайта и т.д. С подходом описанным выше, приходиться либо объявлять поля активити как protected/public или передавать текстуры через конструктор объекта. Как результат:
- грязный код,
- активити заполнено полями которые этой активти не используются,
- множество параметров в конструкторах объектов,
- сложности в отслеживании неиспользуемых ресурсов.

Нам было бы гораздо удобнее иметь некий менеджер, который бы давал нам необходимые текстуры и вызывался тогда когда он нам нужен без его глобального указателя, чтобы этот указатель тоже не таскать за собой через конструкторы объектов, при этом, чтобы не нарушать lifecycle игры, ресурсы все-таки должны грузиться в onCreateResources().

Вы уже наверняка догадались, что этим условиям идеально подходит синглтон.
Сразу скажу, что решение вопроса доступа к ресурсам через синглтон целиком и полностью зависит от того, как организованы ваши игровые объекты. Т.е. игровым объектом может быть простое расширение спрайта (Object extended Sprite), а может быть и объект содержащий несколько спрайтов. Здесь я приведу код простого ResourceManager-а просто для того. чтобы обозначить сам подход к реализации.


package com.expedition107.evileye;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

import org.andengine.audio.music.Music;
import org.andengine.audio.music.MusicFactory;
import org.andengine.audio.sound.Sound;
import org.andengine.audio.sound.SoundFactory;
import org.andengine.engine.Engine;
import org.andengine.entity.scene.Scene;
import org.andengine.opengl.font.Font;
import org.andengine.opengl.font.FontFactory;
import org.andengine.opengl.texture.TextureOptions;
import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.region.ITextureRegion;
import org.andengine.opengl.texture.region.TextureRegionFactory;
import org.andengine.util.adt.color.Color;
import org.andengine.util.adt.io.in.IInputStreamOpener;

import android.content.Context;
import android.graphics.Typeface;

public class ResourceManager {

 private static ResourceManager Instance;
 
 //Поля для доступа к "движковым" переменным
 private Context currentActivity;
 private Engine engine;

 //текстуры
 public BitmapTexture mBackgroundBitmapTexture;
 public BitmapTexture mCloudsBitmapTexture;
 public BitmapTexture mHeroBitmapTexture;
 //регионы
 public ITextureRegion bgrTextureRegion;
    public ITextureRegion heroTextureRegion;
    public ITextureRegion cloudTextureRegion;
    public ITextureRegion arrowTextureRegion;
    
 //музыка и звуки
    public Music bgrMusic;
 public ArrayList<Sound> sounds = new ArrayList<Sound>(); 
 
 //шрифт
 public Font font;


 public synchronized static ResourceManager getInstance(){
  if(Instance == null){
   Instance = new ResourceManager();
  }
  return Instance;
 }
 
 //для того чтобы наш менеджер заработал мы должны его проинициализировать
 public synchronized void init(Context pContext, Engine pEngine){
  currentActivity = pContext;
  engine = pEngine;
 }
 
 public synchronized void loadGameSceneTextures(){
  
  try {
   mBackgroundBitmapTexture = new BitmapTexture(engine.getTextureManager(), new IInputStreamOpener() {
    @Override
    public InputStream open() throws IOException {
     return currentActivity.getAssets().open("gfx/background.png");
    }
   }, TextureOptions.BILINEAR);
   mBackgroundBitmapTexture.load();
   bgrTextureRegion = TextureRegionFactory.extractFromTexture(mBackgroundBitmapTexture);
   
   mCloudsBitmapTexture = new BitmapTexture(engine.getTextureManager(), new IInputStreamOpener() {
    @Override
    public InputStream open() throws IOException {
     return currentActivity.getAssets().open("gfx/clouds.png");
    }
   }, TextureOptions.BILINEAR);
   mCloudsBitmapTexture.load();
   cloudTextureRegion = TextureRegionFactory.extractFromTexture(mCloudsBitmapTexture);
   
   mHeroBitmapTexture = new BitmapTexture(engine.getTextureManager(), new IInputStreamOpener() {
    @Override
    public InputStream open() throws IOException {
     return currentActivity.getAssets().open("gfx/hero.png");
    }
   }, TextureOptions.BILINEAR);
   mHeroBitmapTexture.load();
   heroTextureRegion = TextureRegionFactory.extractFromTexture(mHeroBitmapTexture, 0, 0, 100, 100);
   arrowTextureRegion = TextureRegionFactory.extractFromTexture(mHeroBitmapTexture, 110, 0, 50, 5);
   
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  
  
  
  
  
 }
 
 public synchronized void unloadGameSceneTextures(){
  mBackgroundBitmapTexture.unload();
  mCloudsBitmapTexture.unload();
  mHeroBitmapTexture.unload();
  System.gc();
 }
 
 public synchronized void loadSounds(){
  SoundFactory.setAssetBasePath("sounds/");
   try {
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit1sound.mp3"));    
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit2sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit3sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hit4sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "hero_die_sound.mp3"));
    sounds.add(SoundFactory.createSoundFromAsset(getInstance().engine.getSoundManager(), getInstance().currentActivity, "shootsound.mp3"));
   } catch (final IOException e) {
             
   }
 } 
 
 public synchronized void unloadSounds(){
  while (getInstance().sounds.size() > 0){
   if (!getInstance().sounds.get(0).isReleased()){
    getInstance().sounds.get(0).release();
   }
   getInstance().sounds.remove(0);
  }
 }
 
 public synchronized void loadMusic(){
  MusicFactory.setAssetBasePath("music/");
   try {
    bgrMusic = MusicFactory.createMusicFromAsset(getInstance().engine.getMusicManager(), getInstance().currentActivity, "mfx/bgrmusic1.mp3");
   } catch (final IOException e) {
             
   }
 } 
 public synchronized void unloadMusic(){
  if (!bgrMusic.isReleased())
   bgrMusic.release();
 }
 
 public synchronized void loadFont(){
  FontFactory.setAssetBasePath("fonts/");
  font = FontFactory.create(getInstance().engine.getFontManager(), getInstance().engine.getTextureManager(), 256, 256, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL),  32f, true);
  font.load();
 }
 
 public synchronized void unloadFont(){
  font.unload();
 }
}


Как им пользоваться:
В onCreateResources() нашей активити c начала инициализируем ResourceManager, а затем грузим всю нашу графику, звуки, шрифты и т.д.:

@Override
 public void onCreateResources() {
  ResourceManager.getInstance().init(this, mEngine);
  ResourceManager.getInstance().loadGameSceneTextures();
  ResourceManager.getInstance().loadSounds();
  ResourceManager.getInstance().loadMusic();
  ResourceManager.getInstance().loadFonts();
 }

Где-то в коде..
...
ResourceManager rm = ResourceManager.getInstance();
mArrowSprite = new Sprite(pX, pY, rm.arrowTextureRegion,  rm.engine.getVertexBufferObjectManager());
...

Вы можете спросить - и чо же такого в нем менеджерского, чтобы выделять этот код в отдельный класс? Ответ на этот вопрос кроется в организации структуры игры. Если ваша игра состоит только из одной сцены (а такого не бывает ;-)), то менеджер не нужен. Но в самом простом случае мы имеем как минимум две сцены: 1. Главное меню 2. Сцена с самой игрой. Нам не нужны ресурсы игры когда мы находимся в главном меню и наоборот - в игре нам не нужны ресурсы главного меню. Так вот, чтобы загружать/выгружать нужные/ненужные ресурсы и поможет наш менеджер.
PS. Продумывайте какие ресурсы будут задействованы во время главного геймплея. Все ресурсы необходимо грузить до того как игра началась. Если в процессе игры приложение начнет что-то загружать - это немедленно отразится на производительности.


четверг, 14 февраля 2013 г.

AndEngine GLES2-AnchorCenter vs AndEngine GLES2. Стоит ли овчинка выделки?

Всем привет!

В этом сообщении хочу немного прояснить ситуацию с новой версией AndEngine GLES2-AnchorCenter (сокращенно будем писать AC).

NOTE: Здесь я не буду подробно расписывать разницу между GLES2 и AC потому, что для этого нужно досконально знать оба движка. Данное сообщение скорее призвано  сэкономить ваше время на установку рабочей сборки AC, дать небольшое представление о целесообразности миграции с GLES2 на AC, а также немного осветить особенности нового двига.

Многие из вас, кто уже работал с версиями AndEngine GLES1 и GLES2, встречали на официальном форуме движка упоминание об AndEngine AnchorCenter (AC), а так же о том, что, это, на текущий момент, самая передовая версия AndEngine. Многие старожилы форума уже давно на нем работают, и активно участвуют в развитии данной версии. Тем не менее, от создателя движка, нет никаких официальных сообщений (релизов) этой версии в которых бы говорилось что-то вроде "ребята, велком в новую версию. юзаем, сообщаем о багах... + список отличий от GLES2". Но ведь люди ее используют, причем весьма успешно решают задачи, о которых в GLES2 даже не заикались! Даже книжку написали по нему. Ситуация усугубляется тем, что многие баги движка, почему-то в первую очередь исправляются именно в ветке AC, а не в официальном GLES2. Примером тому может служить баг в Engine.java в функциях

protected void onUpdateScene(final float pSecondsElapsed)
protected void onUpdateUpdateHandlers(final float pSecondsElapsed)

проявляющийся в лагах графики (jitter effect) при движении камеры отслеживающей какой-нибудь объект.

А так же баг в расширении AndEngineLiveWallpaper, проявляющийся в том, что обои после перезагрузки девайса или при переходе с активити на хоумскрин замораживались.И вообще, глядя на историю изменений веток GLES2 и AC, на момент написания этого сообщения, видно, что правки по GLES2 последний раз были два месяца назад, а по AC два дня назад.

На днях, в очередной раз просматривая форум на предмет интересных статей или вопросов, я опять столкнулся с Anchor Center и моя чаша терпения переполнилась...

Итак, настало время разобраться, что это за AnchorCenter и стоит ли на него переходить.
Начал я с того, что для экспериментов создал отдельный workspace. Дальше по порядку:
1. С помощью Git импортируем версию движка из ветки GLES2-AnchorCenter (здесь и далее только из нее)
https://github.com/nicolasgramlich/AndEngine.git (Почему именно c помощью Git а не просто скачать архивом, поясню: раз уж движок активно развивается и дорабатывается, то каждый раз (через месяц, неделю, день) искать отличия и выкачивать зип обновленной версии, а потом повторять процедуру импорта и линковки будет сильно накладно по времени и нервам. Git в этом случае просто спасение утопающих. Кто не умеет им пользоваться, тому самое время научиться).
2. Импортируем расширение для физики AndEngineBox2DExtension.
https://github.com/nicolasgramlich/AndEnginePhysicsBox2DExtension.git
А теперь... АХТУНГ! НЕ КАЧАЙТЕ ЭТУ ВЕРСИЮ! По крайней мере пока. На момент написания сообщения (15 февраля 2013 года) это глючный порт*. Я потратил кучу времени, чтобы удостовериться, что он НЕ юзабельный (но не обижаюсь, потому, что мне никто и не обещал, что он рабочий=)), Так что не тратьте на него свое время. Рабочая и наиболее полная (RopeJoint, GearJoint, WheelJoint, EdgeShape и т.д.) версия порта Box2D лежит тут:
https://github.com/RealFictionFactory/AndEnginePhysicsBox2DExtension.git
3. Также под AC есть расширение AndEngineTMXTiledMapExtension, но я его не проверял. Лежит здесь:
https://github.com/nicolasgramlich/AndEngineTMXTiledMapExtension.git
4. Расширение для живых обоев:
https://github.com/nicolasgramlich/AndEngineLiveWallpaperExtension.git
Должно быть рабочее. В принципе, там особо ничего не поменялось кроме исправления бага про который я написал выше.
5. Настоятельно рекомендую расширение AndEngineDebugDrawExtension:
https://github.com/nazgee/AndEngineDebugDrawExtension.git Честь и слава людям, которые делают подобные вещи! Расширение позволяет без всякой графики (да и с графикой тоже) дебажить физический мир который вы построили в своем приложении.
6. Примеры AndEngineExamples:
https://github.com/nicolasgramlich/AndEngineExamples.git
Когда я их качал пару месяцев назад, то запустить не хватило терпения. Все было в полном хаосе. Сейчас посмотрел в ветку и там, напротив src, написано следующее:
Updated all examples to the latest and greatest of AndEngine@GLES2-AnchorCenter!

Последнее изменение было 9 месяцев назад... Имеет смысл их качать или нет предлагаю удостовериться самим. Сам сделаю это попозже.

Ок. Это вроде и все, что нам нужно для создания игры на новом движке. А как же расширение для  мультитача, текстурпакера? А они уже в самом движке, что не может не радовать =).
По расширениям AndEngineMODPlayerExtension, AndEngineMultiplayerExtension, AndEngineSVGTextureRegionExtension сказать пока ничего не могу, судя по репозиторию, эти расширения присутствуют только для GLES2, но с одной стороны, в них нет ничего такого что не работало бы и в AC, а с другой стороны - за все время пользования движком, я подключал их только ради ознакомления, в реальных задачах они не участвовали (хотя это, возможно, мой минус). И все бы хорошо, но это, как ни странно - мелочи. Приятные, удобные но... мелочи.

Основное отличие AC от предыдущей версии состоит в том, что поменялась ось координат. Теперь точка (0,0) находится не в левом верхнем углу, а в левом нижнем. Такое изменение создатель движка мотивирует тем, что привел координаты сцены к родной системе OpenGL, в результате, освободились ресурсы процессора которые тратились на перевод из одних координат в другие. Ну чтож, это нововведение можно считать целесообразным, а нам не так уж и долго перестроиться. Кроме инверсии оси ординат,  координаты спрайта относительно сцены теперь также не левый верхний угол, а... центр этого спрайта! Т.е. создали спрайт:

Sprite sprite = new Sprite(100,200, mSpriteTextureRegion, VBO);

и поместили на сцену

mScene.attachChild(sprite);

Так вот центр спрайта будет аккурат в координате (100,200) . И sprite.getX(), sprite.getY() вернет нам те же 100 и 200. (Ага! Вот почему он AnchorCenter!) Немного мозголомно правда? Здесь привыкать будет чуть сложнее, но мы привыкнем, я точно знаю =) Хорошо, ну а это-то извращение для чего понадобилось? Думаю, опять же для экономии ресурсов. От центра спрайта, проще высчитывать вершины и делать с ними различные преобразования. Кстати центр этот можно задавать. Для этого в Entity предусмотрены функции setAnchorCenter.... Функции getWidthScaled и getHeightScaled теперь deprecated. Тем кому приходилось сталкиваться со скайлом и дальнейшей привязкой координат это оценят.
Итак, смена системы координат - основное отличие. Имхо, за этим изменением кроются гораздо более серьезные возможности движка о которых пока умалчивается.
Вот что пишет Nicolas Gramlich в своем блоге (цитата):

"...The coordinate system in the GLES2-AnchorCenter branch has its origin in the lower left. This was changed for multiple reasons:
  • It is the native OpenGL coordinate system. (I can save a few +- calculations here and there.)
  • It is the same coordinate system as cocos2d-iphone and cocos2d-x. (This eases porting efforts in both directions by a whole bunch!)
  • It allowed me to easily/efficiently write the AndEngineCocosBuilderExtension, which allows reading a format exported by CocosBuilder.
  • It just feels more natural for any side-scrolling game.
Another thing that changed in this branch is that the anchorpoints (rotatincenter, scalecenter, etc…) are now relative, from 0.0 to 1.0, instead of being absolute values. So in general, AndEngine got a little more cocos2d-like on this branch, which is definitely not a bad thing...."  

Про AndEngineCocosBuilderExtension заметили? Я тоже заметил. =) Судя по всему отличный инструмент, надо будет его попробовать.**

Для эксперимента взял одну из наших недоработанных игр включающую в себя физику и попробовал перенести ее на новый двиг. С тем набором расширений который перечислен выше (а этих наборов пришлось перебрать несколько )) эксперимент завершился удачно, небольшие заминки возникали именно из-за новой системы координат. Кстати, замечено, что с новой системой, прикручивать графику к физтелам стало удобнее. Чтобы понять почему удобнее,  нужно пройти путь от GLES1 до AnchorCenter поэтому объяснить это я не берусь, да оно и не к чему ).

Резюмируя данное сообщение немного по-философствуем на тему переходить или нет на AnchorCenter =):

"ЗА"
1. Исправления багов, рефакторинг и новые плюшки делаются уже для AC, в то время как столкнувшись с багом в GLES2 вам обеспечены скитания по исходникам или форуму в поисках нужной заплатки. Плюшек и рефакторинга в GLES2 ожидать вообще не приходится.
2. Наиболее необходимые расширения движка за исключением Box2D уже интегрированы в сам движок.
3. Отличия GLES2 и AC значительны по внутреннему содержанию, но с точки зрения программиста переходить не так уж и проблемно (вспомним GLES1 vs GLES2 и выдохнем =)).

"ПРОТИВ"
1. Если сам движок вполне юзабельный, то расширение Box2D в официальной ветке вызывает грусть. Не смотря на то, что есть добрые люди как Andrzej J. Debicki (RealFictionFactory) и Michal Stawinski (nazgee), все-таки хочется, чтобы рабочая версия лежала и на официальной ветке.
2. Программистская  агорафобия  =)

В целом, советую дописать то, что не дописано, на той версии на которой вы работаете сейчас, ну а следующий шедевр начинать уже в AnchorCenter! 

Спасибо за внимание.

* - глючность порта у меня проявилась в использовании PrismaticJoint. Тело, которое должно было двигаться по оси X почему-то "тонуло" под действием силы тяжести. Что я только с ним не вытворял - ничего не помогало, хотя по определению PrismaticJoint никак не могло дать такую степень свободы. В итоге оказалось, что такое поведение тела случается если зафиксировать его вращение body.setFixedRotation(true); Ну кто бы мог подумать, что эта опция напрочь убивает джоинт, и какие еще подобные сюрпризы нам уготованы?
** - попробовать это расширение не получилось =(, потому как не смог найти этот билдер под Windows. Мало того, говорят, что его и не будет. А с другой стороны - если у программиста есть Мак, то он наверное пишет под iOS ага? Короче тут облом (.