Показаны сообщения с ярлыком Android. Показать все сообщения
Показаны сообщения с ярлыком Android. Показать все сообщения

воскресенье, 20 января 2013 г.

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


Продолжая рассказ про наш маленький (но очень отважный) 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-вариант будет существенно короче и более удобен для редактирования (за счет отсутствия запятых, разделяющих элементы списка). Вот как он будет выглядеть:

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

Кроме того, YAML поддерживает реляционные данные. С помощью символа '&' в описании может быть определен «якорь», который впоследствии может быть использован для выполнения подстановок, осуществляемых «алиасами» (символ '*'). Таким образом могут быть выражены рекурсивные структуры.

Но, довольно теории, перейдем к делу. Найдем в Интернете любую библиотеку для разбора 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» мы описываем ширину доски. Все остальные размеры, в описании уровня, определяются относительно нее. Обращаю ваше внимание на то, что нам не требуется определять высоту доски в описании уровня. Размеры по вертикали перерассчитываются в том же соотношении, что и размеры по горизонтали. Таким образом, мы добиваемся того, чтобы уровень выглядел практически одинаково на устройствах с различным соотношением ширины и высоты экрана (разница «теряется» в пустой области, имеющейся на любом уровне).

Запустив программу на выполнение, мы увидим, что наши данные успешно загрузились:




Осталось заметить, что возможности LibYAML не ограничиваются разбором YAML-файлов. С помощью нее мы можем формировать YAML-файлы сами, сохраняя в них, например, текущее состояние игровых настроек. Пример того как это делается имеется на странице с описанием библиотеки. Сохранять файлы в файловой системе устройства нам поможет настройка DataDirIsRAM:

app.icf:
[S3E]
SysGlesVersion=1
DispFixRot=FixedPortrait
DataDirIsRAM=1

На этом все. Модуль для работы с YAML выложен на GitHub.

В следующей статье мы научимся работать с Box2D.

воскресенье, 13 января 2013 г.

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


Как я уже говорил, описанному мной ранее framework-у не хватает очень многого, для того чтобы считаться полноценным игровым движком. В нем нет моделирования физики, он использует негибкий и не быстрый Iw2D для вывода графики. Фактически, все что он умеет делать — это выполнение 2D анимации спрайтов, сопровождаемое звуковыми эффектами. Чтобы как-то расти над собой, очевидно, необходимо осваивать новые возможности, но делать это, не имея какой-то цели, скучно и неинтересно.

Мы поставим перед собой цель, и разработаем небольшой прототип всем известной игры Arcanoid. Для начала попытаться разобраться с тем, что-же такое IwGl и как его можно использовать. Начнем с простого, поучимся рисовать треугольники. Итак, об Open GL нам известно, что он умеет рисовать треугольники. Также нам известно, что рисовать он их умеет с красивой градиентной заливкой, быстро и довольно много. Умея быстро рисовать много треугольников, можно нарисовать все что угодно.

Начнем, как обычно с mkb-файла:


#!/usr/bin/env mkb
options
{
}
subprojects
{
    iwgl
}

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)
    Bricks.cpp
    Bricks.h
    Ball.cpp
    Ball.h
    Board.cpp
    Board.h
}
assets
{
}


Здесь мы объявляем, что, для вывода графики, намерены использовать IwGl, а также определяем несколько файлов с исходным текстом, который мы намерены сегодня написать.

Модуль Main, традиционно, будет содержать главный цикл приложения, а также заниматься инициализацией и деинициализацией всех подсистем.

Main.cpp:
#include "Main.h"

#include "s3e.h"
#include "IwGL.h"

#include "Desktop.h"
#include "IO.h"
#include "Quads.h"
#include "Board.h"

Board board;

void init() {
    desktop.init();
    io.init();
    quads.init();
    board.init();
}

void release() {
    io.release();
    desktop.release();
}

int main() {
    init(); {
        while (!s3eDeviceCheckQuitRequest()) {
            io.update();
            if (io.isKeyDown(s3eKeyAbsBSK) || io.isKeyDown(s3eKeyBack)) break;
            quads.update();
            desktop.update();
            board.update();
            board.refresh();
            quads.refresh();
            io.refresh();
            desktop.refresh();
        }
    }
    release();
    return 0;
}

