суббота, 5 января 2013 г.

Разбираем LevelLoaderExample.



Всем привет. Если кто-то не в курсе, то вот отсюда можно скачать все примеры автора движка. Скачайте себе всю эту папочку и будет вам счастье. Сможете сами, как и я , понемногу разбираться в движке.
Ссылка на все примеры движка GLES2.

Сначала я вкратце расскажу что такое парсер, зачем он нужен. Если вы знаете, то это - пропустите первую часть данного поста - до кода. Затем я вам представлю пример, как работает движковый парсер по задумке автора. А после этого поделюсь своими мыслями о том, почему мне НЕ нравится данный парсер и почему я хочу сделать свой.


       Итак, что же такое парсер. Более правильный вопрос - зачем он нам нужен. Представьте себе уровень из платформера, например из Марио. Помните там были блоки разные, трубы и прочие объекты. Враги тоже были. Весь уровень выглядел как мозаика из блоков, которые расставлены и развешаны по уровню в строго определенных местах. Если вы загружаете уровень - всегда загружается одно и то же. Место нахождения этих блоков на уровне записано в некую базу данных. Место врагов - тоже в базе данных. И как враги перемещаются, и какие дропы падают из блоков с "вопросиком" и где переход на секретный уровень - все это записано в некой базе данных. Базу данных можно разместить прямо в коде. Создать кучу многомерных массивов, которые будут хранить координаты всех объектов, скорости и прочие характеристики. Это будет очень большой код. К тому же трудно-редактируемый. И каждый раз, когда мы решим изменить или дополнить уровень - нам придется ковырять код. Мы можем снабдить такой  код кучей комментариев, чтобы не заблудиться. Но это все будет "костыль".
       В современных играх базы данных конечно же размещаются в отдельных файлах. Очень удобно для этого использовать XML-файлы. Ответ на вопрос "Что такое "XML" можно посмотреть здесь. Применительно к данной игре (скорллеру космическому обыкновенному) - что же мы можем хранить в XML? Во-первых весь уровень. Уровень будет храниться в виде записей: что, когда появляется на экране, в какой точке, куда летит, что делает, как взаимодействует с героем, когда удаляется. Время появления на экране, координаты и скорость можно хранить в цифровом формате. А поведение можно тоже хранить в числах. Можно создать некие "программы поведения" и пронумеровать их. И каждому объекту задавать такую программу. А где же мы будем хранить программы поведения? Правильно - тоже в базе данных .Можно в той же, можно в отдельной. Например мы хотим задать несколько программ полета. Прямо, наискосок, по кругу. Как эта сделать? Я делал просто - последовательным набором  чисел. Каждая пара чисел означала X и Y - координаты точки, через которую должна проходить траектория.
       Необязательно все хранить в числовом формате - можно и в строчном. И в представленном внизу примере как раз используются и числовые и строчные данные.
       Ну а в конце вернемся к нашему первому вопросу - зачем нужен парсер. Парсер - это такая штука, которая позволяет прочитать XML-файл, найти в нем нужные данные и перенести данные из него в нашу программу. По-моему я достаточно подробно расписал все. Если что-то все-таки непонятно, то прошу в коменты или в сюда :)

Теперь непосредственно к коду.


Изначально код, который выложен ниже, выглядел вот так:
Авторская версия примера LevelLoaderExample
А я вам представляю свою, переработанную и дополненную русскими комментариями версию:
package ru.sapfil.AETest2;

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.RatioResolutionPolicy;
import org.andengine.entity.IEntity;
import org.andengine.entity.scene.Scene;
import org.andengine.entity.scene.background.Background;
import org.andengine.entity.sprite.AnimatedSprite;
import org.andengine.entity.util.FPSLogger;
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.TiledTextureRegion;
import org.andengine.opengl.vbo.VertexBufferObjectManager;
import org.andengine.ui.activity.SimpleBaseGameActivity;
import org.andengine.util.SAXUtils;
import org.andengine.util.debug.Debug;
import org.andengine.util.level.IEntityLoader;
import org.andengine.util.level.LevelLoader;
import org.andengine.util.level.constants.LevelConstants;
import org.xml.sax.Attributes;

