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

в 14:57, , рубрики: android, iOS, json, marmalade, yaml, Разработка под android, разработка под iOS, метки: , , , ,

Продолжая рассказ про наш маленький (но очень отважный) 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).

Довольно теории, перейдем к делу. Найдем в Интернете любую библиотеку для разбора 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.

Автор: GlukKazan

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js