WebAssembly SDL canvas Arkanoid на C/C++

Table of contents

Introduction

В статье WebAssembly SDL Float Cube на C/C++ мы познакомились с возможностью графической отрисовки, анимацией и взаимодействием с нарисованными объектами. С помощью полученных знаний мы напишем простенькую игру под названием Arkanoid.

Репозиторий с полным кодом проекта: https://github.com/superrosko/wasm-game-sdl-canvas-arkanoid.

Npm package: https://www.npmjs.com/package/wasm-game-sdl-canvas-arkanoid.

Demo: https://pages.rdevelab.ru/wasm-game-sdl-canvas-arkanoid/index.html.

Структура проекта

.
├── build               директория для сборки, которая игнорируется в .gitignore
├── distr               директория с собранным проектом для npm-пакета
├── include             директория для заголовочных файлов .h/.hpp
│   ├── ball.h          заголовочный файл с определением функций, типов и переменных летающего мяча
│   ├── basic.h         заголовочный файл с определением базовых типов и переменных
│   ├── border.h        заголовочный файл с определением функций, типов и переменных границ игры
│   ├── bricks.h        заголовочный файл с определением функций, типов и переменных кирпичиков
│   ├── core.h          файл с определением функций, типов и переменных для отрисовки
│   └── field.h         заголовочный файл с определением функций, типов и переменных поля
│   └── platform.h      заголовочный файл с определением функций, типов и переменных платформы
├── public
│   └── index.html      файл с демонстрацией работы нашего проекта в браузере
├── src                 исходные файлы проекта .c/.cpp
│   ├── ball.cpp        код, отвечающий за отрисовку летающего мяча
│   ├── border.cpp      код, отвечающий за отрисовку границ игры
│   ├── bricks.cpp      код, отвечающий за отрисовку кирпичиков
│   ├── core.cpp        точка входа с функцией main и циклами отрисовки
│   └── field.cpp       код, отвечающий за отрисовку поля
│   └── platform.cpp    код, отвечающий за отрисовку платформы
├── .gitattributes
├── .gitignore
├── CMakeLists.txt      файл с командами cmake для сборки
├── LICENSE
├── README.md
├── build.sh            файл, управляющий сборкой проекта
└── package.json        описание npm-пакета

Вид игрового поля:

wasm game sdl canvas arkanoid

Для реализации игры создаем 5 объектов:

  1. поле, на котором происходят все действия игры
  2. границы поля, за которые мяч не должен вылетать и от которых он должен отбиваться, а при соприкосновении с нижней границей будет происходить еще и уменьшение жизней
  3. платформа, с помощью которой будем отбивать летающий мяч
  4. сам летающий мяч, который будет отбиваться ото всех объектов, а при соприкосновении со статичными кирпичиками будет их уничтожать
  5. статичные кирпичики, которые нужно уничтожить с помощью летающего мяча, отбивая его платформой

Также реализуем вывод информации о заработанных очках и количестве жизней. А для управления создадим 3 кнопки: start, pause, restart.

Исходный код проекта

Файлы basic.h, field.h, field.cpp уже описывались в предыдущей статье и никак не изменились для реализации игры.

Начнем с файла, отвечающего за описание и отрисовку летающего мяча ball.h/ball.cpp:

struct Ball
{
    int velocityX;
    int velocityY;
    SDL_Rect rect;
    Color color;
};

const int BALL_SPAWN_LINE_WPADDING = 20;
const int BALL_SPAWN_LINE = 40;
const int BALL_WIDTH = 10;
const int BALL_HEIGHT = 10;
const int BALL_VELOCITY_X = 5;
const int BALL_VELOCITY_Y = 5;
const Color BALL_COLOR = {253, 21, 27, 255};

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setBallSize(int width, int height);

    EMSCRIPTEN_KEEPALIVE
    void setBallPosition(int x, int y);

    EMSCRIPTEN_KEEPALIVE
    void setBallColor(Color color);

    EMSCRIPTEN_KEEPALIVE
    void setRndBallPosition();

    EMSCRIPTEN_KEEPALIVE
    void setBallVelocity(int velocityX, int velocityY);
}