import android.widget.Toast;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga
 *
 * @author Nicolas Gramlich
 * @since 17:16:10 - 11.10.2010
 */

// Данный файл отредактирован Sapfil-ом, многое убрано, что-то добавлено. 
// Но я решил оставить предыдущий коммент с исходным авторством.
// Комментарии буду добавлять только в места, напрямую касающиеся парсинга.

// Данный код представляет из себя самостоятельную маленькую программу,
// загружающую на экран несколько объектов из XML-базы данных 
// Для работы данного кода необходимо:
// 1. Добавить в папку assets подпапочку gfx, в которую нужно положить файлы
// "face_box_tiled.png" и "face_circle_tiled.png"
// 2. Добавить в папку assets подпапочку level, в которую нужно положить файл
// example.lvl

public class LevelLoaderExample extends SimpleBaseGameActivity {
 // ===========================================================
 // Constants
 // ===========================================================
 private static final int CAMERA_WIDTH = 480;
 private static final int CAMERA_HEIGHT = 320;
 // ===========================================================
 // Fields
 // ===========================================================
 private BitmapTextureAtlas mBitmapTextureAtlas;
 private TiledTextureRegion mBoxFaceTextureRegion;
 private TiledTextureRegion mCircleFaceTextureRegion;
 // ===========================================================
 // Methods for/from SuperClass/Interfaces
 // ===========================================================
 public EngineOptions onCreateEngineOptions() {
  final Camera camera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
  return new EngineOptions(true, ScreenOrientation.LANDSCAPE_FIXED,
            new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), camera);
 } 
 public void onCreateResources() {
  BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
  this.mBitmapTextureAtlas = new BitmapTextureAtlas(this.getTextureManager(), 64, 128,
            TextureOptions.BILINEAR);
  this.mBoxFaceTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(
           this.mBitmapTextureAtlas, this, "face_box_tiled.png", 0, 0, 2, 1); // 64x32
  this.mCircleFaceTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(
           this.mBitmapTextureAtlas, this, "face_circle_tiled.png", 0, 32, 2, 1); // 64x32
  this.mBitmapTextureAtlas.load();
 }
 public Scene onCreateScene() {
  this.mEngine.registerUpdateHandler(new FPSLogger());
  final Scene scene = new Scene();
  scene.setBackground(new Background(0, 0, 0));

  // объявляем загрузчик уровня, указываем базовый путь к ассетам.
  // то есть в папке assets/level/ будет лежать наш файл с базой данных 
  final LevelLoader levelLoader = new LevelLoader();
  levelLoader.setAssetBasePath("level/");

  // в данном блоке мы говорим парсеру - что ему делать, если ему встретится тэг "level"
  levelLoader.registerEntityLoader("level", new IEntityLoader() {
   // каждый загружаемый объект должен соответствовать интерфейсу IEntity,
   // ведь именно такого типа данные выдаются процедурой onLoadEntity.
   public IEntity onLoadEntity(final String pEntityName, final Attributes pAttributes) {
    // у объекта level в базе данных есть 2 атрибута
    // берем значение атрибута по тэгу "width" 
    // и присваиваем его некой внутренней переменной width 
    final int width = SAXUtils.getIntAttributeOrThrow(pAttributes, "width");
    // так же берем из базы данный значение для "height"
    final int height = SAXUtils.getIntAttributeOrThrow(pAttributes, "height");
    // вот это место осталось для меня загадкой. 
    // зачем мы загрузили ширину и высоту, но не использовали их в return...
    // После выполнения return у нас объект level из базы данных будет соответствовать 
    // объекту scene в нашей программе. И дочерние объекты из базы данных
    // будут подключаться методом attachChild() к нашей scene.
    return scene;
   }
  });
  
  // в данном блоке мы говорим парсеру - что ему делать, если ему встретится тэг "entity"
  levelLoader.registerEntityLoader("entity", new IEntityLoader() {
   public IEntity onLoadEntity(final String pEntityName, final Attributes pAttributes) {
    // у объекта entity есть 5 параметров
    // про каждый параметр все расписано - что делать с ним
    final int x = SAXUtils.getIntAttributeOrThrow(pAttributes, "x");
    final int y = SAXUtils.getIntAttributeOrThrow(pAttributes, "y");
    final int width = SAXUtils.getIntAttributeOrThrow(pAttributes, "width");
    final int height = SAXUtils.getIntAttributeOrThrow(pAttributes, "height");
    final String type = SAXUtils.getAttributeOrThrow(pAttributes, "type");
    
    final VertexBufferObjectManager vertexBufferObjectManager = 
                    LevelLoaderExample.this.getVertexBufferObjectManager();
    // у нас загружены все параметры - можно уже создавать объект
    // изначально в примере было больше объектов, но я оставил 2 для экономии места
    final AnimatedSprite face;
    // лично я данную конструкцию бы соорудил на основе switch
    if(type.equals("box")) {
     face = new AnimatedSprite(x, y, width, height, 
                     LevelLoaderExample.this.mBoxFaceTextureRegion, vertexBufferObjectManager);
    } else if(type.equals("circle")) {
     face = new AnimatedSprite(x, y, width, height, 
                     LevelLoaderExample.this.mCircleFaceTextureRegion, vertexBufferObjectManager);
    } else {
     throw new IllegalArgumentException();
    }
    face.animate(200);
    // вообще после завершения функция onLoadEntity выдает полученный объект
    // LevelLoader-у, а тот в свою очередь, подключает его к родителю.
    // При этом соблюдается та же структура, что и в базе данных.
    // В нашем примере объекты entity в базе данных являются вложенными, 
    // дочерними объектами по отношению к объекту level. 
    // И именно так они и будут подключаться в коде методом attachChild()
    // А объекту из БД level соответствует объект кода scene. 
    return face;
   }
  });

  // Выше мы описали все объекты из базы данных, теперь попробуем загрузить
  // нашу БД. Учтем, что мы описали лишь правила. 
  // И они будут применяться столько раз, сколько потребуется.
  // В нашем примере в БД есть много объектов Entity, каждый из них будет обработан,
  // согласно правилу, описанному нами для объектов с тэгом "entity"
  //
  // Пусть расширение lvl не вводит вас в заблуждение.
  // На самом деле это файл формата XML. Расширение можете делать какое угодно.
  try {
   levelLoader.loadLevelFromAsset(this.getAssets(), "example.lvl");
  } catch (final IOException e) {
   Debug.e(e);
  }
  // Кроме того, надо помнить, что в коде должны быть описаны правила для ВСЕХ
  // объектов, встречающихся в базе данных. 
  return scene;
 }
}
Вот так выглядят файлы -картинки для данного кода:
 А вот пример базы данных, соответствующей такому коду.
