WebAssembly SDL Float Cube на C/C++

Table of contents

Introduction

В прошлой статье (WebAssembly Hello World на C/C++) мы познакомились с возможностью компиляции кода на C/C++ в wasm. Сейчас же мы рассмотрим возможности графической отрисовки, анимации и взаимодействия с нарисованными объектами. Для отрисовки объектов на C/C++ мы воспользуемся библиотекой SDL, а вывод на web-странице будет осуществляться в canvas.

Из названия проекта понятно, что мы будем отрисовывать куб на поле с определенными границами, а передвижение куба будет осуществляться передвижением указателя мыши. Мы будем передавать координаты курсора, который находится над объектом canvas, и передвигать отрисованный куб к этим координатам, т.е. данные из JavaScript будут передаваться в функцию на C/C++, а прорисованные объекты из C/C++ в canvas.

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

Demo: https://pages.rdevelab.ru/example-wasm-sdl-canvas-float-cube/index.html.

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

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

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

Начнем с файла basic.h, который содержит подключение нужных заголовочных файлов и описание типов, необходимых для всего проекта:

#include <SDL2/SDL.h>
#include <cstdlib>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif

struct Color
{
    int r;
    int g;
    int b;
    int a;
};

Вначале мы подключаем заголовочный файл SDL2/SDL.h библиотеки SDL, далее подключается стандартная библиотека и заголовочный файл компилятора Emscripten.

После этого определяем структуру Color, в которой будут храниться RGBA наших объектов.

Теперь перейдем к коду, отвечающему за отрисовку поля, и начнем с заголовочного файла field.h:

struct Field
{
    int width;
    int height;
    Color color;
};

const int FIELD_WIDTH = 400;
const int FIELD_HEIGHT = 400;
const Color FIELD_COLOR = {19, 38, 81, 255};

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

    EMSCRIPTEN_KEEPALIVE
    void setFieldColor(Color color);
}

int getFieldWidth();
int getFieldHeight();
void renderField(SDL_Renderer *renderer);
Field *initField();

Вначале мы определяем структуру Field с данными о поле: высоте, ширине, цвете - и задаем значения по умолчанию для них. Далее мы описываем функции, которые потом реализуем в файле field.cpp:

#include "basic.h"
#include "field.h"

Field gField;

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void setFieldSize(int width, int height)
    {
        gField.width = width;
        gField.height = height;
    }

    EMSCRIPTEN_KEEPALIVE
    void setFieldColor(Color color)
    {
        gField.color = color;
    }
}

int getFieldWidth()
{
    return gField.width;
}

int getFieldHeight()
{
    return gField.height;
}

void renderField(SDL_Renderer *renderer)
{
    SDL_SetRenderDrawColor(renderer, gField.color.r, gField.color.g, gField.color.b, gField.color.a);
    SDL_RenderClear(renderer);
}

Field *initField()
{
    setFieldSize(FIELD_WIDTH, FIELD_HEIGHT);
    setFieldColor(FIELD_COLOR);
    return &gField;
}

Вначале мы подключаем заголовочный файл basic.h с базовыми типами и файл field.h для кода поля, после объявляем глобальную переменную gField с типом Field, в которой будут храниться параметры нашего поля. После этого переходим к реализации сеттеров setFieldSize и setFieldColor, которые устанавливают значения в глобальную переменную для хранения характеристик поля (цвет и размеры). Данные функции будут экспортироваться в JS, поэтому для них указан модификатор extern "C" и макрос EMSCRIPTEN_KEEPALIVE.

Далее реализуем геттеры getFieldWidth и getFieldHeight для получения размеров поля из глобальной переменной gField.

За отрисовку поля отвечает функция renderField, которая принимает указатель на SDL_Renderer. Мы устанавливаем цвет рендерера (SDL_SetRenderDrawColor) и очищаем рендерер установленным цветом (SDL_RenderClear).

Для инициализации поля реализуем функцию initField, которая установит в глобальную переменную gField размер и цвет поля и вернет указатель на нее.

С кодом для отрисовки поля мы разобрались и теперь переходим к коду, отвечающему за отрисовку куба. Данный код будет во многом похож на код поля:

struct Cube
{
    SDL_Rect rect;
    Color color;
};

const int CUBE_WIDTH = 50;
const int CUBE_HEIGHT = 50;
const int CUBE_X = 200;
const int CUBE_Y = 200;
const Color CUBE_COLOR = {196, 85, 0, 255};

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

    EMSCRIPTEN_KEEPALIVE
    void setCubePosition(int x, int y);

    EMSCRIPTEN_KEEPALIVE
    void setCubeColor(Color color);
}

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

int getCubeVerifiedPoint(int sizeField, int sizeCube, int point);
void renderCube(SDL_Renderer *renderer);
Cube *initCube();

Вначале мы определяем структуру Cube с данными о кубе - это структура SDL_Rect с размерами и координатами куба, цветом, затем задаем значения по умолчанию для высоты, ширины и цвета. Далее мы описываем функции, которые потом реализуем или будем использовать в файле cube.cpp. Функции getFieldWidth и getFieldHeight имеют модификатор extern, т.е. мы их не будем реализовывать в cube.cpp, а будем лишь использовать там, т.к. реализация есть в файле field.cpp. Другими словами, мы говорим компилятору, что функции getFieldWidth и getFieldHeight имеют описанные сигнатуры и то, что они будут реализованы где-то в другом коде проекта .

