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
Автор : Rosko