extern int getFieldWidth();
extern int getFieldHeight();

int getBallVerifiedPoint(int sizeField, int sizeBall, int point);
int getBallVelocityX();
int revBallVelocityX();
int getBallVelocityY();
int revBallVelocityY();
void setBallRect(SDL_Rect rect);
SDL_Rect getBallRect();
void renderBall(SDL_Renderer *renderer);
Ball *initBall();

Вначале мы определяем структуру Ball с данными о летающем мяче - это структура SDL_Rect с размерами и координатами мяча, структура цвета и две переменные, отвечающие за вектор скорости полета мяча. Затем задаем значения по умолчанию для области, в которой будет рандомно спавниться мяч в начале каждой игры, размерам, вектору скорости и цвету мяча. Далее мы описываем функции, которые потом реализуем или будем использовать в файле ball.cpp:

  • setBallSize - устанавливает размер мяча
  • setBallPosition - устанавливает позицию мяча
  • setBallColor - устанавливает цвет мяча
  • setRndBallPosition - устанавливает рандомную позицию мяча в пределах указанной зоны спавна
  • setBallVelocity - устанавливает скорость полета мяча
  • getBallVerifiedPoint - проверяет и корректирует координаты, в которые мы хотим переместить мяч, на предмет выхода за рамки игрового поля
  • getBallVelocityX - возвращает текущий вектор скорости мяча по координате X
  • revBallVelocityX - реверсирует вектор скорости мяча по координате X
  • getBallVelocityY - возвращает текущий вектор скорости мяча по координате Y
  • revBallVelocityY - реверсирует вектор скорости мяча по координате Y
  • setBallRect - устанавливает структуру с размерами и координатами мяча
  • getBallRect - возвращает структуру с размерами и координатами мяча
  • renderBall - отрисовывает мяч
  • initBall - функция инициализации объекта мяча
#include "basic.h"
#include "ball.h"

Ball gBall;

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setBallSize(int width, int height)
    {
        gBall.rect.w = width;
        gBall.rect.h = height;
    }

    EMSCRIPTEN_KEEPALIVE
    void setBallPosition(int x, int y)
    {
        gBall.rect.x = getBallVerifiedPoint(getFieldWidth(), gBall.rect.w, x);
        gBall.rect.y = getBallVerifiedPoint(getFieldHeight(), gBall.rect.h, y);
    }

    EMSCRIPTEN_KEEPALIVE
    void setBallColor(Color color)
    {
        gBall.color = color;
    }

    EMSCRIPTEN_KEEPALIVE
    void setRndBallPosition()
    {
        srand(time(NULL));
        int x = rand() % (getFieldWidth() - BALL_SPAWN_LINE_WPADDING) + BALL_SPAWN_LINE_WPADDING / 2;
        int y = rand() % BALL_SPAWN_LINE + getFieldHeight() / 2 - gBall.rect.h;
        setBallPosition(x, y);
    }

    EMSCRIPTEN_KEEPALIVE
    void setBallVelocity(int velocityX, int velocityY)
    {
        gBall.velocityX = velocityX;
        gBall.velocityY = velocityY;
    }
}

int getBallVerifiedPoint(int sizeField, int sizeBall, int point)
{
    if (sizeField < (point + sizeBall))
    {
        return sizeField - sizeBall;
    }
    if (point < 0)
    {
        return 0;
    }
    return point;
}

int getBallVelocityX()
{
    return gBall.velocityX;
}

int revBallVelocityX()
{
    return gBall.velocityX *= -1;
}

int getBallVelocityY()
{
    return gBall.velocityY;
}

int revBallVelocityY()
{
    return gBall.velocityY *= -1;
}

void setBallRect(SDL_Rect rect)
{
    gBall.rect = rect;
}

SDL_Rect getBallRect()
{
    return gBall.rect;
}

void renderBall(SDL_Renderer *renderer)
{
    SDL_SetRenderDrawColor(renderer, gBall.color.r, gBall.color.g, gBall.color.b, gBall.color.a);
    SDL_RenderFillRect(renderer, &gBall.rect);
    SDL_RenderPresent(renderer);
}