В файле cube.cpp все организовано схожим образом как и в field.cpp:

#include "basic.h"
#include "cube.h"

Cube gCube;

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

    EMSCRIPTEN_KEEPALIVE
    void setCubePosition(int x, int y)
    {
        gCube.rect.x = getCubeVerifiedPoint(getFieldWidth(), gCube.rect.w, x);
        gCube.rect.y = getCubeVerifiedPoint(getFieldHeight(), gCube.rect.h, y);
    }

    EMSCRIPTEN_KEEPALIVE
    void setCubeColor(Color color)
    {
        gCube.color = color;
    }
}

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

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

Cube *initCube()
{
    setCubeSize(CUBE_WIDTH, CUBE_HEIGHT);
    setCubePosition(CUBE_X, CUBE_Y);
    setCubeColor(CUBE_COLOR);
    return &gCube;
}

Глобальная переменная Cube gCube будет хранить состояние куба, а функции setCubeSize, setCubeColor, setCubePosition будут устанавливать размер, цвет и позицию куба.

В функции setCubePosition как раз используется вызов описанных в заголовочном файле cube.h функций getFieldWidth и getFieldHeight, а также функции getCubeVerifiedPoint, которая проверяет переданные координаты куба на принадлежность полю и в случае, если это не так, то координаты исправляются на правильные и куб не выходит за рамки поля.

Отрисовка куба происходит немного по-другому. В функцию renderCube все также передается указатель на рендерер, и устанавливается цвет рендерера. После этого вызываются SDL_RenderFillRect, которая отрисовывает куб, и SDL_RenderPresent, которая обновляет отрисованный экран.

Мы рассмотрели код, отвечающий за объекты поля и куба, теперь разберем код, который описывает и реализует непосредственную отрисовку. Начнем с заголовочного файла core.h:

struct Context
{
    SDL_Window *window;
    SDL_Renderer *renderer;
};

SDL_Renderer *getGContextRender();

Вначале описывается структура, в которой будут храниться указатели на объект окна рисования и рендерера, а также представлена функция getGContextRender, которая будет реализована для получения указателя на объект структуры типа Context.

Реализация отрисовки находится в файле core.cpp:

#include "basic.h"
#include "field.h"
#include "cube.h"
#include "core.h"

Context gContext;

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

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

void mainloop(void *arg)
{
    SDL_Renderer *render = getGContextRender();
    renderField(render);
    renderCube(render);
}

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

int main()
{
    SDL_Init(SDL_INIT_VIDEO);

    initField();
    initCube();

    renderWindow();
    runLoop();

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

    return EXIT_SUCCESS;
}

Вначале подключаются все заголовочные файлы проекта, т.к. данный файл будет работать с объектами поля и куба. После этого идет объявление глобальной переменной Context gContext, в которой будут содержаться указатели на окно и рендерер.

Точкой входа в программу является функция main, в которой сначала инициализируются подсистемы SDL с помощью функции SDL_Init и объекты поля и куба (initField и initCube). После инициализации происходит отрисовка окна и создание рендерера в функции renderWindow, далее запускается цикл рисования runLoop. При выходе из цикла рисования происходит очистка ресурсов SDL_DestroyRenderer, SDL_DestroyWindow, SDL_Quit.

В функции renderWindow объявляются указатели на объекты SDL_Window, SDL_Renderer и далее происходит их создание функцией SDL_CreateWindowAndRenderer.

В функции runLoop запускается цикл emscripten_set_main_loop_arg, который вызывает функцию mainloop и передает в нее глобальную переменную с окном и рендерером.

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

Итак, мы рассмотрели код проекта, теперь перейдем к сборке.

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

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

USE_SDL=2 - говорим, что будем использовать SDL 2.

EXTRA_EXPORTED_RUNTIME_METHODS=['ccall'] - флаг, указывающий на необходимость обернуть функцию на C и вернуть JavaScript функцию для вызова с web-страницы.

ENVIRONMENT=web - флаг, задающий среду выполнения проекта - позволяет оптимизировать код.

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

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

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

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

<!DOCTYPE html>
<html lang="en">
<head>
    <title>WebAssembly Float cube!</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: crosshair !important;
        }
    </style>
</head>
<body>
    <div class="row">
        <canvas id="canvas"></canvas>
    </div>
    <script src="float_cube.wasm.js"></script>
    <script>
        Module['onRuntimeInitialized'] = onRuntimeInitialized;
        function onRuntimeInitialized() {

            Module['canvas'] = (function() { return document.getElementById('canvas'); })();
            var canvas = document.getElementById('canvas');

            canvas.addEventListener("mousemove", function (evt) {
                var rect = this.getBoundingClientRect();
                var x = evt.clientX - rect.left;
                var y = evt.clientY - rect.top;
                _setCubePosition(x, y);
            }, false);
        }
    </script>
</body>
</html>

Мы сначала подключили файл float_cube.wasm.js:

<script src="float_cube.wasm.js"></script>

Затем мы передали объект canvas в модуль и повесили на него событие движения мышкой. При срабатывании события мы берем координаты курсора, конвертируем их в координаты canvas и вызываем функцию _setCubePosition, передавая в нее координаты, тем самым мы передвигаем куб по полю.

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

cd ./public
http-server

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

Дата публикации :
Дата редактирования : 2020-11-12 00:36:54
Автор :

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.