📜 ⬆️ ⬇️

Little Brave Arkanoid (Part 2 - YAML)

Continuing the story about our small (but very brave) arcanoid , I can not fail to mention such a wonderful language as YAML . Any, even the simplest, game should store a lot of data, such as: description of levels, current status of settings, list of achievements, etc. It is desirable that all this is stored in a human-readable and easily editable form. Traditionally, XML is used for these purposes, but it is quite verbose and can hardly be considered convenient for manual editing.

YAML is significantly more concise, and today, we will learn how to use it.

For a start, let's decide why we need YAML. I believe that, among other things, it will be convenient to store a description of the levels in it, for example, in this form:

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 } ] } 


Just do not cry angrily "we were deceived!". Yes, this is JSON . The good news is that this is also YAML. Simply JSON is a subset of YAML and any JSON description should be parsed without any problems by the YAML parser. JSON is slightly more syntactically strict and slightly less concise (but still much more concise than XML).
')
As shokoladko quite rightly noted, in the comments below, the YAML variant will be significantly shorter and more convenient for editing (due to the absence of commas separating the list items). Here is what it will look like:

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 


In addition, YAML supports relational data. Using the '&' symbol in the description, an “anchor” can be defined, which can later be used to perform substitutions made by “aliases” (the '*' character). In this way, recursive structures can be expressed.

But enough theory get to the point. We will find any YAML library on the Internet and try to embed it in our project. By the way, the library we chose was developed by Kirill Simonov and is freely distributed under the MIT license (which can be found in the section on the Copyright page describing the library).

We could just include all the necessary files in the Marmalade project's mkb file, but this will not be very convenient. I propose to arrange the library in the form of a subproject of Marmalade, the benefit of examples of such design in the supply of Maramalade abound. Create the folder “yaml” and place in it the mkf-file of the following content:

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 } 


We create subdirectories and place the source texts of the library in them in accordance with the description of their placement in the mkf file. That's all. We have created a full-fledged Marmalade module, which we can easily use in any of our projects.

Let's do it:

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) } 


Now, the YAML module is connected to the project and it remains for us to learn how to process the data received from it. It is enough to make a number of changes in the 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(ps), n(pn), v(pv) {} 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(); } 


How does all this work? The file with the description of the level is readable in the load method. After that, we call the parse function yaml_parser_parse in a loop, analyzing the parsing events that occur. This analysis is rather primitive. Some revival is made only by processing the contents of the "types" section. In it, we describe the “templates” of settings that we can later add to the “bricks” description by adding the name of the corresponding type as the value of the “type” attribute.

In the "board" section we describe the width of the board. All other dimensions in the level description are relative to it. I draw your attention to the fact that we do not need to determine the height of the board in the description of the level. Vertical dimensions are recalculated in the same ratio as horizontal dimensions. Thus, we ensure that the level looks almost the same on devices with different ratios of the width and height of the screen (the difference is “lost” in an empty area that exists at any level).

Having started the program for execution, we will see that our data has successfully loaded:

image

It remains to be noted that the capabilities of LibYAML are not limited to parsing YAML files. With the help of it, we can form YAML files ourselves, saving in them, for example, the current state of game settings. An example of how this is done is available on the library description page . DataDirIsRAM setting will help us to save files in the device file system:

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

That's all. The module for working with YAML is posted on GitHub .

In the next article we will learn how to work with Box2D.

Source: https://habr.com/ru/post/166357/


All Articles