Я постарался спрятать весь мармеладный код, раскидав его по различным подсистемам. Самой простой из этих подсистем является модуль IO. Его задача, на сегодня, обработка состояния клавиатуры. Как я уже говорил ранее, мы должны обрабатывать нажатие кнопки «Back» для корректного завершения приложения на платформе Android.

IO.h:
#ifndef _IO_H_
#define _IO_H_

class IO {
    public:
        void init() {}
        void release() {}
        void update();
        void refresh() {}
        bool isKeyDown(s3eKey key) const;
};

extern IO io;

#endif // _IO_H_

IO.cpp:
#include "s3e.h"
#include "IO.h"

IO io;

void IO::update() {
    s3eKeyboardUpdate();
}

bool IO::isKeyDown(s3eKey key) const {
    return (s3eKeyboardGetState(key) & S3E_KEY_STATE_DOWN) == S3E_KEY_STATE_DOWN;
}

Покончив со скучной частью, переходим к модулю Desktop. Он будет заниматься собственно прорисовкой кадров IwGl, а также перерасчетом неких абстрактных координат (в которых будет работать модель) в физические, в зависимости от размеров экрана устройства, на котором мы запустили приложение.

Desktop.h: 
#ifndef _DESKTOP_H_
#define _DESKTOP_H_

class Desktop {
    private:
        int width;
        int height;
        int vSize;
        int duration;
    public:
        void init();
        void release();
        void update();
        void refresh();
        int  getWidth() const {return width;}
        int  getHeight() const {return height;}
        void setVSize(int v) {vSize = v;}
        int  toRSize(int x) const;
};

extern Desktop desktop;

#endif // _DESKTOP_H_

Desktop.cpp:
#include "IwGL.h"
#include "s3e.h"

#include "Desktop.h"

Desktop desktop;

void Desktop::init() {
    IwGLInit();
    glClearColor(0, 0, 0, 0);
    width = IwGLGetInt(IW_GL_WIDTH);
    height = IwGLGetInt(IW_GL_HEIGHT);
    vSize = 0;
    duration = 1000 / 60;
}

void Desktop::release() {
    IwGLTerminate();
}

void Desktop::update() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrthof(0, (float)width, (float)height, 0, -10.0f, 10.0f);
    glViewport(0, 0, width, height);
}

void Desktop::refresh() {
    IwGLSwapBuffers();
    s3eDeviceYield(duration);   
}

int Desktop::toRSize(int x) const {
    if (vSize == 0) return x;
    return (x * width) / vSize;
}

На код перерасчета логических координат в экранные, пока, можно не обращать внимания. Он понадобиться нам когда мы научимся загружать описание уровня.

Следует обратить внимание на то как осуществляется перерисовка кадра в методе update. Изображение первоначально строится в скрытом буфере, после чего выполняется переключение скрытого и видимого буферов вызовом IwGLSwapBuffers. Очистка экрана и настройка камеры для получения 2D изображения осуществляются в методе update (скажу сразу, что весь этот код я посмотрел в стандартном примере IwGL/IwGLVirtualRes). Из сказанного ясно, что метод update должен вызываться до начала рисования любых примитивов, а refresh по завершении, для перерисовки экрана.

Вторым моментом, подсмотренным мной в IwGLVirtualRes является способ быстрого вывода массива треугольников. Этой задачей будет заниматься модуль Quads.

Quads.h:
#ifndef _QUADS_H_
#define _QUADS_H_

#define MAX_QUADS 2000

class Quads {
    private:
        int16  Verts[MAX_QUADS * 4 * 2];
        uint16 Inds[MAX_QUADS * 6];
        uint32 Cols[MAX_QUADS * 4];
        int    outQuad;
    public:
        void init();
        void update() {outQuad = 0;}
        void refresh();
        int16* getQuadPoints();
        uint32* getQuadCols();
};

extern Quads quads;

#endif // _QUADS_H_

Quads.cpp:
#include "IwGL.h"
#include "s3e.h"

#include "Quads.h"

Quads quads;

