Маленький отважный арканоид (часть 2 — YAML)

  • Tutorial
Продолжая рассказ про наш маленький (но очень отважный) arcanoid, я не могу не упомянуть о таком замечательном языке как YAML. Любая, даже самая простая, игра должна хранить массу данных, таких как: описание уровней, текущее состояние настроек, список достижений и т.п. Желательно, чтобы все это хранилось в понятном человеку и легко редактируемом виде. Традиционно, для этих целей используется XML, но он весьма многословен и его вряд-ли можно считать удобным для ручного редактирования.

YAML существенно лаконичнее, и сегодня, мы научимся им пользоваться.

Для начала, определимся, зачем нам YAML. Я считаю, что, помимо всего прочего, в нем будет удобно хранить описание уровней, например, вот в таком виде:

level.json
{
  board: { width: 320 },
  types: { odd:  { inner_color: 0xffffff00,
                   outer_color: 0xff50ff00,
                   width: 40,
                   height: 20
                 },
           even: { inner_color: 0xff1010ff,
                   outer_color: 0xffffffff,
                   width: 40,
                   height: 20
                 }
         },
  level: [
           { type: odd,
             x: 50,
             y: 30
           },
           { type: even,
             x: 94,
             y: 30
           },
           { type: odd,
             x: 138,
             y: 30
           },
           { type: even,
             x: 182,
             y: 30
           },
           { type: odd,
             x: 226,
             y: 30
           },
           { type: even,
             x: 270,
             y: 30
           },
           { type: even,
             x: 50,
             y: 54
           },
           { type: odd,
             x: 94,
             y: 54
           },
           { type: even,
             x: 138,
             y: 54
           },
           { type: odd,
             x: 182,
             y: 54
           },
           { type: even,
             x: 226,
             y: 54
           },
           { type: odd,
             x: 270,
             y: 54
           },
           { type: odd,
             x: 50,
             y: 78
           },
           { type: even,
             x: 94,
             y: 78
           },
           { type: odd,
             x: 138,
             y: 78
           },
           { type: even,
             x: 182,
             y: 78
           },
           { type: odd,
             x: 226,
             y: 78
           },
           { type: even,
             x: 270,
             y: 78
           },
           { type: even,
             x: 50,
             y: 102
           },
           { type: odd,
             x: 94,
             y: 102
           },
           { type: even,
             x: 138,
             y: 102
           },
           { type: odd,
             x: 182,
             y: 102
           },
           { type: even,
             x: 226,
             y: 102
           },
           { type: odd,
             x: 270,
             y: 102
           }
         ]
}


Только не надо гневно кричать «нас обманули!». Да, это JSON. Хорошая новость заключается в том, что это и YAML тоже. Просто напросто JSON является подмножеством YAML и любое JSON описание должно быть без проблем разобрано YAML-парсером. JSON чуть более синтаксически строг и чуть менее лаконичен (но все равно гораздо лаконичнее чем XML).

Как совершенно справедливо заметил shokoladko, в комментариях ниже, YAML-вариант будет существенно короче и более удобен для редактирования (за счет отсутствия запятых, разделяющих элементы списка). Вот как он будет выглядеть:

level.yaml
board:
  width: 320
types:
  odd:
    inner_color: 0xffffff00
    outer_color: 0xff50ff00
    width: 40
    height: 20
  even:
    inner_color: 0xff1010ff
    outer_color: 0xffffffff
    width: 40
    height: 20
level:
  - type: odd
    x: 50
    y: 30
  - type: even
    x: 94
    y: 30
  - type: odd
    x: 138
    y: 30
  - type: even
    x: 182
    y: 30
  - type: odd
    x: 226
    y: 30
  - type: even
    x: 270
    y: 30
  - type: even
    x: 50
    y: 54
  - type: odd
    x: 94
    y: 54
  - type: even
    x: 138
    y: 54
  - type: odd
    x: 182
    y: 54
  - type: even
    x: 226
    y: 54
  - type: odd
    x: 270
    y: 54
  - type: odd
    x: 50
    y: 78
  - type: even
    x: 94
    y: 78
  - type: odd
    x: 138
    y: 78
  - type: even
    x: 182
    y: 78
  - type: odd
    x: 226
    y: 78
  - type: even
    x: 270
    y: 78
  - type: even
    x: 50
    y: 102
  - type: odd
    x: 94
    y: 102
  - type: even
    x: 138
    y: 102
  - type: odd
    x: 182
    y: 102
  - type: even
    x: 226
    y: 102
  - type: odd
    x: 270
    y: 102


