Продолжая рассказ про наш маленький (но очень отважный) arcanoid, я не могу не упомянуть о таком замечательном языке как YAML. Любая, даже самая простая, игра должна хранить массу данных, таких как: описание уровней, текущее состояние настроек, список достижений и т.п. Желательно, чтобы все это хранилось в понятном человеку и легко редактируемом виде. Традиционно, для этих целей используется XML, но он весьма многословен и его вряд-ли можно считать удобным для ручного редактирования.
YAML существенно лаконичнее, и сегодня, мы научимся им пользоваться.
Для начала, определимся, зачем нам 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
}
]
}
Только не надо гневно кричать «нас обманули!». Да, это JSON. Хорошая новость заключается в том, что это и YAML тоже. Просто напросто JSON является подмножеством YAML и любое JSON описание должно быть без проблем разобрано YAML-парсером. JSON чуть более синтаксически строг и чуть менее лаконичен (но все равно гораздо лаконичнее чем XML).
Довольно теории, перейдем к делу. Найдем в Интернете любую библиотеку для разбора YAML и попытаемся встроить ее в наш проект. К слову сказать, выбранная нами библиотека разработана Кириллом Симоновым и свободно распространяется по MIT license (о чем можно прочитать в разделе Copyright страницы с описанием библиотеки).
Мы могли бы просто включить все необходимые файлы в mkb-файл Marmalade-проекта, но это будет не очень удобно. Я предлагаю оформить библиотеку в виде подпроекта Marmalade, благо примеров такого оформления в поставке Maramalade предостаточно. Создаем папку «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-модуль, который легко можем использовать в любом из наших проектов.
Сделаем это:
#!/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:
#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_
#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» мы описываем ширину доски. Все остальные размеры, в описании уровня, определяются относительно нее. Обращаю ваше внимание на то, что нам не требуется определять высоту доски в описании уровня. Размеры по вертикали перерассчитываются в том же соотношении, что и размеры по горизонтали. Таким образом, мы добиваемся того, чтобы уровень выглядел практически одинаково на устройствах с различным соотношением ширины и высоты экрана (разница «теряется» в пустой области, имеющейся на любом уровне).
Запустив программу на выполнение, мы увидим, что наши данные прекрасно загрузились:
Осталось заметить, что возможности LibYAML не ограничиваются разбором YAML-файлов. С помощью нее мы можем формировать YAML-файлы сами, сохраняя в них, например, текущее состояние игровых настроек. Пример того как это делается имеется на странице с описанием библиотеки. Сохранять файлы в файловой системе устройства нам поможет настройка DataDirIsRAM:
[S3E]
SysGlesVersion=1
DispFixRot=FixedPortrait
DataDirIsRAM=1
На этом все. Модуль для работы с YAML выложен на GitHub.
В следующей статье мы научимся работать с Box2D.
Автор: GlukKazan