Ball *initBall()
{
    setBallVelocity(BALL_VELOCITY_X, BALL_VELOCITY_Y);
    setBallSize(BALL_WIDTH, BALL_HEIGHT);
    setBallColor(BALL_COLOR);
    setRndBallPosition();
    return &gBall;
}

Глобальная переменная Ball gBall хранит состояние мяча, а далее идет реализация описанных ранее функций.

Перейдем к коду границ игрового поля border.h/border.cpp, от которых будет отбиваться мяч, а касание нижней границы будет еще и уменьшать жизни:

struct Border
{
    bool status;
    SDL_Rect rect;
};

struct Borders
{
    Border *border;
    Color color;
};

const int BORDER_SIZE = 10;
const Color BORDER_COLOR = {181, 112, 61, 255};

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setBorderColor(Color color);
}

extern int getFieldWidth();
extern int getFieldHeight();

bool getBorderStatus(int number);
SDL_Rect getBorderRect(int number);
void renderBorders(SDL_Renderer *renderer);
Borders *initBorders();

Вначале мы определяем структуру Border со статусом границы, который будет обозначать необходимость вычитать жизни при соприкосновении мяча с ней, и структурой SDL_Rect с размерами и координатами границы. Потом определяем структуру Borders, в которой будем хранить цвет и указатель на массив со структурами границ. Затем задаем значения по умолчанию для размера границ и их цвета. Далее мы описываем функции, которые потом реализуем или будем использовать в файле border.cpp:

  • setBorderColor - устанавливает цвет границ
  • getBorderStatus - возвращает статус указанной границы
  • getBorderRect - возвращает структуру с размерами и координатами указанной границы
  • renderBorders - отрисовывает границы
  • initBorders - функция инициализации объектов границ
#include "basic.h"
#include "border.h"

Borders gBorders;

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setBorderColor(Color color)
    {
        gBorders.color = color;
    }
}

bool getBorderStatus(int number)
{
    return gBorders.border[number].status;
}

SDL_Rect getBorderRect(int number)
{
    return gBorders.border[number].rect;
}

void renderBorders(SDL_Renderer *renderer)
{
    for (int i = 0; i < 4; i++)
    {
        SDL_SetRenderDrawColor(renderer, gBorders.color.r, gBorders.color.g, gBorders.color.b, gBorders.color.a);
        SDL_RenderFillRect(renderer, &(gBorders.border[i].rect));
        SDL_RenderPresent(renderer);
    }
}

Borders *initBorders()
{
    setBorderColor(BORDER_COLOR);
    int fieldSizeW = getFieldWidth();
    int fieldSizeH = getFieldHeight();

    gBorders.border = new Border[4];
    gBorders.border[0].rect = {0, 0, BORDER_SIZE, fieldSizeH};
    gBorders.border[1].rect = {fieldSizeW - BORDER_SIZE, 0, BORDER_SIZE, fieldSizeH};
    gBorders.border[2].rect = {0, 0, fieldSizeW, BORDER_SIZE};
    gBorders.border[3].rect = {0, fieldSizeH - BORDER_SIZE, fieldSizeW, BORDER_SIZE};

    gBorders.border[0].status = false;
    gBorders.border[1].status = false;
    gBorders.border[2].status = false;
    gBorders.border[3].status = true;

    return &gBorders;
}

Перейдем к коду кирпичиков bricks.h/bricks.cpp, от которых будет отбиваться мяч, а касание каждого из них мячом будет их уничтожать и прибавлять по одному очку за каждый:

struct Brick
{
    bool status;
    SDL_Rect rect;
};

struct Bricks
{
    int rowCount;
    int lineCount;
    int width;
    int height;
    int countAllBricks;
    int countActiveBricks;
    Brick *brick;
    Color color;
};

