Всем привет. Если кто-то не в курсе, то вот отсюда можно скачать все примеры автора движка. Скачайте себе всю эту папочку и будет вам счастье. Сможете сами, как и я , понемногу разбираться в движке.
Ссылка на все примеры движка 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 и на основе его собрать свой парсер. Когда и Если я это сделаю - я с вами обязательно поделюсь.


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