Как видите - есть родительский элемент level и он в итоге будет соответствовать scene. А элементы entity являются дочерними по отношению к level и после выполнения кода они подключатся к scene методом attachChild().


 
 
 
 
 
 
 

А авторская версия данного файла выглядит ТАК:

Теперь. как и обещал, я расскажу о том, почему мне не нравится этот парсер. Дело в том, что всё, ВСЁ в движке завязано на IEntity. И в парсере сделано так, что все объекты базы данных в итоге должны принять обличие объекта, унаследованного от IEntity. Да, конечно хорошо, если эти объекты являются спрайтами или партикл-системами. А если нужные нам объекты не совсем вписываются в концепцию Entity? Приведу примеры из своего C-шного проекта.
Пример 1. Собственно сам уровень выглядит вот так.

 
  
  
  


У каждого объекта есть параметр time - время от начала уровня, в которое объект должен появится в системе. id- вид объекта. FlyProgram и Shift задавали траекторию движения, shoot program - как объект стреляет. Ну и dropProgram - что вываливается из объекта, когда он помирает.
Попробуем "прикостылить" мой файл к парсеру AndEngine. Каждый объект сразу создастся, будет добавлен в сцену. ТО есть ВСЕ 500 ОБЪЕКТОВ сразу будут добавлены в сцену. А мне этого не надо. Мне надо, чтобы объекты создавались в то время, которое указано в базе данных. И чтобы удалялись из системы каждый в свое время. А если все 500 будут висеть в системе - система будет сильно загружена. Да. можно каждому из них при загрузке ставить setIgnoreUpdate(false), а в нужный момент включать setIgnoreUpdate(true). Не знаю, кому как, а мне такой подход не нравится. Я бы сделал по другому - загрузил эти данные некий массив, а в нужный момент брал данные из него и по ним уже создавал объекты и аттачил их к сцене.