Кроме того, YAML поддерживает реляционные данные. С помощью символа '&' в описании может быть определен «якорь», который впоследствии может быть использован для выполнения подстановок, осуществляемых «алиасами» (символ '*'). Таким образом могут быть выражены рекурсивные структуры.

Но, довольно теории, перейдем к делу. Найдем в Интернете любую библиотеку для разбора YAML и попытаемся встроить ее в наш проект. К слову сказать, выбранная нами библиотека разработана Кириллом Симоновым и свободно распространяется по MIT license (о чем можно прочитать в разделе Copyright страницы с описанием библиотеки).

Мы могли бы просто включить все необходимые файлы в mkb-файл Marmalade-проекта, но это будет не очень удобно. Я предлагаю оформить библиотеку в виде подпроекта Marmalade, благо примеров такого оформления в поставке Maramalade предостаточно. Создаем папку «yaml» и размещаем в ней mkf-файл следующего содержания:

yaml.mkf
includepath h
includepath source

files
{
    (h)
    yaml.h
    config.h

    (source)
    yaml_private.h
    api.c
    dumper.c
    emitter.c
    loader.c
    parser.c
    reader.c
    scanner.c
    writer.c
}


Создаем подкаталоги и размещаем в них исходные тексты библиотеки в соответствии с описанием их размещения в mkf-файле. На этом все. Мы создали полноценный Marmalade-модуль, который легко можем использовать в любом из наших проектов.

Сделаем это:

arcanoid.mkb
#!/usr/bin/env mkb
options
{
	module_path="../yaml"
}
subprojects
{
	iwgl
	yaml
}
includepath
{
	./source/Main
	./source/Model
}
files
{
	[Main]
	(source/Main)
	Main.cpp
	Main.h
	Quads.cpp
	Quads.h       
	Desktop.cpp
	Desktop.h
	IO.cpp
	IO.h

	[Model]
	(source/Model)
	Board.cpp
	Board.h
	Bricks.cpp
	Bricks.h
	Ball.cpp
	Ball.h

	[Data]
	(data)
}
assets
{
	(data)
	level.json

	(data-ram/data-gles1, data/data-gles1)
}


Теперь, модуль YAML подключен к проекту и нам осталось научиться обрабатывать получаемые от него данные. Достаточно внести ряд изменений в Board:

Board.h
#ifndef _BOARD_H_
#define _BOARD_H_

#include <yaml.h>
#include <vector>
#include <String>

#include "Bricks.h"
#include "Ball.h"

#define MAX_NAME_SZ   100

using namespace std;

enum EBrickMask {
    ebmX            = 0x01,
    ebmY            = 0x02,
    ebmComplete     = 0x03,
    ebmWidth        = 0x04,
    ebmHeight       = 0x08,
    ebmIColor       = 0x10,
    ebmOColor       = 0x20
};

class Board {
    private:
        struct Type {
            Type(const char* s, const char* n, const char* v): s(s), n(n), v(v) {}
            Type(const Type& p): s(p.s), n(p.n), v(p.v) {}
            string s, n, v;
        };
        Bricks bricks;
        Ball ball;
        yaml_parser_t parser;
        yaml_event_t event;
        vector<string> scopes;
        vector<Type> types;
        char currName[MAX_NAME_SZ];
        int  brickMask;
        int  brickX, brickY, brickW, brickH, brickIC, brickOC;
        bool isTypeScope;
        void load();
        void clear();
        void notify();
        const char* getScopeName();
        void setProperty(const char* scope, const char* name, const char* value);
        void closeTag(const char* scope);
        int  fromNum(const char* s);
    public:
        Board(): scopes(), types() {}
        void init();
        void refresh();
		void update() {}

    typedef vector<string>::iterator SIter;
    typedef vector<Type>::iterator TIter;
};

#endif	// _BOARD_H_


Board.cpp
#include "Board.h"
#include "Desktop.h"

const char* BOARD_SCOPE      = "board";
const char* LEVEL_SCOPE      = "level";
const char* TYPE_SCOPE       = "types";

const char* TYPE_PROPERTY    = "type";
const char* WIDTH_PROPERTY   = "width";
const char* HEIGHT_PROPERTY  = "height";
const char* IC_PROPERTY      = "inner_color";
const char* OC_PROPERTY      = "outer_color";
const char* X_PROPERTY       = "x";
const char* Y_PROPERTY       = "y";

void Board::init() {
    load();
    ball.init();
}

void Board::clear() {
    bricks.clear();
    scopes.clear();
    memset(currName, 0, sizeof(currName));
    types.clear();
}

