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

в 18:20, , рубрики: android, iOS, marmalade, opengl es, Разработка под android, разработка под iOS, метки: , , ,

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

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

Итак, об Open GL нам известно, что он умеет рисовать треугольники. Также нам известно, что рисовать он их умеет с красивой градиентной заливкой, быстро и довольно много. Умея быстро рисовать много треугольников, можно нарисовать все что угодно.

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

arcanoid.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;
};

#endif	// _BRICKS_H_

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

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

image

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

Автор: GlukKazan

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js