Содержание блога
Финальная демка запускается в браузерах на Desktop (Windows, Linux, macOS) и в браузерах на мобильных устройствах (Android и iOS). Исходники результата: finish-box2d-v3-wasm-sdl3-c.zip

Box2D - это библиотека для 2D физики для анимаций и игр. С её помощью можно определять были ли коллизии между конкретными объектами и вызывать обработчики событий столкновения. Например, когда главный герой собирает монетки, жизни, касается шипов, врагов и т.д. Пошагово напишем код, в котором по клику левой кнопки мыши (либо одно касание на мобильном) будут создаваться падающие квадраты, а по клику правой кнопки мыши (либо касание двумя пальцами на мобильном) будут создаваться круги. Квадраты и круги падают на пол.
Подключение библиотек Box2D и SDL3 к стартовому примеру
- Установите Emscripten 4.0.15 и CMake по инструкции: Установка Emscripten SDK (emsdk) и CMake
- Скачайте стартовый пример: start-box2d-v3-wasm-sdl3-c.zip

- Далее представлен код стартового примера. В этом коде в функции SDL_AppEvent() добавлены два обработчика. Первый обработчик для клика левой и правой кнопки мыши. Они выводят в консоль текст "Левая" или "Правая" кнопка нажата и координаты клика. Второй обработчик для Android. Имитация левой кнопки - касание одним пальцем, а имитация правой кнопки - это касание двумя и более пальцами:
src/main.c
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
| #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Example", 400, 400, 0, &window, &renderer))
{
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
// --- DESKTOP ---
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
{
if (event->button.which != SDL_TOUCH_MOUSEID)
{
const char *btnName = (event->button.button == SDL_BUTTON_LEFT) ? "Левая" : "Правая";
printf("%s кнопка (Мышь): x=%.0f, y=%.0f\n", btnName, event->button.x, event->button.y);
}
}
// --- MOBILE / WASM (Touch) ---
if (event->type == SDL_EVENT_FINGER_DOWN)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float pixelX = event->tfinger.x * w;
float pixelY = event->tfinger.y * h;
// Проверяем количество пальцев на устройстве (touchId берем из события)
int numFingers = 0;
SDL_GetTouchFingers(event->tfinger.touchID, &numFingers);
if (numFingers <= 1)
{
printf("Левая кнопка (Тап 1 пальцем): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
else
{
printf("Правая кнопка (Тап несколькими): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
// Draw here...
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
// SDL will clean up the window/renderer for us
} |
|
- Откройте папку со стартовым примером в какой-нибудь редакторе кода, например, в Notepad++ или в Sublime Text 4 (ST4): https://www.sublimetext.com/download
- Скачайте библиотеку SDL3 для Wasm: SDL3-devel-3.4.0-wasm.zip
- Скачайте библиотеку Box2D v3 для Wasm: box2d-3.1.1-wasm.zip
- Извлеките содержимое архив выше в какую-нибудь новую общую папку, например, с именем "libs" на какой-нибудь диск, например, на C. Таким образом, создайте на диске C папку "lib", скопируйте в неё архивы и извлеките их в текущую папку:

- Откройте папку "SDL3-devel-3.4.0-wasm", перейдите в папку "lib/cmake/SDL3" и скопируйте абсолютный путь:

- В файле CMakeLists.txt стартового примера создайте переменную SDL3_DIR и установите ей значение скопированного пути, заменив обратные слеши на прямые:
| Bash | 1
| set(SDL3_DIR "C:/libs/SDL3-devel-3.4.0-wasm/lib/cmake/SDL3") |
|
- Откройте папку "box2d-3.1.1-wasm", перейдите в папку "lib/cmake/box2d" и скопируйте абсолютный путь:

- В файле CMakeLists.txt стартового примера создайте переменную box2d_DIR и установите ей значение скопированного пути, заменив обратные слеши на прямые:
| Bash | 1
| set(box2d_DIR "C:/libs/box2d-3.1.1-wasm/lib/cmake/box2d") |
|
- Здесь же в файле CMakeLists.txt добавьте команду для проверки наличия библиотеки box2d в системе, а если пути к этим библиотекам мы задали неверно, то будет выведена ошибка при конфигурировании проекта:
| Bash | 1
2
3
4
5
| # Проверяем наличие библиотек в системе
# Если она не будет найдены, CMake прервет настройку с ошибкой
# REQUIRED - переводится, как «обязательно» или «требуется»
find_package(SDL3 REQUIRED)
find_package(box2d REQUIRED) |
|
- Привязываем библиотеку Box2D к нашему приложению путём добавления box2d::box2d:
Было:
| Bash | 1
| target_link_libraries(app PRIVATE SDL3::SDL3) |
|
Стало:
| Bash | 1
2
| # Привязываем библиотеки к нашему приложению (настройка линковки и путей include)
target_link_libraries(app PRIVATE box2d::box2d SDL3::SDL3) |
|
- К этому моменту CMakeLists.txt выглядит так:
| Bash | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| cmake_minimum_required(VERSION 3.21)
project(start-box2d-v3-wasm-sdl3-c)
# Задаем название будущего приложения (в Windows это был бы app.exe, а в вебе будет app.js / app.wasm)
add_executable(app)
# Устанавливаем стандарт C
set(CMAKE_C_STANDARD 11)
# Указываем точное местоположение конфигурационных файлов библиотек
# Имя переменной должно строго соответствовать: НазваниеБиблиотеки_DIR
set(SDL3_DIR "C:/libs/SDL3-devel-3.4.0-wasm/lib/cmake/SDL3")
set(box2d_DIR "C:/libs/box2d-3.1.1-wasm/lib/cmake/box2d")
# Проверяем наличие библиотек в системе
# Если она не будет найдены, CMake прервет настройку с ошибкой
# REQUIRED - переводится, как «обязательно» или «требуется»
find_package(SDL3 REQUIRED)
find_package(box2d REQUIRED)
# Привязываем библиотеки к нашему приложению (настройка линковки и путей include)
target_link_libraries(app PRIVATE box2d::box2d SDL3::SDL3)
# Добавляем исходный код к проекту
target_sources(app
PRIVATE
src/main.c
) |
|
- Далее создадим тестовую сборку, в которой создадим пустой физический мир с гравитацией и выведем значение гравитации в консоль
Тестовая сборка и тестовый запуск стартового примера в браузере на локальном хостинге
- Прежде чем запустить скомпилировать код и запустить приложение, создадим пустой физический мир с гравитацией и выведем значение гравитации в консоль
- Откройте файл src/main.c в редакторе кода
- Подключите заголовочный файл библиотеки Box2D:
| C | 1
2
3
4
5
6
| #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
#include <box2d/box2d.h> |
|
- Добавьте объявление идентификатора для физического мира Box2D после объявлений переменных-указателей окна и рисовальщика:
| C | 1
2
3
| static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static b2WorldId worldId; |
|
- Добавьте в конец функции SDL_AppInit() создание физического мира и вывод значения гравитации в консоль:
| C | 1
2
3
4
5
6
7
8
9
10
11
| // Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 9.8f };
b2WorldDef worldDef = b2DefaultWorldDef();
worldDef.gravity = gravity;
worldId = b2CreateWorld(&worldDef);
// Выводим значение гравитации в консоль
// Получаем текущее значение гравитации
b2Vec2 currentGravity = b2World_GetGravity(worldId);
// Выводим компоненты x и y
printf("Текущее значение гравитации: x = %.2f, y = %.2f\n", currentGravity.x, currentGravity.y); |
|
- Добавьте в начало функции SDL_AppQuit() удаление физического мира:
| C | 1
2
3
4
5
6
7
8
9
10
| void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
- Откройте консоль (CMD) в корне стартового примера. Для этого можете просто в адресной строке папки (там где путь) написать "cmd" (без кавычек) и нажать Enter
- В CMD введите команду для конфигурирования:
- Примечание. "config-web" - это имя bash-файла (батника), который находится в корне папки стартового примера
- В CMD введите команду сборки проекта:
- Собранные файлы (app.js и app.wasm) будут скопированы в папку "public/js" (папка "public" лежит в корне проекта)
- В корне проекта запустите локальный сервер командой:
- Примечание 1. Будут выведены адреса:
| Bash | 1
2
3
4
| Available on:
http://192.168.1.65:8080
http://127.0.0.1:8080
Hit CTRL-C to stop the server |
|
- Примечание 2. Адрес 192.168.1.65:8080 можно использовать для запуска приложения на мобильном устройстве, если у вас есть Wi-Fi - просто вводите этот адрес в браузере мобильного устройства и приложение запустится. Если интересен запуск на мобильном устройстве, то пройдите пошаговую инструкцию: Основы отладки веб-приложений на SDL3 по USB и Wi-Fi
- Откройте новую вкладку в браузере. Если у вас Chrome или Edge, то нажмите Ctrl+Shift+J, чтобы открыть консоль браузера для контроля вывода информации. Если у вас FireFox, то нажмите Ctrl+Shift+K
- Перейдите по адресу:
- Примечание. Обновлять страницу после повторной сборки проекта лучше с очисткой кэша браузера. Для этого (обязательно с открытой консолью браузера) в браузере Chrome нужно найти в левом верхнем углу браузера кнопку обновления страницы (круговая стрелка), нажать по ней правой кнопкой мыши и выбрать "Empty Cache and Hard Reload"
- В браузере должен выводиться квадрат тёмного-серого цвета - это холст для рисования. В консоль браузера будет выведено сообщение:
| Bash | 1
| Текущее значение гравитации: x = 0.00, y = 9.80 |
|
- Код main.c к этому моменту:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
| #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
#include <box2d/box2d.h>
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static b2WorldId worldId;
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Example", 400, 400, 0, &window, &renderer))
{
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
// Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 9.8f };
b2WorldDef worldDef = b2DefaultWorldDef();
worldDef.gravity = gravity;
worldId = b2CreateWorld(&worldDef);
// Выводим значение гравитации в консоль
// Получаем текущее значение гравитации
b2Vec2 currentGravity = b2World_GetGravity(worldId);
// Выводим компоненты x и y
printf("Текущее значение гравитации: x = %.2f, y = %.2f\n", currentGravity.x, currentGravity.y);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
// --- DESKTOP ---
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
{
if (event->button.which != SDL_TOUCH_MOUSEID)
{
const char *btnName = (event->button.button == SDL_BUTTON_LEFT) ? "Левая" : "Правая";
printf("%s кнопка (Мышь): x=%.0f, y=%.0f\n", btnName, event->button.x, event->button.y);
}
}
// --- MOBILE / WASM (Touch) ---
if (event->type == SDL_EVENT_FINGER_DOWN)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float pixelX = event->tfinger.x * w;
float pixelY = event->tfinger.y * h;
// Проверяем количество пальцев на устройстве (touchId берем из события)
int numFingers = 0;
SDL_GetTouchFingers(event->tfinger.touchID, &numFingers);
if (numFingers <= 1)
{
printf("Левая кнопка (Тап 1 пальцем): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
else
{
printf("Правая кнопка (Тап несколькими): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
// Draw here...
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
Добавляем пол, падающие квадрат и круг и рисуем коллайдеры
Определение Gemini 3, что такое коллайдер простыми словами:
Коллайдер в Box2D - "физическая оболочка" объекта, которая определяет его форму для столкновений. В цифровом мире картинка (спрайт) — это просто набор пикселей. Без коллайдера два танка на экране просто проедут друг сквозь друга, как привидения. Чтобы они столкнулись, движку нужно знать их границы.
| Коллайдеры можно рисовать линиями. В Box2D есть вспомогательные функции, которые помогают рисовать коллайдеры. Далее рассмотрим рисование коллайдера прямоугольной формы и в форме круга. В Box2D есть структура, которую мы создадим позже, под названием b2DefaultDebugDraw. У этой структуры есть два указателя на функции: DrawSolidPolygonFcn (для прямоугольников) и DrawSolidCircleFcn (для круговых коллайдеров). К этим указателям привязываем свои функции, которые будут вызываться, когда нужно будет нарисовать коллайдеры.
- Добавьте в main.c после объявления идентификатора физического мира объявление структуры b2DebugDraw:
| C | 1
2
3
4
| static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static b2WorldId worldId;
static b2DebugDraw debugDrawer; |
|
- В конце функции SDL_AppInit() добавьте код создания структуры типа b2DebugDraw. Две функции: drawSolidPolygon() и drawSolidCircle() мы создадим чуть позже:
| C | 1
2
3
4
| debugDrawer = b2DefaultDebugDraw();
debugDrawer.drawShapes = true;
debugDrawer.DrawSolidPolygonFcn = drawSolidPolygon;
debugDrawer.DrawSolidCircleFcn = drawSolidCircle; |
|
- После создания структуры debugDrawer напишите код создания пола:
| C | 1
2
3
4
5
6
7
8
| // Floor
b2BodyDef floorBodyDef = b2DefaultBodyDef();
floorBodyDef.type = b2_staticBody;
floorBodyDef.position = (b2Vec2) { 200.f / ppm, 300.f / ppm };
b2BodyId floorBodyId = b2CreateBody(worldId, &floorBodyDef);
b2Polygon floorShape = b2MakeBox((300.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef floorShapeDef = b2DefaultShapeDef();
b2ShapeId topWallShapeId = b2CreatePolygonShape(floorBodyId, &floorShapeDef, &floorShape); |
|
- Создайте файл "debug-drawer.h" в папке "src". Скопируйте код определения двух функций: drawSolidPolygon() и drawSolidCircle():
debug-drawer.h
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
| #ifndef DEBUG_DRAWER_H
#define DEBUG_DRAWER_H
#include <SDL3/SDL.h>
#include <box2d/box2d.h>
extern SDL_Renderer *renderer;
extern const SDL_PixelFormatDetails *format;
extern float ppm;
static void drawSolidPolygon(b2Transform transform,
const b2Vec2 *vertices, int vertexCount,
float radius, b2HexColor color, void *context)
{
(void)radius;
(void)context;
// Extract RGB
Uint8 r, g, b;
SDL_GetRGB(color, format, NULL, &r, &g, &b);
// Draw a collider rectangle with lines
SDL_SetRenderDrawColor(renderer, r, g, b, SDL_ALPHA_OPAQUE);
for (int i = 0; i < vertexCount; ++i)
{
int next_index = (i + 1 == vertexCount) ? 0 : i + 1;
b2Vec2 p0 = b2TransformPoint(transform, vertices[i]);
b2Vec2 p1 = b2TransformPoint(transform, vertices[next_index]);
float x0 = p0.x * ppm;
float y0 = p0.y * ppm;
float x1 = p1.x * ppm;
float y1 = p1.y * ppm;
SDL_RenderLine(renderer, x0, y0, x1, y1);
}
// --- Tip: Draw orientation line ---
// Compute polygon center
float cx = 0.f, cy = 0.f;
for (int i = 0; i < vertexCount; ++i)
{
b2Vec2 p = b2TransformPoint(transform, vertices[i]);
cx += p.x;
cy += p.y;
}
cx = (cx / vertexCount) * ppm;
cy = (cy / vertexCount) * ppm;
// Midpoint of second edge (vertex[1] → vertex[2])
b2Vec2 p1 = b2TransformPoint(transform, vertices[1]);
b2Vec2 p2 = b2TransformPoint(transform, vertices[2]);
float mx = (p1.x + p2.x) * 0.5f * ppm;
float my = (p1.y + p2.y) * 0.5f * ppm;
// Draw center → edge midpoint line (acts like "direction tip")
SDL_RenderLine(renderer, cx, cy, mx, my);
}
static void drawSolidCircle(b2Transform transform, float radius,
b2HexColor color, void *context)
{
(void)radius;
(void)context;
float angle = 0.f;
const int numberOfSegments = 20;
const float angleStep = 360.f / numberOfSegments;
// Extract RGB
Uint8 r, g, b;
SDL_GetRGB(color, format, NULL, &r, &g, &b);
// Draw a collider rectangle with lines
SDL_SetRenderDrawColor(renderer, r, g, b, SDL_ALPHA_OPAQUE);
float x = radius * cos(angle * SDL_PI_F / 180.f);
float y = radius * sin(angle * SDL_PI_F / 180.f);
b2Vec2 p0 = b2TransformPoint(transform, (b2Vec2) { x, y });
float x0 = p0.x * ppm;
float y0 = p0.y * ppm;
angle += angleStep;
for (int i = 0; i < numberOfSegments; ++i)
{
float x = radius * cos(angle * SDL_PI_F / 180.f);
float y = radius * sin(angle * SDL_PI_F / 180.f);
b2Vec2 p1 = b2TransformPoint(transform, (b2Vec2) { x, y });
float x1 = p1.x * ppm;
float y1 = p1.y * ppm;
SDL_RenderLine(renderer, x0, y0, x1, y1);
x0 = x1;
y0 = y1;
angle += angleStep;
if (angle >= 360.f)
{
angle = 0.f;
}
}
// --- Tip: Draw orientation line ---
// Circle center
b2Vec2 c = b2TransformPoint(transform, (b2Vec2) { 0.f, 0.f });
float cx = c.x * ppm;
float cy = c.y * ppm;
// Direction = transform applied to (radius, 0)
b2Vec2 orient = b2TransformPoint(transform, (b2Vec2) { radius, 0.f });
float ox = orient.x * ppm;
float oy = orient.y * ppm;
// Draw center → orientation marker
SDL_RenderLine(renderer, cx, cy, ox, oy);
}
#endif // DEBUG_DRAWER_H |
|
- Этому файлу требуются глобальные переменные renderer и ppm (pixels per meter). Поэтому нужно в main.c убрать "static" в объявлении "renderer". Добавьте константу "format" и "ppm":
| C | 1
2
3
4
5
6
7
| static SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
static b2WorldId worldId;
static b2DebugDraw debugDrawer;
const SDL_PixelFormatDetails *format = NULL;
float ppm = 30.f; // Pixels per meter |
|
- Примечание. "ppm" (pixel per meter) преобразует координатную сетку из мира пикселей в мир метрики Box2D. В Box2D измерения в метрах
- Глобальная константа "format" нужна для работы SDL3. Мы объявили её в файле main.c. Необходимо добавить её создание в конце функции SDL_AppInit():
| C | 1
2
3
| // Get the pixel format
SDL_Surface *surface = SDL_GetWindowSurface(window);
format = SDL_GetPixelFormatDetails(surface->format); |
|
- Рисование коллайдеров происходит при вызове функции b2World_Draw(), которой передаётся идентификатор мира и debugDrawer:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
| SDL_AppResult SDL_AppIterate(void *appstate)
{
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
b2World_Draw(worldId, &debugDrawer);
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
} |
|
- Соберите проект командой:
- Перейдите по адресу в браузере:
- Программа рисует пол:

- Полный код функции main.c на текущий момент, чтобы свериться, на случай если возникли ошибки:
main.c
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
| #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
#include <box2d/box2d.h>
#include "debug-drawer.h"
static SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
static b2WorldId worldId;
static b2DebugDraw debugDrawer;
const SDL_PixelFormatDetails *format = NULL;
float ppm = 30.f; // Pixels per meter
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Example", 400, 400, 0, &window, &renderer))
{
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
// Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 9.8f };
b2WorldDef worldDef = b2DefaultWorldDef();
worldDef.gravity = gravity;
worldId = b2CreateWorld(&worldDef);
// Выводим значение гравитации в консоль
// Получаем текущее значение гравитации
b2Vec2 currentGravity = b2World_GetGravity(worldId);
// Выводим компоненты x и y
printf("Текущее значение гравитации: x = %.2f, y = %.2f\n", currentGravity.x, currentGravity.y);
debugDrawer = b2DefaultDebugDraw();
debugDrawer.drawShapes = true;
debugDrawer.DrawSolidPolygonFcn = drawSolidPolygon;
debugDrawer.DrawSolidCircleFcn = drawSolidCircle;
// Floor
b2BodyDef floorBodyDef = b2DefaultBodyDef();
floorBodyDef.type = b2_staticBody;
floorBodyDef.position = (b2Vec2) { 200.f / ppm, 300.f / ppm };
b2BodyId floorBodyId = b2CreateBody(worldId, &floorBodyDef);
b2Polygon floorShape = b2MakeBox((300.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef floorShapeDef = b2DefaultShapeDef();
b2ShapeId floorShapeId = b2CreatePolygonShape(floorBodyId, &floorShapeDef, &floorShape);
// Get the pixel format
SDL_Surface *surface = SDL_GetWindowSurface(window);
format = SDL_GetPixelFormatDetails(surface->format);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
// --- DESKTOP ---
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
{
if (event->button.which != SDL_TOUCH_MOUSEID)
{
const char *btnName = (event->button.button == SDL_BUTTON_LEFT) ? "Левая" : "Правая";
printf("%s кнопка (Мышь): x=%.0f, y=%.0f\n", btnName, event->button.x, event->button.y);
}
}
// --- MOBILE / WASM (Touch) ---
if (event->type == SDL_EVENT_FINGER_DOWN)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float pixelX = event->tfinger.x * w;
float pixelY = event->tfinger.y * h;
// Проверяем количество пальцев на устройстве (touchId берем из события)
int numFingers = 0;
SDL_GetTouchFingers(event->tfinger.touchID, &numFingers);
if (numFingers <= 1)
{
printf("Левая кнопка (Тап 1 пальцем): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
else
{
printf("Правая кнопка (Тап несколькими): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
b2World_Draw(worldId, &debugDrawer);
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
- Добавим два объекта, которые пока не падают на пол: куб и круг, а зависают в воздухе над полом. В функции SDL_AppInit() добавьте, после создания пола, код для квадрата и круга:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Square
b2BodyDef squareBodyDef = b2DefaultBodyDef();
squareBodyDef.type = b2_dynamicBody;
squareBodyDef.position = (b2Vec2) { 200.f / ppm, 100.f / ppm };
squareBodyDef.enableSleep = false;
b2BodyId squareBodyId = b2CreateBody(worldId, &squareBodyDef);
b2Polygon squareShape = b2MakeBox((30.f / 2.f) / ppm, (30.f / 2.f) / ppm);
b2ShapeDef squareShapeDef = b2DefaultShapeDef();
b2ShapeId squareShapeId = b2CreatePolygonShape(squareBodyId, &squareShapeDef, &squareShape);
// Circle
b2BodyDef circleBodyDef = b2DefaultBodyDef();
circleBodyDef.type = b2_dynamicBody;
circleBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
circleBodyDef.enableSleep = false;
b2BodyId circleBodyId = b2CreateBody(worldId, &circleBodyDef);
b2Circle circleCircle = {
.center = { 0.0f, 0.0f }, // <--- ВАЖНО: 0,0 означает, что центр круга совпадает с центром тела
.radius = 20.f / ppm // Радиус в метрах
};
b2ShapeDef circleShapeDef = b2DefaultShapeDef();
circleShapeDef.density = 1.0f;
b2ShapeId circleShapeId = b2CreateCircleShape(circleBodyId, &circleShapeDef, &circleCircle); |
|
- Скомпилируйте код:
- Запустите программу:
- Мы видим пол, над которым зависли квадрат и круг:

- Полный код main.c:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
| #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
#include "debug-drawer.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <box2d/box2d.h>
#include <stdio.h>
static SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
static b2WorldId worldId;
static b2DebugDraw debugDrawer;
const SDL_PixelFormatDetails *format = NULL;
float ppm = 30.f; // Pixels per meter
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Example", 400, 400, 0, &window, &renderer))
{
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
// Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 9.8f };
b2WorldDef worldDef = b2DefaultWorldDef();
worldDef.gravity = gravity;
worldId = b2CreateWorld(&worldDef);
// Выводим значение гравитации в консоль
// Получаем текущее значение гравитации
b2Vec2 currentGravity = b2World_GetGravity(worldId);
// Выводим компоненты x и y
printf("Текущее значение гравитации: x = %.2f, y = %.2f\n", currentGravity.x, currentGravity.y);
debugDrawer = b2DefaultDebugDraw();
debugDrawer.drawShapes = true;
debugDrawer.DrawSolidPolygonFcn = drawSolidPolygon;
debugDrawer.DrawSolidCircleFcn = drawSolidCircle;
// Floor
b2BodyDef floorBodyDef = b2DefaultBodyDef();
floorBodyDef.type = b2_staticBody;
floorBodyDef.position = (b2Vec2) { 200.f / ppm, 300.f / ppm };
b2BodyId floorBodyId = b2CreateBody(worldId, &floorBodyDef);
b2Polygon floorShape = b2MakeBox((300.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef floorShapeDef = b2DefaultShapeDef();
b2ShapeId floorShapeId = b2CreatePolygonShape(floorBodyId, &floorShapeDef, &floorShape);
// Square
b2BodyDef squareBodyDef = b2DefaultBodyDef();
squareBodyDef.type = b2_dynamicBody;
squareBodyDef.position = (b2Vec2) { 200.f / ppm, 100.f / ppm };
squareBodyDef.enableSleep = false;
b2BodyId squareBodyId = b2CreateBody(worldId, &squareBodyDef);
b2Polygon squareShape = b2MakeBox((30.f / 2.f) / ppm, (30.f / 2.f) / ppm);
b2ShapeDef squareShapeDef = b2DefaultShapeDef();
b2ShapeId squareShapeId = b2CreatePolygonShape(squareBodyId, &squareShapeDef, &squareShape);
// Circle
b2BodyDef circleBodyDef = b2DefaultBodyDef();
circleBodyDef.type = b2_dynamicBody;
circleBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
circleBodyDef.enableSleep = false;
b2BodyId circleBodyId = b2CreateBody(worldId, &circleBodyDef);
b2Circle circleCircle = {
.center = { 0.0f, 0.0f }, // <--- ВАЖНО: 0,0 означает, что центр круга совпадает с центром тела
.radius = 20.f / ppm // Радиус в метрах
};
b2ShapeDef circleShapeDef = b2DefaultShapeDef();
circleShapeDef.density = 1.0f;
b2ShapeId circleShapeId = b2CreateCircleShape(circleBodyId, &circleShapeDef, &circleCircle);
// Get the pixel format
SDL_Surface *surface = SDL_GetWindowSurface(window);
format = SDL_GetPixelFormatDetails(surface->format);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
// --- DESKTOP ---
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
{
if (event->button.which != SDL_TOUCH_MOUSEID)
{
const char *btnName = (event->button.button == SDL_BUTTON_LEFT) ? "Левая" : "Правая";
printf("%s кнопка (Мышь): x=%.0f, y=%.0f\n", btnName, event->button.x, event->button.y);
}
}
// --- MOBILE / WASM (Touch) ---
if (event->type == SDL_EVENT_FINGER_DOWN)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float pixelX = event->tfinger.x * w;
float pixelY = event->tfinger.y * h;
// Проверяем количество пальцев на устройстве (touchId берем из события)
int numFingers = 0;
SDL_GetTouchFingers(event->tfinger.touchID, &numFingers);
if (numFingers <= 1)
{
printf("Левая кнопка (Тап 1 пальцем): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
else
{
printf("Правая кнопка (Тап несколькими): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
b2World_Draw(worldId, &debugDrawer);
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
Анимация падения. Независимость скорости анимации от частоты обновления экрана
- Добавьте в начале файла main.c после "include" две константы:
| C | 1
2
| #define TIME_STEP (1.0f / 60.0f)
#define MAX_FRAME_TIME 0.25f |
|
- Примечание. Анимация физики будет происходить с фиксированным периодом равным TIME_STEP. Константа MAX_FRAME_TIME нужна будет для защиты от слишком длинных задержек
- Добавьте две глобальные переменные смысл которых будет понятен далее:
| C | 1
2
| static float accumulator = 0.0f;
static Uint64 last_ticks = 0; |
|
- В конце функции SDL_AppInit() инициализируйте переменную last_ticks:
| C | 1
| last_ticks = SDL_GetTicks(); |
|
- В начале функции SDL_AppIterate() добавьте код для расчёта длительности кадра (frameTime) и удержания кадра пределах TIME_STEP:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| SDL_AppResult SDL_AppIterate(void *appstate)
{
Uint64 current_ticks = SDL_GetTicks();
float frameTime = (float)(current_ticks - last_ticks) / 1000.0f;
last_ticks = current_ticks;
if (frameTime > MAX_FRAME_TIME)
{
frameTime = MAX_FRAME_TIME;
}
accumulator += frameTime;
while (accumulator >= TIME_STEP)
{
// Step physics
b2World_Step(worldId, TIME_STEP, 3);
accumulator -= TIME_STEP;
}
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
b2World_Draw(worldId, &debugDrawer);
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
} |
|
- Примечание. Функция b2World_Step() - это шаг во времени в физическом мире на величину TIME_STEP
- Скомпилируте код:
- Запустите приложение в браузере:
- Вы увидите, что квадрат падает на круг и удерживает равновесие. Сместите на один пиксель квадрат вправо:
| C | 1
| squareBodyDef.position = (b2Vec2) { 201.f / ppm, 100.f / ppm }; |
|
- Скомпилируйте и запустите приложение снова и вы увидите нормальную анимацию падения
- Полный код main.c:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
| #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
#include "debug-drawer.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <box2d/box2d.h>
#include <stdio.h>
#define TIME_STEP (1.0f / 60.0f)
#define MAX_FRAME_TIME 0.25f
static SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
static b2WorldId worldId;
static b2DebugDraw debugDrawer;
const SDL_PixelFormatDetails *format = NULL;
float ppm = 30.f; // Pixels per meter
static float accumulator = 0.0f;
static Uint64 last_ticks = 0;
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Example", 400, 400, 0, &window, &renderer))
{
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
// Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 9.8f };
b2WorldDef worldDef = b2DefaultWorldDef();
worldDef.gravity = gravity;
worldId = b2CreateWorld(&worldDef);
// Выводим значение гравитации в консоль
// Получаем текущее значение гравитации
b2Vec2 currentGravity = b2World_GetGravity(worldId);
// Выводим компоненты x и y
printf("Текущее значение гравитации: x = %.2f, y = %.2f\n", currentGravity.x, currentGravity.y);
debugDrawer = b2DefaultDebugDraw();
debugDrawer.drawShapes = true;
debugDrawer.DrawSolidPolygonFcn = drawSolidPolygon;
debugDrawer.DrawSolidCircleFcn = drawSolidCircle;
// Floor
b2BodyDef floorBodyDef = b2DefaultBodyDef();
floorBodyDef.type = b2_staticBody;
floorBodyDef.position = (b2Vec2) { 200.f / ppm, 300.f / ppm };
b2BodyId floorBodyId = b2CreateBody(worldId, &floorBodyDef);
b2Polygon floorShape = b2MakeBox((300.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef floorShapeDef = b2DefaultShapeDef();
b2ShapeId floorShapeId = b2CreatePolygonShape(floorBodyId, &floorShapeDef, &floorShape);
// Square
b2BodyDef squareBodyDef = b2DefaultBodyDef();
squareBodyDef.type = b2_dynamicBody;
squareBodyDef.position = (b2Vec2) { 201.f / ppm, 100.f / ppm };
squareBodyDef.enableSleep = false;
b2BodyId squareBodyId = b2CreateBody(worldId, &squareBodyDef);
b2Polygon squareShape = b2MakeBox((30.f / 2.f) / ppm, (30.f / 2.f) / ppm);
b2ShapeDef squareShapeDef = b2DefaultShapeDef();
b2ShapeId squareShapeId = b2CreatePolygonShape(squareBodyId, &squareShapeDef, &squareShape);
// Circle
b2BodyDef circleBodyDef = b2DefaultBodyDef();
circleBodyDef.type = b2_dynamicBody;
circleBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
circleBodyDef.enableSleep = false;
b2BodyId circleBodyId = b2CreateBody(worldId, &circleBodyDef);
b2Circle circleCircle = {
.center = { 0.0f, 0.0f }, // <--- ВАЖНО: 0,0 означает, что центр круга совпадает с центром тела
.radius = 20.f / ppm // Радиус в метрах
};
b2ShapeDef circleShapeDef = b2DefaultShapeDef();
circleShapeDef.density = 1.0f;
b2ShapeId circleShapeId = b2CreateCircleShape(circleBodyId, &circleShapeDef, &circleCircle);
// Get the pixel format
SDL_Surface *surface = SDL_GetWindowSurface(window);
format = SDL_GetPixelFormatDetails(surface->format);
last_ticks = SDL_GetTicks();
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
// --- DESKTOP ---
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
{
if (event->button.which != SDL_TOUCH_MOUSEID)
{
const char *btnName = (event->button.button == SDL_BUTTON_LEFT) ? "Левая" : "Правая";
printf("%s кнопка (Мышь): x=%.0f, y=%.0f\n", btnName, event->button.x, event->button.y);
}
}
// --- MOBILE / WASM (Touch) ---
if (event->type == SDL_EVENT_FINGER_DOWN)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float pixelX = event->tfinger.x * w;
float pixelY = event->tfinger.y * h;
// Проверяем количество пальцев на устройстве (touchId берем из события)
int numFingers = 0;
SDL_GetTouchFingers(event->tfinger.touchID, &numFingers);
if (numFingers <= 1)
{
printf("Левая кнопка (Тап 1 пальцем): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
else
{
printf("Правая кнопка (Тап несколькими): x=%.2f, y=%.2f\n", pixelX, pixelY);
}
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
Uint64 current_ticks = SDL_GetTicks();
float frameTime = (float)(current_ticks - last_ticks) / 1000.0f;
last_ticks = current_ticks;
if (frameTime > MAX_FRAME_TIME)
{
frameTime = MAX_FRAME_TIME;
}
accumulator += frameTime;
while (accumulator >= TIME_STEP)
{
// Step physics
b2World_Step(worldId, TIME_STEP, 3);
accumulator -= TIME_STEP;
}
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
b2World_Draw(worldId, &debugDrawer);
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
Сборка в релиз
- Для сборки в релиз нужно открыть файл "config-web.bat" в редакторе кода и исправить Debug на Release:
config-web.bat
| Bash | 1
| emcmake cmake -S . -B dist -DCMAKE_BUILD_TYPE=Release |
|
- Выполните команды конфигурирования и сборки:
- Примечание. Вес файлов после сборки в Debug:
| Code | 1
2
| app.js - 375 KB
app.wasm - 2.05 MB |
|
Вес файлов после сборки в Release:
| Code | 1
2
| app.js - 183 KB
app.wasm - 1.03 MB |
|
Развёртывание на бесплатном хостинге Vercel
- Скачайте установщик Node.js и установите: https://nodejs.org/en/download
- Установите Vercel следующей командой из консоли глобально:
- Зарегистрируйтесь на Vercel: https://vercel.com/
- Примечание. В стартовом прмер, который скачали в начале, уже есть файл vercel.json для решения проблемы с загрузкой wasm-файла
- В консоле в корне проекта выполните команду:
- Выполните команду:
- Примечание. Будут заданы вопросы в консоли - просто нажимайте всегда Enter
- В консоль будет выведен адрес вашего приложения: https://start-box2d-v3-wasm-sdl3-c.vercel.app
- Примечание. Когда вы что-то измените в проекте и захотите загрузить изменённое приложение на сервер, то введите команду:
- Результат работы приложения:

|