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

от автора

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

ссылка на оригинал статьи http://habrahabr.ru/post/166357/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *