Little Brave Arkanoid (Part 2 - YAML)

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

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

To begin with, we will 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 angrily shout "they deceived us!". Yes, this is JSON . The good news is that this is YAML too. It’s just that JSON is a subset of YAML and any JSON description should be parsed without a problem by a YAML parser. JSON is a little more syntactically strong and a little less concise (but still much more concise than XML).

As shokoladko rightly remarked , in the comments below, the YAML version will be significantly shorter and more convenient for editing (due to the absence of commas separating the list items). Here's 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 '&' character in the description, an “anchor” can be defined, which can subsequently be used to perform substitutions performed by “aliases” (the '*' character). In this way, recursive structures can be expressed.

But enough theory, let's get down to business. We find on the Internet any library for parsing YAML 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 Copyright section of the library description page).

We could simply include all the necessary files in mkb-file Marmalade-project, but it will not be very convenient. I suggest designing the library as a subproject of Marmalade, since there are plenty of examples of such design in the Maramalade package. We create the yaml folder and place the mkf file of the following content in it:

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 we need to learn how to process the data received from it. It is enough to make a number of changes to 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   100usingnamespacestd;
enum EBrickMask {
    ebmX            = 0x01,
    ebmY            = 0x02,
    ebmComplete     = 0x03,
    ebmWidth        = 0x04,
    ebmHeight       = 0x08,
    ebmIColor       = 0x10,
    ebmOColor       = 0x20
};
classBoard {private:
        structType {
            Type(constchar* s, constchar* n, constchar* 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;
        voidload();
        voidclear();
        voidnotify();
        constchar* getScopeName();
        voidsetProperty(constchar* scope, constchar* name, constchar* value);
        voidcloseTag(constchar* scope);
        intfromNum(constchar* s);
    public:
        Board(): scopes(), types() {}
        voidinit();
        voidrefresh();
		voidupdate(){}
    typedefvector<string>::iterator SIter;
    typedefvector<Type>::iterator TIter;
};
#endif// _BOARD_H_


Board.cpp
#include"Board.h"#include"Desktop.h"constchar* BOARD_SCOPE      = "board";
constchar* LEVEL_SCOPE      = "level";
constchar* TYPE_SCOPE       = "types";
constchar* TYPE_PROPERTY    = "type";
constchar* WIDTH_PROPERTY   = "width";
constchar* HEIGHT_PROPERTY  = "height";
constchar* IC_PROPERTY      = "inner_color";
constchar* OC_PROPERTY      = "outer_color";
constchar* X_PROPERTY       = "x";
constchar* 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, 
                            (constchar*)event.data.scalar.value, 
                            sizeof(currName)-1);
                break;
            }
            setProperty(getScopeName(), 
                               currName, 
                               (constchar*)event.data.scalar.value);
            memset(currName, 0, sizeof(currName));
            break; 
        default:
            break;
    }
}
constchar* Board::getScopeName() {
    constchar* 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(constchar* 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(constchar* scope, constchar* name, constchar* 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(constchar* 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 it all work? The file with the level description is read in the load method. After that, we call the parser function yaml_parser_parse in a loop, analyzing the parsing events that occur. This analysis is rather primitive. Some revival is introduced only by processing the contents of the “types” section. In it, we describe the “templates” of settings, which later we can add to the description of “bricks” 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 sizes, in the description of the level, are defined relative to it. I draw your attention to the fact that we do not need to determine the height of the board in the level description. The vertical dimensions are recalculated in the same ratio as the 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 the empty area available at any level).

Running the program on execution, we will see that our data successfully loaded:

image

It remains to note that the capabilities of LibYAML are not limited to parsing YAML files. With it, we can create YAML files ourselves, saving in them, for example, the current state of the game settings. An example of how this is done is available on the library description page . Setting DataDirIsRAM will help us save files in the device’s 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.

Also popular now: