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