Пример 2. В базе данных кроме прочего были вот такие данные:
 
  
  
  
  
  
  
  
    

Это программы стрельбы. В предыдущем примере вы видели атрибуты shootProgram - вот в них было указано, какую программу стрельбы использовать. А здесь собственно расписаны все эти программы. По сути они представляют лишь временные отрезки - промежутки между залпами. Смотрите например 23ю программу. Через 1.5 секунды после появления объект стреляет, а затем еще через 0.3 секунды и еще и еще. Очередь из 4х выстрелов. Теперь попробуем втиснуть это в парсер движка. То есть мы создадим некую сущность, имеющую много лишнего, а кроме того имеющую небольшой "отросток", хранящий наши данные. Это как если бы нам надо было записывать номера телефонов, а мы каждый раз покупали бы новый рояль и не его крышке записывали один телефон.

Пример 3. В моей базе данных были разные родительские объекты, а вложенные часто назывались "object". Ну вы видели уже в первых двух листингах. И как же поведет себя парсер, работая с такой базой данных? Получается что для корректной работы мне надо переделывать всю базу данных - каждой группе дочерних объектов давать уникальные имена, чтобы парсер их не путал.

Кому интересно - вот так выглядела вся база данных от игры. В ней есть комментарии, так что разобраться несложно.
http://www.everfall.com/paste/id.php?v44l7p10o441

Вот такие мои мысли по поводу всего этого.

Теперь я бы хотел поделиться пока временным, жутко кривым костылем, который я собрал среди ночи. Вот так он выглядит:
// обманем парсер :)
mDataBase.registerEntityLoader("object", new IEntityLoader() {
   public IEntity onLoadEntity(final String pEntityName, final Attributes pAttributes) {
     //загружаем наши данные из БД
    final int x = SAXUtils.getIntAttributeOrThrow(pAttributes, "x");
    final int y = SAXUtils.getIntAttributeOrThrow(pAttributes, "y");
    final int time = SAXUtils.getIntAttributeOrThrow(pAttributes, "time");
// полученные данные используем туда, куда нам надо.   
    TimeList.add(new DataNode(x, y, time));
    count++;
// ну а на выход выдаем Null. Если на выходе null, то ничего в сцену не аттачится.   
    return null;
   }
  });
// П.С. TimeList и DataNode я не расписываю, думаю это не важно
// Хотя если кому-то почему-то будет важно - отпишитесь в коментах
Однако, такой костыль мне не нравится по двум причинам.
Во-первых он является костылем, а костыль - это всегда плохо.
Во-вторых он не решает проблему, связанную с тем, что в разных родительских объектах могут находиться "тезки" - объекты с одинаковыми именами.

Все это приводит меня к мысли "расковырять" движковый парсер до основания - до самого SAX и на основе его собрать свой парсер. Когда и Если я это сделаю - я с вами обязательно поделюсь.

Комментариев нет:

Отправить комментарий