Содержание блога
Финальная демка в браузере. Итоговый код: finish-sync-physics-sprites-sdl3-c.zip
На первой гифке отладочные линии отключены, а на второй включены:

Вспомогательные линии нужны только для разработки и отладки, а пользователь не должен их видеть. Пользователь должен видеть спрайты, а линии будут скрыты. Во время отладки будет рисовать линии поверх спрайтов. Спрайты должны рисоваться по тем же координатам и с теми же углами поворота, как и коллайдеры. Box2D имеет специальные функции для считывания координат и углов поворота физических тел, то есть коллайдеров.
Подключение библиотек к стартовому примеру
- Скачайте пример: start-sync-physics-sprites-sdl3-c.zip
- Примечание 1. Стартовый пример написан по пошаговой инструкции: Подключение Box2D v3, физика и отрисовка коллайдеров
- Примечание 2. Разница только в одной строке в файле CMakeLists.txt, где вместо SDL 3.4.0 подключается более новая версия SDL 3.4.2:
| Bash | 1
| set(SDL3_DIR "C:/libs/SDL3-devel-3.4.2-wasm/lib/cmake/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-sync-physics-sprites-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.2-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
| 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
} |
|
src/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 |
|
- Результатом стартового примеря являеются: падающие круговой и квадратный коллайдер и статическая платформа:

- Запустим стартовый пример, а для этого скачаем и подключим библиотеки SDL3 и Box2D
- Откройте папку со стартовым примером в какой-нибудь редакторе кода, например, в Notepad++ или в Sublime Text 4 (ST4): https://www.sublimetext.com/download
- Скачайте библиотеку SDL3 для Wasm: SDL3-devel-3.4.2-wasm.zip
- Скачайте библиотеку Box2D v3 для Wasm: box2d-3.1.1-wasm.zip
- Извлеките содержимое архив выше в какую-нибудь новую общую папку, например, с именем "libs" на какой-нибудь диск, например, на C:

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

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