const int BRICKS_LINE_COUNT = 6;
const int BRICKS_ROWS = 10;
const int BRICKS_PADDING = 30;
const int BRICKS_SPACE = 12;
const int BRICKS_HEIGHT = 10;
const Color BRICKS_COLOR = {255, 179, 15, 255};

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setBricksCount(int rowCount, int lineCount);

    EMSCRIPTEN_KEEPALIVE
    void setBricksColor(Color color);
}

extern int getFieldWidth();
extern int getFieldHeight();

void setBricksSize();
void setBrickStatus(int number, bool status);
bool getBrickStatus(int number);
int getActiveBricks();
int incActiveBricks();
int decActiveBricks();
SDL_Rect getBrickRect(int number);
void renderBricks(SDL_Renderer *renderer);
Bricks *initBricks();

Вначале мы определяем структуру Brick со статусом, который будет указывать на активность кирпичика (попал ли в него мяч или нет), и структурой SDL_Rect с размерами и координатами кирпичика. Потом определяем структуру Bricks, в которой будем хранить значения количества кирпичиков в ряду, цвета, количества рядов, общее количество кирпичиков, количества активных кирпичиков, размеры и указатель на массив с их структурами. Затем задаем значения по умолчанию для количества рядов, количества кирпичиков в каждом ряду, расстояния между кирпичиками и высоты кирпичиков. Далее мы описываем функции, которые потом реализуем или будем использовать в файле bricks.cpp:

  • setBricksCount - устанавливает количество кирпичиков
  • setBricksColor - устанавливает цвет кирпичиков
  • setBricksSize - устанавливает размер кирпичиков
  • setBrickStatus - устанавливает статус указанного кирпичика
  • getBrickStatus - возвращает статус указанного кирпичика
  • getActiveBricks - возвращает количество активных кирпичиков
  • incActiveBricks - увеличивает и возвращает количество активных кирпичиков
  • decActiveBricks - уменьшает и возвращает количество активных кирпичиков
  • getBrickRect - возвращает структуру с размерами и координатами указанного кирпичика
  • renderBricks - отрисовывает кирпичики
  • initBricks - функция инициализации объектов кирпичиков
#include "basic.h"
#include "bricks.h"

Bricks gBricks;

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setBricksCount(int rowCount, int lineCount)
    {
        gBricks.rowCount = rowCount;
        gBricks.lineCount = lineCount;
    }

    EMSCRIPTEN_KEEPALIVE
    void setBricksColor(Color color)
    {
        gBricks.color = color;
    }
}

void setBricksSize()
{
    gBricks.width = (int)((getFieldWidth() - 2 * BRICKS_PADDING + BRICKS_SPACE) / gBricks.lineCount) - BRICKS_SPACE;
    gBricks.height = BRICKS_HEIGHT;
}

void setBrickStatus(int number, bool status)
{
    gBricks.brick[number].status = status;
}

bool getBrickStatus(int number)
{
    return gBricks.brick[number].status;
}

int getActiveBricks()
{
    return gBricks.countActiveBricks;
}

int incActiveBricks()
{
    gBricks.countActiveBricks++;
    return gBricks.countActiveBricks;
}

int decActiveBricks()
{
    gBricks.countActiveBricks--;
    return gBricks.countActiveBricks;
}

SDL_Rect getBrickRect(int number)
{
    return gBricks.brick[number].rect;
}

void renderBricks(SDL_Renderer *renderer)
{
    for (int i = 0; i < gBricks.countAllBricks; i++)
    {
        if (getBrickStatus(i))
        {
            SDL_SetRenderDrawColor(renderer, gBricks.color.r, gBricks.color.g, gBricks.color.b, gBricks.color.a);
            SDL_RenderFillRect(renderer, &(gBricks.brick[i].rect));
            SDL_RenderPresent(renderer);
        }
    }
}

Bricks *initBricks()
{
    gBricks.countAllBricks = BRICKS_LINE_COUNT * BRICKS_ROWS;
    gBricks.countActiveBricks = gBricks.countAllBricks;

    setBricksCount(BRICKS_ROWS, BRICKS_LINE_COUNT);
    setBricksSize();
    setBricksColor(BRICKS_COLOR);

    gBricks.brick = new Brick[gBricks.countAllBricks];
    for (int i = 0; i < BRICKS_ROWS; i++)
    {
        for (int j = 0; j < BRICKS_LINE_COUNT; j++)
        {
            SDL_Rect r;
            r.x = BRICKS_PADDING + (BRICKS_SPACE + gBricks.width) * j;
            r.y = BRICKS_PADDING + (BRICKS_SPACE + gBricks.height) * i;
            r.w = gBricks.width;
            r.h = gBricks.height;
            gBricks.brick[i * BRICKS_LINE_COUNT + j].status = true;
            gBricks.brick[i * BRICKS_LINE_COUNT + j].rect = r;
        }
    }

    return &gBricks;
}

Перейдем к коду платформы platform.h/platform.cpp, которой мы будем управлять для того, чтобы отбивать мяч и не давать ему коснуться нижней границы:

struct Platform
{
    SDL_Rect rect;
    Color color;
};

const int PLATFORM_TOP_LINE = 50;
const int PLATFORM_WIDTH = 100;
const int PLATFORM_HEIGHT = 10;
const Color PLATFORM_COLOR = {150, 150, 150, 255};

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setPlatformSize(int width, int height);

    EMSCRIPTEN_KEEPALIVE
    void setPlatformPosition(int x, int y, bool fixX, bool fixY);

    EMSCRIPTEN_KEEPALIVE
    void setPlatformColor(Color color);
}

extern int getFieldWidth();
extern int getFieldHeight();

int getPlatformVerifiedPoint(int sizeField, int sizePlatform, int point);
SDL_Rect getPlatformRect();
void renderPlatform(SDL_Renderer *renderer);
Platform *initPlatform();

Вначале мы определяем структуру Platform со структурой SDL_Rect и цветом платформы. Затем задаем значения по умолчанию для координат линии расположения, ширины, толщины и цвета платформы. Далее мы описываем функции, которые потом реализуем или будем использовать в файле platform.cpp:

  • setPlatformSize - устанавливает размер платформы
  • setPlatformPosition - устанавливает позицию платформы
  • setPlatformColor - устанавливает цвет платформы
  • getPlatformVerifiedPoint - проверяет и корректирует координаты, в которые мы хотим переместить платформу, на предмет выхода за рамки игрового поля
  • getPlatformRect - возвращает структуру с размерами и координатами платформы
  • renderPlatform - отрисовывает платформу
  • initPlatform - функция инициализации объекта платформы
#include "basic.h"
#include "platform.h"

Platform gPlatform;

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setPlatformSize(int width, int height)
    {
        gPlatform.rect.w = width;
        gPlatform.rect.h = height;
    }

    EMSCRIPTEN_KEEPALIVE
    void setPlatformPosition(int x, int y, bool fixX = true, bool fixY = true)
    {
        if (!fixX)
        {
            gPlatform.rect.x = getPlatformVerifiedPoint(getFieldWidth(), gPlatform.rect.w, x);
        }

        if (!fixY)
        {
            gPlatform.rect.y = getPlatformVerifiedPoint(getFieldHeight(), gPlatform.rect.h, y);
        }
    }

    EMSCRIPTEN_KEEPALIVE
    void setPlatformColor(Color color)
    {
        gPlatform.color = color;
    }
}

int getPlatformVerifiedPoint(int sizeField, int sizePlatform, int point)
{
    point -= sizePlatform / 2;
    if (sizeField < (point + sizePlatform))
    {
        return sizeField - sizePlatform;
    }
    if (point < 0)
    {
        return 0;
    }
    return point;
}

SDL_Rect getPlatformRect()
{
    return gPlatform.rect;
}

void renderPlatform(SDL_Renderer *renderer)
{
    SDL_SetRenderDrawColor(renderer, gPlatform.color.r, gPlatform.color.g, gPlatform.color.b, gPlatform.color.a);
    SDL_RenderFillRect(renderer, &gPlatform.rect);
    SDL_RenderPresent(renderer);
}

Platform *initPlatform()
{
    setPlatformSize(PLATFORM_WIDTH, PLATFORM_HEIGHT);
    setPlatformColor(PLATFORM_COLOR);
    setPlatformPosition(getFieldWidth() / 2, getFieldHeight() - PLATFORM_TOP_LINE, false, false);
    return &gPlatform;
}

Итак, мы рассмотрели реализацию каждого объекта игрового поля и теперь нам необходимо описать саму игру, т.е. взаимодействие между объектами, за это отвечают файлы core.h и core.cpp:

struct Context
{
    SDL_Window *window;
    SDL_Renderer *renderer;
    int countLives;
    int countScore;
    int gameActive;
};

const int GAME_LIVES_COUNT = 5;

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void gameRestart();

    EMSCRIPTEN_KEEPALIVE
    void gameStart();

    EMSCRIPTEN_KEEPALIVE
    void gameStop();

    EMSCRIPTEN_KEEPALIVE
    int getLivesCount();

    EMSCRIPTEN_KEEPALIVE
    int getScoreCount();
}

int valueInRange(int value, int min, int max);
int objectsOverlap(SDL_Rect *A, SDL_Rect *B);
bool checkObjectMoving(SDL_Rect *A, SDL_Rect *B);
void moveBall();
void mainloop(void *arg);
SDL_Renderer *getGContextRender();
void runLoop();
void renderWindow();
int decLivesCount();
void setLivesCount(int lives);
int incScoreCount();
void setScoreCount(int score);
bool getGameActive();
void setGameActive(bool status);

Вначале мы определяем структуру Context, в которой будем хранить указатели на объекты для рендеринга и параметры игры, такие как жизни, набранные очки и статус игры (поставлена на паузу или нет). Затем задаем значения по умолчанию для максимального количества начальных жизней. Далее мы описываем функции, которые потом реализуем или будем использовать в файле core.cpp:

  • gameRestart - функция перезапуска игры, которая будет вызываться по кнопке со страницы
  • gameStart - функция запуска игры с паузы, которая будет вызываться по кнопке со страницы
  • gameStop - функция приостановки игры, которая будет вызываться по кнопке со страницы
  • getLivesCount - функция, возвращающая текущее количество жизней для вывода на страницу
  • getScoreCount - функция, возвращающая текущее количество очков для вывода на страницу
  • valueInRange - функция для определения вхождения значения в определенный промежуток
  • objectsOverlap - функция, которая определяет пересек ли мяч какой-то объект или нет
  • checkObjectMoving - функция, которая определяет новый вектор движения мяча, проверяя его пересечение с указанным объектом
  • moveBall - основная логика игры, полета, отбития мяча посредством проверки пересечения объекта мяча с остальными объектами
  • mainloop - основной цикл рендеринга объектов игры
  • getGContextRender - возвращает объект рендеринга
  • runLoop - запуск цикла рендеринга
  • renderWindow - отрисовка окна рендеринга
  • decLivesCount - уменьшает количество жизней
  • setLivesCount - устанавливает количество жизней и вызывает внешнюю функцию wasmGameArkanoidJsSetLives
  • incScoreCount - увеличивает количество очков
  • setScoreCount - устанавливает количество очков и вызывает внешнюю функцию wasmGameArkanoidJsSetScore
  • getGameActive - возвращает статус активности игры
  • setGameActive - устанавливает статус активности игры
#include "basic.h"
#include "ball.h"
#include "border.h"
#include "bricks.h"
#include "field.h"
#include "platform.h"
#include "core.h"

Context gContext;

int valueInRange(int value, int min, int max)
{
    return (value >= min) && (value <= max);
}

int objectsOverlap(SDL_Rect *A, SDL_Rect *B)
{
    int xOverlap =
        valueInRange(A->x, B->x, B->x + B->w) || valueInRange(B->x, A->x, A->x + A->w);
    int yOverlap =
        valueInRange(A->y, B->y, B->y + B->h) || valueInRange(B->y, A->y, A->y + A->h);
    return xOverlap && yOverlap;
}

bool checkObjectMoving(SDL_Rect *A, SDL_Rect *B)
{
    SDL_Rect rBall = {};
    if (objectsOverlap(A, B))
    {
        rBall = getBallRect();
        rBall.x += getBallVelocityX();
        if (objectsOverlap(&rBall, B))
        {
            revBallVelocityX();
        }

        rBall = getBallRect();
        rBall.y += getBallVelocityY();
        if (objectsOverlap(&rBall, B))
        {
            revBallVelocityY();
        }

        A->x = rBall.x + getBallVelocityX();
        A->y = rBall.y + getBallVelocityY();
        return true;
    }
    return false;
}

void moveBall()
{
    SDL_Rect rBall = getBallRect();
    rBall.x += getBallVelocityX();
    rBall.y += getBallVelocityY();

    SDL_Rect wo = getPlatformRect();
    checkObjectMoving(&rBall, &wo);

    for (int i = 0; i < 4; i++)
    {
        SDL_Rect wo = getBorderRect(i);
        if (checkObjectMoving(&rBall, &wo) && getBorderStatus(i))
        {
            decLivesCount();
        }
    }

    int count = BRICKS_LINE_COUNT * BRICKS_ROWS;
    for (int i = 0; i < count; i++)
    {
        SDL_Rect wo = getBrickRect(i);
        if (getBrickStatus(i))
        {
            if (checkObjectMoving(&rBall, &wo))
            {
                setBrickStatus(i, false);
                incScoreCount();
                decActiveBricks();
                ;
            }
        }
    }

    setBallRect(rBall);
}

void mainloop(void *arg)
{
    SDL_Renderer *render = getGContextRender();
    renderField(render);
    renderBricks(render);
    renderBall(render);
    renderPlatform(render);
    renderBorders(render);
    if (getGameActive() && getActiveBricks() && getLivesCount())
    {
        moveBall();
    }
}

SDL_Renderer *getGContextRender()
{
    return gContext.renderer;
}

void runLoop()
{
    const int simulate_infinite_loop = 1;
    const int fps = -1;
    emscripten_set_main_loop_arg(mainloop, &gContext, fps, simulate_infinite_loop);
}

void renderWindow()
{
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_CreateWindowAndRenderer(getFieldWidth(), getFieldHeight(), 0, &window, &renderer);
    gContext.renderer = renderer;
    gContext.window = window;
}

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void gameRestart()
    {
        setLivesCount(GAME_LIVES_COUNT);
        initBall();
        initBricks();
        setGameActive(false);
        setScoreCount(0);
    }

    EMSCRIPTEN_KEEPALIVE
    void gameStart()
    {
        setGameActive(true);
    }

    EMSCRIPTEN_KEEPALIVE
    void gameStop()
    {
        setGameActive(false);
    }

    EMSCRIPTEN_KEEPALIVE
    int getLivesCount()
    {
        return gContext.countLives;
    }

    EMSCRIPTEN_KEEPALIVE
    int getScoreCount()
    {
        return gContext.countScore;
    }
}

int decLivesCount()
{
    gContext.countLives--;
    setLivesCount(gContext.countLives);
    return gContext.countLives;
}

void setLivesCount(int lives)
{
    gContext.countLives = lives;
    char buffer[50];
    sprintf(buffer, "wasmGameArkanoidJsSetLives(%d);", lives);
    emscripten_run_script(buffer);
}

int incScoreCount()
{
    gContext.countScore++;
    setScoreCount(gContext.countScore);
    return gContext.countScore;
}

void setScoreCount(int score)
{
    gContext.countScore = score;
    char buffer[50];
    sprintf(buffer, "wasmGameArkanoidJsSetScore(%d);", score);
    emscripten_run_script(buffer);
}

bool getGameActive()
{
    return gContext.gameActive;
}

void setGameActive(bool status)
{
    gContext.gameActive = status;
}