void Quads::init() {
    uint16* inds = Inds;
    for (int n = 0; n < MAX_QUADS; n++)
    {
        uint16 baseInd = n*4;
        //Triangle 1
        *inds++ = baseInd;
        *inds++ = baseInd+1;
        *inds++ = baseInd+2;
        //Triangle 2
        *inds++ = baseInd;
        *inds++ = baseInd+2;
        *inds++ = baseInd+3;
    }
    glVertexPointer(2, GL_SHORT, 0, Verts);
    glEnableClientState(GL_VERTEX_ARRAY);
    glColorPointer(4, GL_UNSIGNED_BYTE, 0, Cols);
    glEnableClientState(GL_COLOR_ARRAY);
}

void Quads::refresh() {
    glDrawElements(GL_TRIANGLES, outQuad*6, GL_UNSIGNED_SHORT, Inds);
}

int16* Quads::getQuadPoints() {
    if (outQuad >= MAX_QUADS) return NULL;
    return Verts + 2 * 4 * outQuad;
}

uint32* Quads::getQuadCols() {
    if (outQuad >= MAX_QUADS) return NULL;
    return Cols + 4 * outQuad++;
}

Любой экранный объект, для своей прорисовки, может затребовать у модуля Quads набор пар треугольников, соответствующим образом изменив их параметры. По завершении, Quads одним вызовом glDrawElements отобразит все треугольники, которые были изменены. Таким образом модуль Bricks сможет изобразить на экране несколько прямоугольников.

Bricks.h:
#ifndef _BRICKS_H_
#define _BRICKS_H_

#include "IwGL.h"
#include "s3e.h"
#include "Desktop.h"

#define BRICK_COLOR_1      0xffffff00
#define BRICK_COLOR_2      0xff50ff00
#define BRICK_HALF_WIDTH   20
#define BRICK_HALF_HEIGHT  10

#include <vector>

using namespace std;

class Bricks {
    private:
        struct SBrick {
            SBrick(int x, int y): x(x), 
                                          y(y), 
                                          hw(BRICK_HALF_WIDTH), 
                                          hh(BRICK_HALF_HEIGHT), 
                                          ic(BRICK_COLOR_1), 
                                          oc(BRICK_COLOR_2) {}
            SBrick(const SBrick& p): x(p.x), 
                                          y(p.y), 
                                          hw(p.hw), 
                                          hh(p.hh), 
                                          ic(p.ic), 
                                          oc(p.oc) {}
            int x, y, hw, hh, ic, oc;
        };
        vector<SBrick> bricks;
    public:
        Bricks(): bricks() {}
        void refresh();
        void clear(){bricks.clear();}
        void add(SBrick& b);

    typedef vector<SBrick>::iterator BIter;
};

Bricks.cpp:
#include "Bricks.h"
#include "Quads.h"

void Bricks::refresh() {
    for (BIter p = bricks.begin(); p != bricks.end(); ++p) {
            CIwGLPoint point(p->x, p->y);
            point = IwGLTransform(point);

            int16* quadPoints = quads.getQuadPoints();
            uint32* quadCols = quads.getQuadCols();
            if ((quadPoints == NULL) || (quadCols == NULL)) break;

            *quadPoints++ = point.x - p->hw;
            *quadPoints++ = point.y + p->hh;
            *quadCols++   = p->ic;

            *quadPoints++ = point.x + p->hw;
            *quadPoints++ = point.y + p->hh;
            *quadCols++   = p->oc;

            *quadPoints++ = point.x + p->hw;
            *quadPoints++ = point.y - p->hh;
            *quadCols++   = p->ic;

            *quadPoints++ = point.x - p->hw;
            *quadPoints++ = point.y - p->hh;
            *quadCols++   = p->oc;
    }
}

void Bricks::add(SBrick& b) { 
    bricks.push_back(b);
}

Мы используем два цвета, чтобы изобразить прямоугольники с градиентной заливкой.

Теперь, когда мы справились с прямоугольниками, нам предстоит более интересная задача. Нам необходимо, используя треугольники, изобразить шар (ну хорошо, не шар, а кругляшок, с симпатичной градиетной заливкой, изображающей блик).

Ball.h:
#ifndef _BALL_H_
#define _BALL_H_

#include <vector>
#include "IwGL.h"
#include "s3e.h"
#include "Desktop.h"

#define MAX_SEGMENTS       7
#define BALL_COLOR_1       0x00000000
#define BALL_COLOR_2       0xffffffff
#define BALL_RADIUS        15

using namespace std;