- Замените в переменной box2d_DIR путь на свой, если необходимо:
| Bash | 1
| set(SDL3_DIR "C:/libs/box2d-3.1.1-wasm/lib/cmake/box2d") |
|
- По следующей инструкции проведём тестовую сборку, чтобы убедиться, что библиотеки подключены и пример работает
Тестовая сборка и тестовый запуск стартового примера в браузере на локальном хостинге
- Откройте консоль (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"
- Мы убедились, что тестовый пример запускается нормально. Если вы что-то меняете в коде (в файлах main.c или CMakeLists.txt), то достаточно ввести команду для сборки build-web (в CMD нажать стрелку-вверх и Enter) и обновить вкладку в браузере. Удобнее всего, чтобы локальный хостинг был запущен в одном окне CMD, а сборку делать в другом окне CMD
Загружаем спрайты в наше приложение
На сайте Kenney (https://kenney.nl) большое количество бесплатных паков с изображениями для игр.
- Скачайте следующий пак под названием Physics со спрайтами и тайлами на сайте Kenney: https://kenney.nl/assets/physics-assets
- Примечание. На случай, если ссылка выше не открывается, то я прикрепил пак Kenney архивом: kenney_physics-assets.zip (2.46 MB)
- В корне стартового примера создайте папку "assets", а в ней папку "sprites"
- Скопируйте в папку "assets/sprites" следующие файлы спрайтов:
- kenney_physics-assets/PNG/Aliens/alienGreen_square.png
- kenney_physics-assets/PNG/Aliens/alienYellow_round.png
- kenney_physics-assets/PNG/Other/grass.png

- Откройте файл CMakeLists.txt в редакторе кода и скопируйте в него (в конец файла) код для встраивания всех трёх файлов изображений в файл "app.wasm":
| Bash | 1
2
3
4
5
6
7
8
9
10
11
| # Встраиваем изображения в файл app.wasm
if (EMSCRIPTEN)
target_link_options("app" PRIVATE "SHELL:--embed-file \"${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites/alienGreen_square.png@/assets/sprites/alienGreen_square.png\"")
set_property(TARGET "app" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites/alienGreen_square.png")
target_link_options("app" PRIVATE "SHELL:--embed-file \"${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites/alienYellow_round.png@/assets/sprites/alienYellow_round.png\"")
set_property(TARGET "app" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites/alienYellow_round.png")
target_link_options("app" PRIVATE "SHELL:--embed-file \"${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites/grass.png@/assets/sprites/grass.png\"")
set_property(TARGET "app" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/sprites/grass.png")
endif() |
|
- Файлы изображений добавлены в проект. Таким же образом вы можете добавлять и другие файлы: звуковые файлы, файлы шрифтов, XML/JSON-файлы и т.д.
- Откройте файл src/main.c в редакторе кода
- Добавьте три глобальные переменные для хранения текстур:
| C | 1
2
3
| static SDL_Texture *alien_green_square_texture = NULL;
static SDL_Texture *alien_yellow_round_texture = NULL;
static SDL_Texture *grass_texture = NULL; |
|
- Сразу же, чтобы не забыть освободить ресурсы, перейдём в конец файла "main.c" и добавим код удаления текстур перед завершением программы, в функцию SDL_AppQuit():
| 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
| void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
if (alien_green_square_texture)
{
SDL_DestroyTexture(alien_green_square_texture);
alien_green_square_texture = NULL;
}
if (alien_yellow_round_texture)
{
SDL_DestroyTexture(alien_yellow_round_texture);
alien_yellow_round_texture = NULL;
}
if (grass_texture)
{
SDL_DestroyTexture(grass_texture);
grass_texture = NULL;
}
// Удаляем физический мир 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
| SDL_Texture* LoadTexture(const char* path)
{
SDL_Surface* surface = SDL_LoadPNG(path);
if (!surface)
{
SDL_Log("Load failed (%s): %s", path, SDL_GetError());
return NULL;
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_DestroySurface(surface);
return texture;
} |
|
- Добавьте следующий год загрузки файлов-текстур в теле функции инициализации, то есть в теле функции SDL_AppInit() после функции SDL_SetRenderVSync():
| C | 1
2
3
| alien_green_square_texture = LoadTexture("./assets/sprites/alienGreen_square.png");
alien_yellow_round_texture = LoadTexture("./assets/sprites/alienYellow_round.png");
grass_texture = LoadTexture("./assets/sprites/grass.png"); |
|
- В функции SDL_AppIterate() добавьте код вывода изображения для тестового запуска после функции SDL_RenderClear(), до перед функций b2World_Draw(), чтобы в будущем отладочные линии коллайдеров рисовались поверх текстуры:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| SDL_AppResult SDL_AppIterate(void *appstate)
{
// ..
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
SDL_FRect srcRect = { 0, 0, 70, 70 };
SDL_FRect destRect = { 150, 250, 100, 100 };
SDL_RenderTexture(renderer, alien_green_square_texture, &srcRect, &destRect);
b2World_Draw(worldId, &debugDrawer);
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
} |
|
- Сделайте сборку проекта командой "build-web" и обновите страницу браузера
- Мы видим, что изображение выводится, а отладочные линии коллайдеров выводятся поверх изображения:

Синхронизация спрайтов SDL3 и тел Box2D
Нам нужно выводить спрайты с теми же координатами и углами повотора, что и тела (коллайдеры). У Box2D есть специальные функции для того, чтобы считать координаты тела (коллайдера): b2Body_GetTransform() и угла: b2Rot_GetAngle(). Найдите эти функции через поиск в правом верхнем углу в официальной документации: https://box2d.org/documentation/, чтобы привыкать работать с документаций.
Вот что по этому поводу написал Gemini
Для того чтобы спрайт «приклеился» к физическому телу, нам нужно в каждом кадре спрашивать у Box2D: «Где сейчас находится тело и под каким углом оно повернуто?».
Вот пошаговый план, как это внедрить в ваш код.
- Как получить данные из Box2D. В Box2D версии 3.0 (которую вы используете) позиция и поворот извлекаются через функцию b2Body_GetTransform.
- Позиция: Извлекается в метрах, поэтому умножаем на ppm.
- Угол: Извлекается в радианах. Для SDL_RenderTextureRotated нам нужно перевести их в градусы.
- Изменения в коде. Чтобы не загромождать SDL_AppIterate, лучше всего создать вспомогательную функцию для отрисовки. Добавьте эту функцию перед SDL_AppIterate:
| 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
| void RenderBody(b2BodyId bodyId, SDL_Texture* texture, float width, float height) {
// 1. Получаем трансформацию тела из Box2D
b2Transform transform = b2Body_GetTransform(bodyId);
b2Vec2 pos = transform.p;
float angleRadians = b2Rot_GetAngle(transform.q);
// 2. Конвертируем метры в пиксели и радианы в градусы
float centerX = pos.x * ppm;
float centerY = pos.y * ppm;
float angleDegrees = angleRadians * (180.0f / SDL_PI_F);
// 3. Определяем прямоугольник отрисовки
// SDL рисует от верхнего левого угла, поэтому смещаем координаты на половину размера
SDL_FRect destRect = {
centerX - (width / 2.0f),
centerY - (height / 2.0f),
width,
height
};
// 4. Отрисовка с учетом поворота
// NULL в качестве центра означает вращение вокруг центра destRect
SDL_RenderTextureRotated(renderer, texture, NULL, &destRect, (double)angleDegrees, NULL, SDL_FLIP_NONE);
} |
|
- Обновляем переменные. Чтобы функция RenderBody могла дотянуться до ID тел, сделайте их статическими (перенесите из SDL_AppInit в начало файла к остальным static переменным):
| C | 1
2
3
| static b2BodyId floorBodyId;
static b2BodyId squareBodyId;
static b2BodyId circleBodyId; |
|
- Обновляем цикл отрисовки. Теперь в SDL_AppIterate замените ваш тестовый блок отрисовки на вызов новой функции:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| SDL_AppResult SDL_AppIterate(void *appstate)
{
// ... (код обновления физики остается прежним) ...
// Очистка экрана
SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255);
SDL_RenderClear(renderer);
// Рисуем спрайты поверх физических тел
// Размеры берем те же, что задавали при создании коллайдеров (умноженные на 2, т.к. b2MakeBox берет полуобхват)
RenderBody(floorBodyId, grass_texture, 300.f, 20.f);
RenderBody(squareBodyId, alien_green_square_texture, 30.f, 30.f);
RenderBody(circleBodyId, alien_yellow_round_texture, 40.f, 40.f); // диаметр = радиус 20 * 2
// Если нужно видеть линии коллайдеров для проверки - оставляем:
// b2World_Draw(worldId, &debugDrawer);
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
} |
|
Почему это работает:
- Центрирование: Box2D считает позицию тела по его центру масс. SDL по умолчанию рисует от угла. Мы вычитаем width / 2, чтобы "центр" картинки совпал с "центром" физического объекта.
- Синхронизация: Поскольку RenderBody вызывается сразу после b2World_Step, визуальная картинка никогда не будет отставать от физики.
- Вращение: SDL_RenderTextureRotated — это мощная функция SDL3, которая берет на себя всю математику поворота текстуры на GPU.
- Используем код Gemini выше. Добавьте функцию RenderBody() перед SDL_AppIterate():
| 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
| void RenderBody(b2BodyId bodyId, SDL_Texture *texture, float width, float height)
{
// 1. Получаем трансформацию тела из Box2D
b2Transform transform = b2Body_GetTransform(bodyId);
b2Vec2 pos = transform.p;
float angleRadians = b2Rot_GetAngle(transform.q);
// 2. Конвертируем метры в пиксели и радианы в градусы
float centerX = pos.x * ppm;
float centerY = pos.y * ppm;
float angleDegrees = angleRadians * (180.0f / SDL_PI_F);
// 3. Определяем прямоугольник отрисовки
// SDL рисует от верхнего левого угла, поэтому смещаем координаты на половину размера
SDL_FRect destRect = {
centerX - (width / 2.0f),
centerY - (height / 2.0f),
width,
height
};
// 4. Отрисовка с учетом поворота
// NULL в качестве центра означает вращение вокруг центра destRect
SDL_RenderTextureRotated(renderer, texture, NULL, &destRect, (double)angleDegrees, NULL, SDL_FLIP_NONE);
} |
|
- Идентификаторы тел делаем глобальными:
| C | 1
2
3
| static b2BodyId floorBodyId;
static b2BodyId squareBodyId;
static b2BodyId circleBodyId; |
|
- Примечание. Не забудьте удалить b2BodyId перед floorBodyId, squareBodyId и circleBodyId в функции SDL_AppInit()
- Добавьте код рисования пола плиткой перед SDL_AppIterate():
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| void RenderFloorTiled(b2BodyId bodyId, SDL_Texture *texture, float floorWidth, float floorHeight)
{
// 1. Получаем позицию из Box2D (в метрах)
b2Vec2 pos = b2Body_GetPosition(bodyId);
// 2. Переводим координаты центра из метров в пиксели
float centerX = pos.x * ppm;
float centerY = pos.y * ppm;
// 3. Целевой прямоугольник для отрисовки всего пола
SDL_FRect dstRect = {
centerX - (floorWidth / 2.0f),
centerY - (floorHeight / 2.0f),
floorWidth,
floorHeight
};
// 4. Вызываем исправленную функцию с 5 аргументами (добавлен 1.0f как масштаб)
// Параметры: renderer, texture, src_rect (NULL = вся текстура), scale, dst_rect
SDL_RenderTextureTiled(renderer, texture, NULL, 1.0f, &dstRect);
} |
|
- В функции SDL_AppIterate() измените и добавьте код таким образом:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
SDL_FRect srcRect = { 0, 0, 70, 70 };
SDL_FRect destRect = { 150, 250, 100, 100 };
// Рисуем ПОЛ плиткой (Tiled)
RenderFloorTiled(floorBodyId, grass_texture, 300.f, 20.f);
// Рисуем спрайты поверх физических тел
// Размеры берем те же, что задавали при создании коллайдеров (умноженные на 2, т.к. b2MakeBox берет полуобхват)
RenderBody(squareBodyId, alien_green_square_texture, 30.f, 30.f);
RenderBody(circleBodyId, alien_yellow_round_texture, 40.f, 40.f); // диаметр = радиус 20 * 2
b2World_Draw(worldId, &debugDrawer); |
|
- Примечание. Читайте комментарии в коде
- Текущий код в 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
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
| #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;
static SDL_Texture *alien_green_square_texture = NULL;
static SDL_Texture *alien_yellow_round_texture = NULL;
static SDL_Texture *grass_texture = NULL;
const SDL_PixelFormatDetails *format = NULL;
float ppm = 30.f; // Pixels per meter
static float accumulator = 0.0f;
static Uint64 last_ticks = 0;
static b2BodyId floorBodyId;
static b2BodyId squareBodyId;
static b2BodyId circleBodyId;
SDL_Texture *LoadTexture(const char *path)
{
SDL_Surface *surface = SDL_LoadPNG(path);
if (!surface)
{
SDL_Log("Load failed (%s): %s", path, SDL_GetError());
return NULL;
}
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_DestroySurface(surface);
return texture;
}
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);
alien_green_square_texture = LoadTexture("./assets/sprites/alienGreen_square.png");
alien_yellow_round_texture = LoadTexture("./assets/sprites/alienYellow_round.png");
grass_texture = LoadTexture("./assets/sprites/grass.png");
// Создаём физический мир 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 };
floorBodyId = b2CreateBody(worldId, &floorBodyDef);
// Половина ширины 150, половина высоты 10. Итого весь бокс 300x20 пикселей.
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;
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;
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;
}
void RenderBody(b2BodyId bodyId, SDL_Texture *texture, float width, float height)
{
// 1. Получаем трансформацию тела из Box2D
b2Transform transform = b2Body_GetTransform(bodyId);
b2Vec2 pos = transform.p;
float angleRadians = b2Rot_GetAngle(transform.q);
// 2. Конвертируем метры в пиксели и радианы в градусы
float centerX = pos.x * ppm;
float centerY = pos.y * ppm;
float angleDegrees = angleRadians * (180.0f / SDL_PI_F);
// 3. Определяем прямоугольник отрисовки
// SDL рисует от верхнего левого угла, поэтому смещаем координаты на половину размера
SDL_FRect destRect = {
centerX - (width / 2.0f),
centerY - (height / 2.0f),
width,
height
};
// 4. Отрисовка с учетом поворота
// NULL в качестве центра означает вращение вокруг центра destRect
SDL_RenderTextureRotated(renderer, texture, NULL, &destRect, (double)angleDegrees, NULL, SDL_FLIP_NONE);
}
void RenderFloorTiled(b2BodyId bodyId, SDL_Texture *texture, float floorWidth, float floorHeight)
{
// 1. Получаем позицию из Box2D (в метрах)
b2Vec2 pos = b2Body_GetPosition(bodyId);
// 2. Переводим координаты центра из метров в пиксели
float centerX = pos.x * ppm;
float centerY = pos.y * ppm;
// 3. Целевой прямоугольник для отрисовки всего пола
SDL_FRect dstRect = {
centerX - (floorWidth / 2.0f),
centerY - (floorHeight / 2.0f),
floorWidth,
floorHeight
};
// 4. Вызываем исправленную функцию с 5 аргументами (добавлен 1.0f как масштаб)
// Параметры: renderer, texture, src_rect (NULL = вся текстура), scale, dst_rect
SDL_RenderTextureTiled(renderer, texture, NULL, 1.0f, &dstRect);
}
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);
SDL_FRect srcRect = { 0, 0, 70, 70 };
SDL_FRect destRect = { 150, 250, 100, 100 };
// Рисуем ПОЛ плиткой (Tiled)
RenderFloorTiled(floorBodyId, grass_texture, 300.f, 20.f);
// Рисуем спрайты поверх физических тел
// Размеры берем те же, что задавали при создании коллайдеров (умноженные на 2, т.к. b2MakeBox берет полуобхват)
RenderBody(squareBodyId, alien_green_square_texture, 30.f, 30.f);
RenderBody(circleBodyId, alien_yellow_round_texture, 40.f, 40.f); // диаметр = радиус 20 * 2
b2World_Draw(worldId, &debugDrawer);
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
if (alien_green_square_texture)
{
SDL_DestroyTexture(alien_green_square_texture);
alien_green_square_texture = NULL;
}
if (alien_yellow_round_texture)
{
SDL_DestroyTexture(alien_yellow_round_texture);
alien_yellow_round_texture = NULL;
}
if (grass_texture)
{
SDL_DestroyTexture(grass_texture);
grass_texture = NULL;
}
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
- После сборки командой "build-web" и обновления вкладки браузера вы увидите, что графика рисуется внутри коллайдеров:

- Для production нужно отключить рисование коллайдеров, то есть закомментировать эту строку:
| C | 1
| // b2World_Draw(worldId, &debugDrawer); |
|
- Коллайдеры скрыты:

- Для сборки в Release поменяйте Debug на Release в файле config-web.bat
| Bash | 1
| emcmake cmake -S . -B dist -DCMAKE_BUILD_TYPE=Release |
|
- Размер сборки в Release:
- app.js - 185 KB
- app.wasm - 916 KB
- Выполните команды в консоле: "config-web" и "build-web". Теперь папку "public" вы можете загрузить на бесплатный хостинг: GitHub Pages, Firebase и т.д.
- Итоговый код: finish-sync-physics-sprites-sdl3-c.zip
|