int main()
{
    SDL_Init(SDL_INIT_VIDEO);

    initField();
    initPlatform();
    gameRestart();
    initBorders();
    renderWindow();
    runLoop();

    SDL_DestroyRenderer(gContext.renderer);
    SDL_DestroyWindow(gContext.window);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Сборка проекта

Файлы (build.sh и CMakeLists.txt) для сборки практически не изменились: в CMakeLists.txt поменялось название проекта, сменилась директория для собранных файлов на distr и добавилась пара флагов:

MODULARIZE=1 - флаг, указывающий на необходимость собрать файл game_arkanoid.wasm.js как модуль.

EXPORT_NAME=wasmGameArkanoid - флаг, указывающий имя модуля.

Для сборки проекта нам необходимо выполнить build.sh, после чего в директории distr появятся файлы game_arkanoid.wasm.js и game_arkanoid.wasm.wasm.

Также собранные файлы будут продублированы в директорию public с помощью добавленной в build.sh команды:

find distr/ -type f -name "*.wasm.*" -exec cp {} public/ \;

После того, как мы собрали проект, нам необходимо посмотреть на его выполнение в web (браузере).

Запуск проекта в web

Для запуска через web добавим следующий код в файл ./public/index.html:

<!DOCTYPE html>
<html>

<head>
    <title>WebAssembly Game Arkanoid!</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        .row {
            display: flex;
            height: 100vh;
            justify-content: center;
            align-items: center;
        }

        canvas {
            cursor: move !important;
            width: 100%;
            max-width: 400px;
        }
    </style>
</head>

<body>
    <div class="row">
        <div id="game_arkanoid">
            <div class="info">
                score : <span class="score">0</span>
                lives: <span class="lives">0</span>
            </div>
            <canvas id="canvas"></canvas>
            <div class="controls">
                <button class="stop">stop</button>
                <button class="start">start</button>
                <button class="restart">restart</button>
            </div>
        </div>
    </div>
    <script src="game_arkanoid.wasm.js"></script>
    <script>
        wasmGameArkanoid['canvas'] = (function () {
            return document.getElementById('canvas');
        })();
        wasmGameArkanoid(wasmGameArkanoid).then(function (Module) {

            var game = document.getElementById('game_arkanoid');
            var canvas = document.getElementById('canvas');
            var score = game.querySelector('.score');
            var lives = game.querySelector('.lives');
            var stop = game.querySelector('.stop');
            var start = game.querySelector('.start');
            var restart = game.querySelector('.restart');

            stop.addEventListener("click", function (evt) {
                Module._gameStop();
            });
            start.addEventListener("click", function (evt) {
                Module._gameStart();
            });
            restart.addEventListener("click", function (evt) {
                Module._gameRestart();
            });

            canvas.addEventListener("mousemove", function (evt) {
                var rect = this.getBoundingClientRect();
                var x = evt.clientX - rect.left;
                var y = evt.clientY - rect.top;
                Module._setPlatformPosition(x, y, false, true);
            }, false);
            canvas.addEventListener("touchmove", function (evt) {
                var rect = this.getBoundingClientRect();
                var x = evt.touches[0].clientX - rect.left;
                var y = evt.touches[0].clientY - rect.top;
                Module._setPlatformPosition(x, y, false, true);
            }, false);

        })
        function wasmGameArkanoidJsSetLives(lives) {
            document.querySelector('#game_arkanoid .lives').textContent = lives;
        };
        function wasmGameArkanoidJsSetScore(score) {
            document.querySelector('#game_arkanoid .score').textContent = score;
        };
    </script>
</body>

</html>

У нас есть 3 кнопки, по клику на которые вызываются функции _gameStop, _gameStart, _gameRestart, и объявлены 2 функции: wasmGameArkanoidJsSetLives, wasmGameArkanoidJsSetScore, которые вызываются при изменении количества жизней и очков. Также события на mousemove и touchmove позволяют управлять платформой как движением мыши, так и с помощью свайпа на мобильных устройствах.

Перейдем в директорию ./public и запустим в ней сервер:

cd ./public
http-server

Откроем в браузере адрес http://127.0.0.1:8080 и можем наслаждаться игрой Arkanoid!

Дата публикации :
Дата редактирования : 2020-06-18 21:28:15
Автор :

Cookies and IP addresses allow us to deliver and improve our web content, resolve technical errors, and provide you with a personalized experience. Our website uses cookies and collects your IP address for these purposes.