void Board::load() {
    clear();
    yaml_parser_initialize(&parser);
    FILE *input = fopen("level.json", "rb");
    yaml_parser_set_input_file(&parser, input);
    int done = 0;
    while (!done) {
        if (!yaml_parser_parse(&parser, &event)) {
            break;
        }
        notify();
        done = (event.type == YAML_STREAM_END_EVENT);
        yaml_event_delete(&event);
    }
    yaml_parser_delete(&parser);
    fclose(input);
}

void Board::notify() {
    switch (event.type) {
        case YAML_MAPPING_START_EVENT:
        case YAML_SEQUENCE_START_EVENT:
            scopes.push_back(currName);
            memset(currName, 0, sizeof(currName));
            break;           
        case YAML_MAPPING_END_EVENT:
            closeTag(getScopeName());
        case YAML_SEQUENCE_END_EVENT:
            scopes.pop_back();
            break;
        case YAML_SCALAR_EVENT:
            if (currName[0] == 0) {
                strncpy(currName, 
                            (const char*)event.data.scalar.value, 
                            sizeof(currName)-1);
                break;
            }
            setProperty(getScopeName(), 
                               currName, 
                               (const char*)event.data.scalar.value);
            memset(currName, 0, sizeof(currName));
            break; 
        default:
            break;
    }
}

const char* Board::getScopeName() {
    const char* r = NULL;
    isTypeScope = false;
    for (SIter p = scopes.begin(); p !=scopes.end(); ++p) {
         if (!(*p).empty()) {
             if (strcmp((*p).c_str(), TYPE_SCOPE) == 0) {
                isTypeScope = true;
                continue;
             }
             r = (*p).c_str();
         }
    }
    return r;
}

int Board::fromNum(const char* s) {
    int r = 0;
    int x = 10;
    for (size_t i = 0; i < strlen(s); i++) {
        switch (s[i]) {
            case 'x':
            case 'X':
                x = 16;
                break;
            case 'a':
            case 'b':
            case 'c':
            case 'd':
            case 'e':
            case 'f':
                x = 16;
                r *= x;
                r += s[i] - 'a' + 10;
                break;
            case 'A':
            case 'B':
            case 'C':
            case 'D':
            case 'E':
            case 'F':
                x = 16;
                r *= x;
                r += s[i] - 'A' + 10;
                break;
            default:
                r *= x;
                r += s[i] - '0';
                break;
        }
    }
    return r;
}

void Board::setProperty(const char* scope, const char* name, const char* value) {
    if (scope == NULL) return;
    if (isTypeScope) {
        types.push_back(Type(scope, name, value));
        return;
    }
    if (strcmp(scope, BOARD_SCOPE) == 0) {
        if (strcmp(name, WIDTH_PROPERTY) == 0) {
            desktop.setVSize(fromNum(value));
        }
    }
    if (strcmp(scope, LEVEL_SCOPE) == 0) {
        if (strcmp(name, TYPE_PROPERTY) == 0) {
            for (TIter p = types.begin(); p != types.end(); ++p) {
                 if (strcmp(value, p->s.c_str()) == 0) {
                    setProperty(scope, p->n.c_str(), p->v.c_str());
                 }
            }
        }
        if (strcmp(name, X_PROPERTY) == 0) {
            brickMask |= ebmX;
            brickX = fromNum(value);
        }
        if (strcmp(name, Y_PROPERTY) == 0) {
            brickMask |= ebmY;
            brickY = fromNum(value);
        }
        if (strcmp(name, WIDTH_PROPERTY) == 0) {
            brickMask |= ebmWidth;
            brickW = fromNum(value);
        }
        if (strcmp(name, HEIGHT_PROPERTY) == 0) {
            brickMask |= ebmHeight;
            brickH = fromNum(value);
        }
        if (strcmp(name, IC_PROPERTY) == 0) {
            brickMask |= ebmIColor;
            brickIC = fromNum(value);
        }
        if (strcmp(name, OC_PROPERTY) == 0) {
            brickMask |= ebmOColor;
            brickOC = fromNum(value);
        }
    }
}

void Board::closeTag(const char* scope) {
    if (scope == NULL) return;
    if (strcmp(scope, LEVEL_SCOPE) == 0) {
        if ((brickMask & ebmComplete) == ebmComplete) {
            Bricks::SBrick b(desktop.toRSize(brickX), desktop.toRSize(brickY));
            if ((brickMask & ebmWidth) != 0) {
                b.hw = desktop.toRSize(brickW) / 2;
            }
            if ((brickMask & ebmHeight) != 0) {
                b.hh = desktop.toRSize(brickH) / 2;
            }
            if ((brickMask & ebmIColor) != 0) {
                b.ic = brickIC;
            }
            if ((brickMask & ebmOColor) != 0) {
                b.oc = brickOC;
            }
            bricks.add(b);
        }
        brickMask = 0;
    }
}