class Ball {
    private:
        struct Offset {
            Offset(int dx, int dy): dx(dx), dy(dy) {}
            Offset(const Offset& p): dx(p.dx), dy(p.dy) {}
            int dx, dy;
        };
        vector<Offset> offsets;
        int     x;
        int     y;
    public:
        void init();
        void refresh();
        virtual void setXY(int X, int Y);

    typedef vector<Offset>::iterator OIter;
};

#endif // _BALL_H_

Ball.cpp:
#include "Ball.h"
#include "Quads.h"
#include "Desktop.h"
#include <math.h>

#define PI 3.14159265f

void Ball::init(){
    x = desktop.getWidth() / 2;
    y = desktop.getHeight()/ 2;

    float delta = PI / (float)MAX_SEGMENTS;
    float angle = delta / 2.0f;
    float r = (float)desktop.toRSize(BALL_RADIUS);
    for (int i = 0; i < MAX_SEGMENTS; i++) {
        offsets.push_back(Offset((int16)(cos(angle) * r), (int16)(sin(angle) * r)));
        angle = angle + delta;
        offsets.push_back(Offset((int16)(cos(angle) * r), (int16)(sin(angle) * r)));
        angle = angle + delta;
        offsets.push_back(Offset((int16)(cos(angle) * r), (int16)(sin(angle) * r)));
    }
}

void Ball::setXY(int X, int Y) {
    x = X;
    y = Y;
}

void Ball::refresh() {
    CIwGLPoint point(x, y);
    point = IwGLTransform(point);
    OIter o = offsets.begin();
    int r = desktop.toRSize(BALL_RADIUS);
    for (int i = 0; i < MAX_SEGMENTS; i++) {

            int16* quadPoints = quads.getQuadPoints();
            uint32* quadCols = quads.getQuadCols();
            if ((quadPoints == NULL) || (quadCols == NULL)) break;

            *quadPoints++ = point.x + (r / 4);
            *quadPoints++ = point.y + (r / 4);
            *quadCols++   = BALL_COLOR_2;

            *quadPoints++ = point.x + o->dx;
            *quadPoints++ = point.y + o->dy;
            *quadCols++   = BALL_COLOR_1;
            o++;

            *quadPoints++ = point.x + o->dx;
            *quadPoints++ = point.y + o->dy;
            *quadCols++   = BALL_COLOR_1;
            o++;

            *quadPoints++ = point.x + o->dx;
            *quadPoints++ = point.y + o->dy;
            *quadCols++   = BALL_COLOR_1;
            o++;
    }
}


Вспомнив курс школьной тригонометрии, разобьем окружность на сегменты. Для того, чтобы имитировать блик, сдвинем вершину всех сегментов немного вправо и вниз от центра окружности. Ну и для того, чтобы не заниматься всей этой тригонометрией при каждой перерисовке, вычислим все координаты однократно, в методе init.

Реализация оставшегося модуля Board, пока что, тривиальна.

Board.h:
#ifndef _BOARD_H_
#define _BOARD_H_

#include "Bricks.h"
#include "Ball.h"

class Board {
    private:
        Bricks bricks;
        Ball ball;
    public:
        void init();
        void refresh();
        void update() {}
};

#endif // _BOARD_H_

Board.cpp:
#include "Board.h"

void Board::init() {
    // DEBUG:
    SBrick b(200, 80);
    bricks.add(b);
    //

    ball.init();
}

void Board::refresh() {
    bricks.refresh();
    ball.refresh();
}

Осталось внести необходимые настройки в app.icf:

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

и запустить программу на выполнение:
 






В следующей статье, мы научимся загружать описание уровня из YAML-файла.

вторник, 25 декабря 2012 г.

Сделай паузу

Некоторое время назад, мной был разработан Framework, предназначенный для создания несложных 2D-игр, с использованием инструментальной платформы Marmalade, который я описал в цикле статей этого блога. Разумеется, все что я описывал в статьях было предельно упрощено для того, чтобы не увязнуть в деталях реализации, по ходу изложения. Созданному Framework-у не хватает многих возможностей без которых немыслимо его применение в реальных проектах. Сегодня я хочу рассказать о добавлении одной из них.

Речь пойдет о реализации режима паузы. Я думаю, эта возможность будет полезна практически в любой игре. При активации паузы, весь игровой процесс должен на неопределенное время замирать, а при ее снятии, продолжаться с того-же места, где он остановился. В общем-то понятно, что на время паузы, анимированные спрайты должны приостановить всю обработку в методе update, но Дьявол, как всем известно, в деталях.

Начнем с интерфейсов. В метод IObject.update добавим флаг isPaused:

class IObject {
    public:
                   ...
        virtual void update(uint64 timestamp, bool isPaused) = 0;
};

В AbstractSpriteOwner добавиться аналогичный флаг и метод, который мы будем вызывать для корректировки таймстампов анимированных объектов при завершении паузы:

class AbstractSpriteOwner: public ISpriteOwner {
    public:
                   ...
        virtual void update(uint64 timestamp, bool isPaused);
        virtual void correctPauseDelta(uint64 pauseDelta);
};


Реализация метода correctPauseDelta тривиальна. Мы просто передаем продолжительность паузы в миллисекундах всем вложенным спрайтам:

void AbstractSpriteOwner::correctPauseDelta(uint64 pauseDelta) {
    for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) {
        p->second->addPauseDelta(pauseDelta);
    }
}


В реализации AnimatedSprite мы добавляем полученную продолжительность паузы ко всем таймстампам текущих анимаций. Также мы добавляем досрочный выход из метода update в случае, если действует режим паузы:

void AnimatedSprite::addPauseDelta(uint64 pauseDelta) {
    for (CIter p = currentMessages.begin(); p != currentMessages.end(); ++p) {
        p->timestamp += pauseDelta;
    }
}

void AnimatedSprite::update(uint64 timestamp, bool isPaused) {
    if (isPaused && isPausable()) return;
         ...
}


О методе isPausable следует сказать особо. Мы должны иметь возможность разделять обычные объекты и объекты анимация которых (а также обработка ими событий) не останавливается в режиме паузы. К таким объектам, например, должно относиться меню, содержащее кнопку выхода из состояния паузы (в противном случае, включив паузу, мы никогда не сможем ее выключить). Определим реализацию метода isPausable по умолчанию в AbstractScreenObject и, при необходимости, будем переопределять ее в тех его наследниках, отключать анимацию которых не стоит:

class AbstractScreenObject: public IScreenObject {
    public:
                   ...
        virtual void addPauseDelta(uint64 pauseDelta) {}
        virtual bool isPausable() const {return true;}
};


Чтобы внести изменения в CompositeSprite, необходимо помнить, что он, с одной стороны, является контейнером спрайтов, в то время как с другой, сам является наследником AnimatedSprite и может обрабатывать правила анимации:

void CompositeSprite::addPauseDelta(uint64 pauseDelta) {
    AbstractSpriteOwner::correctPauseDelta(pauseDelta);
    AnimatedSprite::addPauseDelta(pauseDelta);
}

void CompositeSprite::update(uint64 timestamp, bool isPaused) {
    AnimatedSprite::update(timestamp, isPaused);
    AbstractSpriteOwner::update(timestamp, isPaused);
}


Основную работу по приостановке и возобновлению анимации возьмет на себя реализация Scene.

Scene.h:

class Scene: public AbstractSpriteOwner {
    protected:
                  ...
        bool IsPaused;
        uint64 pauseTimestamp;
    public:
                  ...
        virtual void update(uint64 timestamp, bool isPaused);
        bool isPaused() const {return IsPaused;}
        void suspend();
        void resume();
};

Scene.cpp:

void Scene::update(uint64 timestamp, bool) {
    if (IsPaused && (pauseTimestamp == 0)) {
        pauseTimestamp = pauseTimestamp;
    }
    if (!IsPaused && (pauseTimestamp != 0)) {
        uint64 pauseDelta = timestamp - pauseTimestamp;
        if (pauseDelta > 0) {
            correctPauseDelta(pauseDelta);
        }
        pauseTimestamp = 0;
    }
         ...
    AbstractSpriteOwner::update(timestamp, IsPaused);
}

bool Scene::sendMessage(int id, int x, int y) {
    if (AbstractSpriteOwner::sendMessage(id, x, y)) {
        return true;
    }
    if (IsPaused) return false;
    if (background != NULL) {
        return background->sendMessage(id, x, y);
    }
    return false;
}

void Scene::suspend() {
    desktop.suspend();
    IsPaused = true;
}

void Scene::resume() {
    desktop.resume();
    IsPaused = false;
}


Осталось добавить код приостановки фоновой музыки в класс Desktop.


Desktop.h:

class Desktop {
    private:
                   ...
        bool isMusicStarted;
    public:
        Desktop(): touches(), names(), currentScene(NULL), isMusicStarted(false) {}
                  ...
        void startMusic(const char* res);
        void stopMusic();
        void suspend();
        void resume();
};

Desktop.cpp:

void Desktop::startMusic(const char* res) {
    if (s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_MP3) &&
        s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_PCM))
        s3eAudioPlay(res, 0);
    isMusicStarted = true;
}

void Desktop::stopMusic() {
    isMusicStarted = false;
    s3eAudioStop();
}

void Desktop::suspend() {
    if (isMusicStarted) {
        s3eAudioPause();
    }
}

void Desktop::resume() {
    if (isMusicStarted) {
        s3eAudioResume();
    }
}


Теперь мы умеем останавливать время в нашем приложении :)

Проект на GitHub обновлен.

суббота, 10 ноября 2012 г.

Работа над ошибками

Как легко сделать ошибку. И насколько-же сложно потом ее бывает найти и исправить! Если кто-то потрудился запустить мой пример, он мог обратить внимание на его странное поведение, заметное даже на эмуляторе. Кнопки не реагируют на нажатие до тех пор, пока мы не завершим касание. После этого, корректным образом, анимируется нажатие кнопки. Если сделать анимацию посложнее, можно заметить, что в момент касания вся активность приложения приостанавливается, а как только мы отпускаем палец, все продолжается, как ни в чем не бывало.

Я не сразу это заметил, а затем еще дольше искал, в чем ошибка. Ошибка здесь:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = timestamp;
    if (isChanged) {
        getScreenSizes();
    }
    int cnt = touchPad.getTouchCount();
    if (cnt > 0) {
        for (int i = 0; i < MAX_TOUCHES; i++) {
            Touch* t = touchPad.getTouch(i);
            if (t->isActive) {
                int msg = (cnt > 1)?emtMultiTouchUp:emtSingleTouchUp;
                if (t->isMoved) {
                    msg = (cnt > 1)?emtMultiTouchMove:emtSingleTouchMove;
                }
                if (t->isPressed) {
                    msg = (cnt > 1)?emtMultiTouchDown:emtSingleTouchDown;
                }
                if (checkBounce(t->id, msg)) return;
                if (currentScene != NULL) {
                    currentScene->sendMessage(msg | (t->id & emtTouchIdMask), 
                                              t->x, t->y);
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Как всегда, все произошло из за того, что "хотелось как лучше". Дело в том, что если не предпринимать каких-то дополнительных действий, при нажатии, Touchpad (имеется в виду модуль проекта) посылает целую последовательность событий "нажатие", там где кнопке хватило бы всего одного. В результате, кнопка начинала "дребезжать", быстро нажимаясь и отжимаясь. Чтобы этого избежать, была добавлена функция checkBounce, формирующая правильную последовательность событий, убирая лишние. В процессе поиска ошибки, ее код был перепроверен, наверное, раз десять. А ошибка была совсем не там.

Вот правильный код:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = timestamp;
    if (isChanged) {
        getScreenSizes();
    }
    int cnt = touchPad.getTouchCount();
    if (cnt > 0) {
        for (int i = 0; i < MAX_TOUCHES; i++) {
            Touch* t = touchPad.getTouch(i);
            if (t->isActive) {
                int msg = (cnt > 1)?emtMultiTouchUp:emtSingleTouchUp;
                if (t->isMoved) {
                    msg = (cnt > 1)?emtMultiTouchMove:emtSingleTouchMove;
                }
                if (t->isPressed) {
                    msg = (cnt > 1)?emtMultiTouchDown:emtSingleTouchDown;
                }
                if (!checkBounce(t->id, msg)) {
                   if (currentScene != NULL) {
                       currentScene->sendMessage(msg | (t->id & emtTouchIdMask), 
                                                 t->x, t->y);
                   }
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Найдите десять отличий ;)

Для тех, кто не следил за циклом статей, как всегда, ссылка на исправленный проект.

P.S. В эти выходные закончился Evaluation период у Мармелада. Я решил его продлить, теперь уже за денежку :) Считаю, что $149 совсем не дорого за такое удовольствие.