void Board::refresh() {
    bricks.refresh();
    ball.refresh();
}


Как все это работает? Файл с описанием уровня читается в методе load. После этого, мы вызываем функцию разбора yaml_parser_parse в цикле, анализируя возникающие события разбора. Анализ этот довольно примитивен. Некоторое оживление вносит лишь обработка содержимого секции «types». В ней мы описываем «шаблоны» настроек, которые впоследсвии сможем добавлять в описание «кирпичей», добавляя имя соответвующего типа в качестве значения атрибута «type».

В разделе «board» мы описываем ширину доски. Все остальные размеры, в описании уровня, определяются относительно нее. Обращаю ваше внимание на то, что нам не требуется определять высоту доски в описании уровня. Размеры по вертикали перерассчитываются в том же соотношении, что и размеры по горизонтали. Таким образом, мы добиваемся того, чтобы уровень выглядел практически одинаково на устройствах с различным соотношением ширины и высоты экрана (разница «теряется» в пустой области, имеющейся на любом уровне).

Запустив программу на выполнение, мы увидим, что наши данные успешно загрузились:

image

Осталось заметить, что возможности LibYAML не ограничиваются разбором YAML-файлов. С помощью нее мы можем формировать YAML-файлы сами, сохраняя в них, например, текущее состояние игровых настроек. Пример того как это делается имеется на странице с описанием библиотеки. Сохранять файлы в файловой системе устройства нам поможет настройка DataDirIsRAM:

[S3E]
SysGlesVersion=1
DispFixRot=FixedPortrait
DataDirIsRAM=1

На этом все. Модуль для работы с YAML выложен на GitHub.

В следующей статье мы научимся работать с Box2D.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 12

    +2
    Я когда-то для этих целей написал библиотеку и тулзу позволяющую конвертировать YAML в бинарный файл, а потом получать к этому доступ в рантайме. Хотя и появляется дополнительный этап конвертации данных, у такого подхода есть преимущества. Например, минимальная фрагментация памяти, т.к. все загружается одним блоком и больше никогда не меняется. Скорость загрузки тоже заметно возрастает — всего один read. В играх это все актуально. На большом количестве файлов yaml-cpp заметно тормозил. Ну и доступ был достаточно удобный:

    rodb::Database db("config.rodb");
    rodb::Value root = db.root();
    
    float x = root["ball"]["start_position"]["x"];
    float y = root["ball"]["start_position"]["y"];
    

    Код лежит тут, если интересно: https://github.com/detunized/rodb
      0
      Спасибо, дома обязательно посмотрю (на работе нет доступа к GitHub).
        0
        на работе нет доступа к GitHub
        o_O
        Вы в microsoft работаете?
          0
          Нет. Я не знаю причин по которым доступ к GitHub закрыт, а спрашивать админа как-то не с руки, поскольку для моей работы доступ туда не нужен (пока во всяком случае).
      +6
      И почему же вы не показываете нам лаконичность YAML, а вместо этого yaml-парсером парсите json? Я люблю yaml, и как-то грустно видеть такое его использование. Вот, например, лаконичная запись вашего уровня на валидном YAML.
        0
        Целиком поддерживаю, все эти скобки и непонятная вложенность в примере в статье на корню убивают «понятный человеку и легко редактируемый вид».
          0
          Согласен, критика вполне объективна. Дело в том, что мне не очень нравятся языки со значимыми отступами (но это просто мои тараканы). Что касается сравнения лаконичности YAML и JSON, тема вполне раскрыта на Wiki, каждый волен выбирать тот вариант, который ему больше нравится. Главное, что выбор этот есть.
          0
          Стоит добавить, что, в отличие от XML и JSON, YAML не поддерживается Android «из коробки», поэтому придётся подтягивать сторонние библиотеки в любом случае.
            0
            Статья больше про Marmalade чем чисто про Android. И вроде-бы я где-то в мармеладе видел JSON-парсер, но когда писал вчера статью, не нашел. Поэтому писать об этом не стал.
              0
              В целом, статья даже больше о том, как прикрутить стороннюю C++ библиотеку к Marmalade-проекту
          +1
          DispFixRot=FixedPortrait не всегда работает правильно на Анроиде. Лучше в main.cpp пропишите
          s3eSurfaceSetInt(S3E_SURFACE_DEVICE_ORIENTATION_LOCK, S3E_SURFACE_PORTRAIT_FIXED);
            0
            Спасибо, буду иметь в виду.

          Only users with full accounts can post comments. Log in, please.