Содержание блога
Финальная демка в браузере. Итоговый код: finish-collision-events-sdl3-c.zip Сканируйте QR-код на мобильном и вы увидите, что появится джойстик для управления главным героем.

Одним из самый важных элементов большинства игр является обработка событий коллизий и идентификация объектов между которыми произошла коллизия. Конечно, Box2D имеет встроенные средства для обработки событий коллизий. Пошагово создадим пример, где главный герой будет касаться трёх стен по очереди, на экран будет выводиться текст, показывающий до какой стены дотронулся ГГ.
Подключение библиотек к стартовому примеру
- Установите Emscripten 4.0.15 и CMake по инструкции: Установка Emscripten SDK (emsdk) и CMake
- Скачайте пример: start-collision-events-sdl3-c.zip, который мы создали пошагово по инструкции: Реализация движения на Box2D v3 - трение и коллизии с повёрнутыми стенами
- Примечание. В пошаговой инструкции выше использовалась SDL 3.4.0, а мы подключим более новую версию SDL3 3.4.2
- Так выглядит код стартового примера:
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
| #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;
typedef struct
{
bool up;
bool down;
bool left;
bool right;
} Keys;
static Keys keys = { 0 };
b2BodyId circleBodyId;
static const float playerSpeed = 5.f;
static bool is_mobile = false;
static const int BTN_SIZE = 60;
static const int BTN_GAP = 5;
static SDL_FRect btnUp, btnDown, btnLeft, btnRight;
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
// This JavaScript function returns true if the user agent matches mobile devices
EM_JS(bool, is_mobile_browser, (),
{
// We use the RegExp constructor to avoid using / / slashes which confuse Clang
var filter = new RegExp('iPhone|iPad|iPod|Android', 'i');
return filter.test(navigator.userAgent);
});
#endif
void updateButtonLayout(int w, int h)
{
// Positioning squares in a cross pattern at the bottom-left
float startX = 20.0f;
float startY = h - (BTN_SIZE * 3) - 20.0f;
btnUp = (SDL_FRect) { startX + BTN_SIZE + BTN_GAP, startY, BTN_SIZE, BTN_SIZE };
btnLeft = (SDL_FRect) { startX, startY + BTN_SIZE + BTN_GAP, BTN_SIZE, BTN_SIZE };
btnRight = (SDL_FRect) { startX + (BTN_SIZE + BTN_GAP) * 2, startY + BTN_SIZE + BTN_GAP, BTN_SIZE, BTN_SIZE };
btnDown = (SDL_FRect) { startX + BTN_SIZE + BTN_GAP, startY + (BTN_SIZE + BTN_GAP) * 2, BTN_SIZE, BTN_SIZE };
}
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;
}
// Platform detection
#ifdef __EMSCRIPTEN__
is_mobile = is_mobile_browser();
#elif defined(__ANDROID__) || defined(__IPHONEOS__)
is_mobile = true;
#endif
printf("Is Mobile: %s\n", is_mobile ? "Yes" : "No");
SDL_SetRenderVSync(renderer, 1);
// Initial Layout
int w, h;
SDL_GetWindowSize(window, &w, &h);
updateButtonLayout(w, h);
// Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 0.f };
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;
const float friction = 0.1f;
// Top wall
{
b2BodyDef topWallBodyDef = b2DefaultBodyDef();
topWallBodyDef.type = b2_staticBody;
topWallBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
topWallBodyDef.rotation = b2MakeRot(20.0f * (3.14f / 180.0f));
b2BodyId topWallBodyId = b2CreateBody(worldId, &topWallBodyDef);
b2Polygon topWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef topWallShapeDef = b2DefaultShapeDef();
topWallShapeDef.material.friction = friction;
topWallShapeDef.enableContactEvents = true;
b2ShapeId topWallShapeId = b2CreatePolygonShape(topWallBodyId, &topWallShapeDef, &topWallShape);
}
// Left wall
{
b2BodyDef leftWallBodyDef = b2DefaultBodyDef();
leftWallBodyDef.type = b2_staticBody;
leftWallBodyDef.position = (b2Vec2) { 120.f / ppm, 230.f / ppm };
leftWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId leftWallBodyId = b2CreateBody(worldId, &leftWallBodyDef);
b2Polygon leftWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef leftWallShapeDef = b2DefaultShapeDef();
leftWallShapeDef.material.friction = friction;
leftWallShapeDef.enableContactEvents = true;
b2ShapeId leftWallShapeId = b2CreatePolygonShape(leftWallBodyId, &leftWallShapeDef, &leftWallShape);
}
// Right wall
{
b2BodyDef rightWallBodyDef = b2DefaultBodyDef();
rightWallBodyDef.type = b2_staticBody;
rightWallBodyDef.position = (b2Vec2) { 280.f / ppm, 285.f / ppm };
rightWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId rightWallBodyId = b2CreateBody(worldId, &rightWallBodyDef);
b2Polygon rightWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef rightWallShapeDef = b2DefaultShapeDef();
rightWallShapeDef.material.friction = friction;
rightWallShapeDef.enableContactEvents = true;
b2ShapeId rightWallShapeId = b2CreatePolygonShape(rightWallBodyId, &rightWallShapeDef, &rightWallShape);
}
// Circle
b2BodyDef circleBodyDef = b2DefaultBodyDef();
circleBodyDef.type = b2_dynamicBody;
circleBodyDef.position = (b2Vec2) { 200.f / ppm, 250.f / ppm };
circleBodyDef.enableSleep = false;
circleBodyDef.fixedRotation = true;
circleBodyId = b2CreateBody(worldId, &circleBodyDef);
b2Circle circleCircle = {
.center = { 0.0f, 0.0f }, // <--- ВАЖНО: 0,0 означает, что центр круга совпадает с центром тела
.radius = 20.f / ppm // Радиус в метрах
};
b2ShapeDef circleShapeDef = b2DefaultShapeDef();
circleShapeDef.density = 1.0f;
circleShapeDef.material.friction = friction;
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;
}
if (event->type == SDL_EVENT_KEY_DOWN)
{
SDL_Scancode sc = event->key.scancode;
if (sc == SDL_SCANCODE_W || sc == SDL_SCANCODE_UP)
keys.up = true;
if (sc == SDL_SCANCODE_S || sc == SDL_SCANCODE_DOWN)
keys.down = true;
if (sc == SDL_SCANCODE_A || sc == SDL_SCANCODE_LEFT)
keys.left = true;
if (sc == SDL_SCANCODE_D || sc == SDL_SCANCODE_RIGHT)
keys.right = true;
}
if (event->type == SDL_EVENT_KEY_UP)
{
SDL_Scancode sc = event->key.scancode;
if (sc == SDL_SCANCODE_W || sc == SDL_SCANCODE_UP)
keys.up = false;
if (sc == SDL_SCANCODE_S || sc == SDL_SCANCODE_DOWN)
keys.down = false;
if (sc == SDL_SCANCODE_A || sc == SDL_SCANCODE_LEFT)
keys.left = false;
if (sc == SDL_SCANCODE_D || sc == SDL_SCANCODE_RIGHT)
keys.right = false;
}
// Touch handling
if (event->type == SDL_EVENT_FINGER_DOWN || event->type == SDL_EVENT_FINGER_UP || event->type == SDL_EVENT_FINGER_MOTION)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float tx = event->tfinger.x * w;
float ty = event->tfinger.y * h;
bool pressed = (event->type != SDL_EVENT_FINGER_UP);
SDL_Point pt = { (int)tx, (int)ty };
// Check rectangles (Manual check since FRect is float)
if (tx >= btnUp.x && tx <= btnUp.x + btnUp.w && ty >= btnUp.y && ty <= btnUp.y + btnUp.h)
keys.up = pressed;
if (tx >= btnDown.x && tx <= btnDown.x + btnDown.w && ty >= btnDown.y && ty <= btnDown.y + btnDown.h)
keys.down = pressed;
if (tx >= btnLeft.x && tx <= btnLeft.x + btnLeft.w && ty >= btnLeft.y && ty <= btnLeft.y + btnLeft.h)
keys.left = pressed;
if (tx >= btnRight.x && tx <= btnRight.x + btnRight.w && ty >= btnRight.y && ty <= btnRight.y + btnRight.h)
keys.right = pressed;
}
return SDL_APP_CONTINUE;
}
void keyboard(void)
{
b2Vec2 velocity = { 0.0f, 0.0f };
if (keys.up)
{
velocity.y = -playerSpeed;
}
else if (keys.down)
{
velocity.y = playerSpeed;
}
else if (keys.left)
{
velocity.x = -playerSpeed;
}
else if (keys.right)
{
velocity.x = playerSpeed;
}
b2Body_SetLinearVelocity(circleBodyId, velocity);
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
keyboard();
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);
// Render Touch UI only if mobile/web
if (is_mobile)
{
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 100); // Semi-transparent white
SDL_RenderFillRect(renderer, &btnUp);
SDL_RenderFillRect(renderer, &btnDown);
SDL_RenderFillRect(renderer, &btnLeft);
SDL_RenderFillRect(renderer, &btnRight);
}
// 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 |
|
CMakeLists.txt
| 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
| cmake_minimum_required(VERSION 3.21)
project(start-collision-events-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
) |
|
- Напомню, что результатом этого стартового примера являются перемещение главного героя с помощью клавиш WASD (и клавиш стрелок) на в браузерах на Desktop и при запуске на мобильных браузерах появляется простейший джойстик из 4-х кнопок, которые мы рисуем квадратами. Запустить демку можно по этой ссылке

- Запустим стартовый пример, а для этого скачаем и подключим библиотеки SDL3 и Box2D, а так же мы скачаем и подключим библиотеки FreeType и SDL3_ttf для вывода текста
- Откройте папку со стартовым примером в какой-нибудь редакторе кода, например, в 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
- Скачайте библиотеку FreeType для Wasm: freetype-2.14.1-wasm
- Скачайте библиотеку SDL3_ttf для Wasm: SDL3_ttf-devel-3.2.2-wasm.zip
- Извлеките содержимое архив выше в какую-нибудь новую общую папку, например, с именем "libs" на какой-нибудь диск, например, на C. Таким образом, создайте на диске C папку "lib", скопируйте в неё архивы и извлеките их в текущую папку:

- Откройте папку "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") |
|
- Проделайте почти тоже самое для подключения библиотек FreeType и SDL3_ttf, то есть сначала копируйте пути:


- А потом копируйте пути в CMakeLists.txt, создавая соответствующую переменную с суффиксом _DIR, но обратите внимание, что для FreeType нужно создать переменные FREETYPE_INCLUDE_DIRS и FREETYPE_LIBRARY и изменить пути, как показано в коде:
| Bash | 1
2
3
| set(FREETYPE_INCLUDE_DIRS "C:/libs/freetype-2.14.1-wasm/include")
set(FREETYPE_LIBRARY "C:/libs/freetype-2.14.1-wasm/lib/libfreetype.a")
set(SDL3_ttf_DIR "C:/libs/SDL3_ttf-devel-3.2.2-wasm/lib/cmake/SDL3_ttf") |
|
- В итоге получается такой код подключения файлов библиотек:
| Bash | 1
2
3
4
5
6
| # Указываем точное местоположение конфигурационных файлов библиотек
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")
set(FREETYPE_INCLUDE_DIRS "C:/libs/freetype-2.14.1-wasm/include")
set(FREETYPE_LIBRARY "C:/libs/freetype-2.14.1-wasm/lib/libfreetype.a")
set(SDL3_ttf_DIR "C:/libs/SDL3_ttf-devel-3.2.2-wasm/lib/cmake/SDL3_ttf") |
|
- После команды find_package(box2d REQUIRED) добавим аналогичную команду для SDL3_ttf:
| Bash | 1
2
3
4
5
6
| # Проверяем наличие библиотек в системе
# Если она не будет найдены, CMake прервет настройку с ошибкой
# REQUIRED - переводится, как «обязательно» или «требуется»
find_package(SDL3 REQUIRED)
find_package(box2d REQUIRED)
find_package(SDL3_ttf REQUIRED) |
|
- Добавьте SDL3_ttf::SDL3_ttf в настройку линковки:
| Bash | 1
2
| # Привязываем библиотеки к нашему приложению (настройка линковки и путей include)
target_link_libraries(app PRIVATE box2d::box2d SDL3_ttf::SDL3_ttf SDL3::SDL3) |
|
- По следующей инструкции проведём тестовую сборку, чтобы убедиться, что библиотеки подключены и пример работает
Тестовая сборка и тестовый запуск стартового примера в браузере на локальном хостинге
- Откройте консоль (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
Что такое userData и как использовать
userData - это возможность к каждому объекты прикрепить пользовательские данные. В нашем случае к каждому объекты прикрепим имя. Это нужно для того, чтобы при возникновении события коллизии между двумя объектами мы могли определить по именам - между какими двумя объектами произошла коллизия и таким образом идентифицировать объекты. Добавим userData с именами объектов по следующей инструкции:
- В файле src/main.c найдите код создания создания коллайдера верхней стены:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Top wall
{
b2BodyDef topWallBodyDef = b2DefaultBodyDef();
topWallBodyDef.type = b2_staticBody;
topWallBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
topWallBodyDef.rotation = b2MakeRot(20.0f * (3.14f / 180.0f));
b2BodyId topWallBodyId = b2CreateBody(worldId, &topWallBodyDef);
b2Polygon topWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef topWallShapeDef = b2DefaultShapeDef();
topWallShapeDef.material.friction = friction;
topWallShapeDef.enableContactEvents = true;
b2ShapeId topWallShapeId = b2CreatePolygonShape(topWallBodyId, &topWallShapeDef, &topWallShape);
} |
|
- В конце блока с фигурными скобками добавьте имя для верхней стены:
| C | 1
2
3
4
5
6
| // Top wall
{
// ...
// User data
b2Shape_SetUserData(topWallShapeId, "top_wall");
} |
|
- Тоже самое проделайте для боковых стен и для для коллайдера главного героя. В итоге получился следующий код:
| 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
| // Top wall
{
b2BodyDef topWallBodyDef = b2DefaultBodyDef();
topWallBodyDef.type = b2_staticBody;
topWallBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
topWallBodyDef.rotation = b2MakeRot(20.0f * (3.14f / 180.0f));
b2BodyId topWallBodyId = b2CreateBody(worldId, &topWallBodyDef);
b2Polygon topWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef topWallShapeDef = b2DefaultShapeDef();
topWallShapeDef.material.friction = friction;
topWallShapeDef.enableContactEvents = true;
b2ShapeId topWallShapeId = b2CreatePolygonShape(topWallBodyId, &topWallShapeDef, &topWallShape);
// User data
b2Shape_SetUserData(topWallShapeId, "top_wall");
}
// Left wall
{
b2BodyDef leftWallBodyDef = b2DefaultBodyDef();
leftWallBodyDef.type = b2_staticBody;
leftWallBodyDef.position = (b2Vec2) { 120.f / ppm, 230.f / ppm };
leftWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId leftWallBodyId = b2CreateBody(worldId, &leftWallBodyDef);
b2Polygon leftWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef leftWallShapeDef = b2DefaultShapeDef();
leftWallShapeDef.material.friction = friction;
leftWallShapeDef.enableContactEvents = true;
b2ShapeId leftWallShapeId = b2CreatePolygonShape(leftWallBodyId, &leftWallShapeDef, &leftWallShape);
// User data
b2Shape_SetUserData(leftWallShapeId, "left_wall");
}
// Right wall
{
b2BodyDef rightWallBodyDef = b2DefaultBodyDef();
rightWallBodyDef.type = b2_staticBody;
rightWallBodyDef.position = (b2Vec2) { 280.f / ppm, 285.f / ppm };
rightWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId rightWallBodyId = b2CreateBody(worldId, &rightWallBodyDef);
b2Polygon rightWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef rightWallShapeDef = b2DefaultShapeDef();
rightWallShapeDef.material.friction = friction;
rightWallShapeDef.enableContactEvents = true;
b2ShapeId rightWallShapeId = b2CreatePolygonShape(rightWallBodyId, &rightWallShapeDef, &rightWallShape);
// User data
b2Shape_SetUserData(rightWallShapeId, "right_wall");
}
// Circle
b2BodyDef circleBodyDef = b2DefaultBodyDef();
circleBodyDef.type = b2_dynamicBody;
circleBodyDef.position = (b2Vec2) { 200.f / ppm, 250.f / ppm };
circleBodyDef.enableSleep = false;
circleBodyDef.fixedRotation = true;
circleBodyId = b2CreateBody(worldId, &circleBodyDef);
b2Circle circleCircle = {
.center = { 0.0f, 0.0f }, // <--- ВАЖНО: 0,0 означает, что центр круга совпадает с центром тела
.radius = 20.f / ppm // Радиус в метрах
};
b2ShapeDef circleShapeDef = b2DefaultShapeDef();
circleShapeDef.density = 1.0f;
circleShapeDef.material.friction = friction;
b2ShapeId circleShapeId = b2CreateCircleShape(circleBodyId, &circleShapeDef, &circleCircle);
// User data
b2Shape_SetUserData(circleShapeId, "player"); |
|
- Теперь у каждого объекта на сцене есть своё имя. Это имя мы будет использовать, чтобы понять какие два коллайдера столкнулись и какой коллайдер является коллайдером главного героя, а какой является определённой стеной
Обработчик событий коллизий
Для удобства организации кода, обработчики событий коллизий будем писать в отдельном файле с именем "contact-listener.h"
- Создайте новый файл с именем "contact-listener.h" в папке "src". Скопируйте следующий код в файл "contact-listener.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
| #ifndef CONTACT_LISTENER_H
#define CONTACT_LISTENER_H
#include <box2d/box2d.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
typedef struct
{
char text[128];
bool dirty;
} ContactListenerData;
// Call this every frame after stepping the world
void ProcessContactEvents(b2WorldId worldId, ContactListenerData *listener)
{
b2ContactEvents events = b2World_GetContactEvents(worldId);
// --- Begin contacts ---
for (int i = 0; i < events.beginCount; i++)
{
b2ContactBeginTouchEvent event = events.beginEvents[i];
const char *nameA = (const char *)b2Shape_GetUserData(event.shapeIdA);
const char *nameB = (const char *)b2Shape_GetUserData(event.shapeIdB);
if (!nameA) nameA = "unknown";
if (!nameB) nameB = "unknown";
const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA;
snprintf(listener->text, sizeof(listener->text),
"Begin contact with %s", wallName);
listener->dirty = true;
}
// --- End contacts ---
for (int i = 0; i < events.endCount; i++)
{
b2ContactEndTouchEvent event = events.endEvents[i];
const char *nameA = (const char *)b2Shape_GetUserData(event.shapeIdA);
const char *nameB = (const char *)b2Shape_GetUserData(event.shapeIdB);
if (!nameA) nameA = "unknown";
if (!nameB) nameB = "unknown";
const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA;
snprintf(listener->text, sizeof(listener->text),
"End contact with %s", wallName);
listener->dirty = true;
}
}
#endif // CONTACT_LISTENER_H |
|
- Функцию ProcessContactEvents() в коде выше нужно будет вызывать в главном цикле приложения сразу после вызова b2World_Step(), то есть после каждого тика физического мира
- Комментарии к тому, что происходит внутри функции ProcessContactEvents(). Я не будут расписывать это сам построчно. Я предлагаю скопировать весь этот код в Gemini и написать ему перед скопированным кодом "Объясни простыми словами для начинающего, что происходит в следующем коде":
Вот что мне выдал Gemini
Этот код — типичный обработчик столкновений для физического движка Box2D (версия 3.x). Если говорить совсем просто: это «уши» твоей игры, которые слушают, когда один объект касается другого, и записывают это событие текстом.
Давай разберем его по частям.
- Структура данных (Контейнер для информации)
| C | 1
2
3
4
| typedef struct {
char text[128]; // Сюда запишем сообщение, например: "Начало контакта с lef_wall"
bool dirty; // Флаг-сигнал: "Эй, текст изменился, обнови его на экране!"
} ContactListenerData; |
|
Эта структура нужна, чтобы передавать информацию о столкновениях из физического мира в графическую часть игры (например, чтобы вывести надпись в углу экрана).
- Главная функция: ProcessContactEvents
Она работает в два этапа: обрабатывает начало касания и конец касания.
Этап А: Начало касания (Begin contacts)
Когда два объекта ударяются друг о друга, Box2D создает событие.
- Получение имен: Код берет два объекта (shapeIdA и shapeIdB) и спрашивает: "Как вас зовут?". Имена хранятся в UserData.
- Логика "Кто не игрок?": Программа ищет объект "player". Если первый объект — игрок, значит нам интересно имя второго. Так мы узнаем, обо что именно ударился игрок (о стену, о врага и т.д.).
| C | 1
| const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA; |
|
- Запись: Текст "Начало контакта с [Имя]" копируется в буфер, а флаг dirty ставится в true.
Этап Б: Конец касания (End contacts)
Здесь происходит ровно то же самое, но в момент, когда объекты разлетаются в разные стороны. Текст меняется на "End contact with...".
Как это работает визуально
Представь, что у тебя есть круг (Player) и квадрат (Box).
- Как только круг касается края квадрата, срабатывает первый цикл for, и в listener->text попадает фраза: "Begin contact with Box".
- Пока они соприкасаются, ничего не происходит.
- Как только круг отлетает от квадрата, срабатывает второй цикл, и текст меняется на: "End contact with Box".
Важные нюансы для новичка
- #ifndef / #define: Это «защита от дурака». Она гарантирует, что если ты случайно подключишь этот файл в нескольких местах, компилятор не сойдет с ума от дубликатов.
- snprintf: Это безопасный способ склеить строку и переменную. Она следит, чтобы текст не вылез за пределы выделенных 128 байт (чтобы программа не "сломалась").
- b2World_GetContactEvents: Важно помнить, что в Box2D 3.0 события нужно запрашивать самому каждый кадр, они не прилетают автоматически.
- Вы можете копировать отдельные строки кода или несколько строк кода и просить Gemini подробнее объяснить
- Откройте файл "src/main.c"
- Добавьте подключение файла "contact-listener.h":
| C | 1
| #include "contact-listener.h" |
|
- Добавьте глобальную переменную "contactData":
| C | 1
| static ContactListenerData contactData; |
|
- В функции SDL_AppInit() после кода вывода значения гравитации в консоль добавьте код инициализации структуры "contactData":
| C | 1
2
| strcpy(contactData.text, "Коснитесь стены");
contactData.dirty = true; |
|
- В функции SDL_AppIterate() после вызова b2World_Step() добавьте вызов ProcessContactEvents():
| C | 1
2
3
4
5
6
7
8
9
10
| while (accumulator >= TIME_STEP)
{
// Step physics
b2World_Step(worldId, TIME_STEP, 3);
// Read contact events immediately
ProcessContactEvents(worldId, &contactData);
accumulator -= TIME_STEP;
} |
|
- Добавим в консоль вывод текста при срабатывании события начала контакта в файле "contact-listener.h". В обоих цикла ("Begin contacts" и "End contacts") добавьте строку вывода текста после функций snprintf():
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // --- Begin contacts ---
for (int i = 0; i < events.beginCount; i++)
{
// ...
printf("%s\n", listener->text);
}
// --- End contacts ---
for (int i = 0; i < events.endCount; i++)
{
// ...
printf("%s\n", listener->text);
} |
|
- Сверьте код:
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
| #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>
#include "contact-listener.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 ContactListenerData contactData;
const SDL_PixelFormatDetails *format = NULL;
float ppm = 30.f; // Pixels per meter
static float accumulator = 0.0f;
static Uint64 last_ticks = 0;
typedef struct
{
bool up;
bool down;
bool left;
bool right;
} Keys;
static Keys keys = { 0 };
b2BodyId circleBodyId;
static const float playerSpeed = 5.f;
static bool is_mobile = false;
static const int BTN_SIZE = 60;
static const int BTN_GAP = 5;
static SDL_FRect btnUp, btnDown, btnLeft, btnRight;
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
// This JavaScript function returns true if the user agent matches mobile devices
EM_JS(bool, is_mobile_browser, (),
{
// We use the RegExp constructor to avoid using / / slashes which confuse Clang
var filter = new RegExp('iPhone|iPad|iPod|Android', 'i');
return filter.test(navigator.userAgent);
});
#endif
void updateButtonLayout(int w, int h)
{
// Positioning squares in a cross pattern at the bottom-left
float startX = 20.0f;
float startY = h - (BTN_SIZE * 3) - 20.0f;
btnUp = (SDL_FRect) { startX + BTN_SIZE + BTN_GAP, startY, BTN_SIZE, BTN_SIZE };
btnLeft = (SDL_FRect) { startX, startY + BTN_SIZE + BTN_GAP, BTN_SIZE, BTN_SIZE };
btnRight = (SDL_FRect) { startX + (BTN_SIZE + BTN_GAP) * 2, startY + BTN_SIZE + BTN_GAP, BTN_SIZE, BTN_SIZE };
btnDown = (SDL_FRect) { startX + BTN_SIZE + BTN_GAP, startY + (BTN_SIZE + BTN_GAP) * 2, BTN_SIZE, BTN_SIZE };
}
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;
}
// Platform detection
#ifdef __EMSCRIPTEN__
is_mobile = is_mobile_browser();
#elif defined(__ANDROID__) || defined(__IPHONEOS__)
is_mobile = true;
#endif
printf("Is Mobile: %s\n", is_mobile ? "Yes" : "No");
SDL_SetRenderVSync(renderer, 1);
// Initial Layout
int w, h;
SDL_GetWindowSize(window, &w, &h);
updateButtonLayout(w, h);
// Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 0.f };
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);
strcpy(contactData.text, "Коснитесь стены");
contactData.dirty = true;
debugDrawer = b2DefaultDebugDraw();
debugDrawer.drawShapes = true;
debugDrawer.DrawSolidPolygonFcn = drawSolidPolygon;
debugDrawer.DrawSolidCircleFcn = drawSolidCircle;
const float friction = 0.1f;
// Top wall
{
b2BodyDef topWallBodyDef = b2DefaultBodyDef();
topWallBodyDef.type = b2_staticBody;
topWallBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
topWallBodyDef.rotation = b2MakeRot(20.0f * (3.14f / 180.0f));
b2BodyId topWallBodyId = b2CreateBody(worldId, &topWallBodyDef);
b2Polygon topWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef topWallShapeDef = b2DefaultShapeDef();
topWallShapeDef.material.friction = friction;
topWallShapeDef.enableContactEvents = true;
b2ShapeId topWallShapeId = b2CreatePolygonShape(topWallBodyId, &topWallShapeDef, &topWallShape);
// User data
b2Shape_SetUserData(topWallShapeId, "top_wall");
}
// Left wall
{
b2BodyDef leftWallBodyDef = b2DefaultBodyDef();
leftWallBodyDef.type = b2_staticBody;
leftWallBodyDef.position = (b2Vec2) { 120.f / ppm, 230.f / ppm };
leftWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId leftWallBodyId = b2CreateBody(worldId, &leftWallBodyDef);
b2Polygon leftWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef leftWallShapeDef = b2DefaultShapeDef();
leftWallShapeDef.material.friction = friction;
leftWallShapeDef.enableContactEvents = true;
b2ShapeId leftWallShapeId = b2CreatePolygonShape(leftWallBodyId, &leftWallShapeDef, &leftWallShape);
// User data
b2Shape_SetUserData(leftWallShapeId, "left_wall");
}
// Right wall
{
b2BodyDef rightWallBodyDef = b2DefaultBodyDef();
rightWallBodyDef.type = b2_staticBody;
rightWallBodyDef.position = (b2Vec2) { 280.f / ppm, 285.f / ppm };
rightWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId rightWallBodyId = b2CreateBody(worldId, &rightWallBodyDef);
b2Polygon rightWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef rightWallShapeDef = b2DefaultShapeDef();
rightWallShapeDef.material.friction = friction;
rightWallShapeDef.enableContactEvents = true;
b2ShapeId rightWallShapeId = b2CreatePolygonShape(rightWallBodyId, &rightWallShapeDef, &rightWallShape);
// User data
b2Shape_SetUserData(rightWallShapeId, "right_wall");
}
// Circle
b2BodyDef circleBodyDef = b2DefaultBodyDef();
circleBodyDef.type = b2_dynamicBody;
circleBodyDef.position = (b2Vec2) { 200.f / ppm, 250.f / ppm };
circleBodyDef.enableSleep = false;
circleBodyDef.fixedRotation = true;
circleBodyId = b2CreateBody(worldId, &circleBodyDef);
b2Circle circleCircle = {
.center = { 0.0f, 0.0f }, // <--- ВАЖНО: 0,0 означает, что центр круга совпадает с центром тела
.radius = 20.f / ppm // Радиус в метрах
};
b2ShapeDef circleShapeDef = b2DefaultShapeDef();
circleShapeDef.density = 1.0f;
circleShapeDef.material.friction = friction;
b2ShapeId circleShapeId = b2CreateCircleShape(circleBodyId, &circleShapeDef, &circleCircle);
// User data
b2Shape_SetUserData(circleShapeId, "player");
// 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;
}
if (event->type == SDL_EVENT_KEY_DOWN)
{
SDL_Scancode sc = event->key.scancode;
if (sc == SDL_SCANCODE_W || sc == SDL_SCANCODE_UP)
keys.up = true;
if (sc == SDL_SCANCODE_S || sc == SDL_SCANCODE_DOWN)
keys.down = true;
if (sc == SDL_SCANCODE_A || sc == SDL_SCANCODE_LEFT)
keys.left = true;
if (sc == SDL_SCANCODE_D || sc == SDL_SCANCODE_RIGHT)
keys.right = true;
}
if (event->type == SDL_EVENT_KEY_UP)
{
SDL_Scancode sc = event->key.scancode;
if (sc == SDL_SCANCODE_W || sc == SDL_SCANCODE_UP)
keys.up = false;
if (sc == SDL_SCANCODE_S || sc == SDL_SCANCODE_DOWN)
keys.down = false;
if (sc == SDL_SCANCODE_A || sc == SDL_SCANCODE_LEFT)
keys.left = false;
if (sc == SDL_SCANCODE_D || sc == SDL_SCANCODE_RIGHT)
keys.right = false;
}
// Touch handling
if (event->type == SDL_EVENT_FINGER_DOWN || event->type == SDL_EVENT_FINGER_UP || event->type == SDL_EVENT_FINGER_MOTION)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float tx = event->tfinger.x * w;
float ty = event->tfinger.y * h;
bool pressed = (event->type != SDL_EVENT_FINGER_UP);
SDL_Point pt = { (int)tx, (int)ty };
// Check rectangles (Manual check since FRect is float)
if (tx >= btnUp.x && tx <= btnUp.x + btnUp.w && ty >= btnUp.y && ty <= btnUp.y + btnUp.h)
keys.up = pressed;
if (tx >= btnDown.x && tx <= btnDown.x + btnDown.w && ty >= btnDown.y && ty <= btnDown.y + btnDown.h)
keys.down = pressed;
if (tx >= btnLeft.x && tx <= btnLeft.x + btnLeft.w && ty >= btnLeft.y && ty <= btnLeft.y + btnLeft.h)
keys.left = pressed;
if (tx >= btnRight.x && tx <= btnRight.x + btnRight.w && ty >= btnRight.y && ty <= btnRight.y + btnRight.h)
keys.right = pressed;
}
return SDL_APP_CONTINUE;
}
void keyboard(void)
{
b2Vec2 velocity = { 0.0f, 0.0f };
if (keys.up)
{
velocity.y = -playerSpeed;
}
else if (keys.down)
{
velocity.y = playerSpeed;
}
else if (keys.left)
{
velocity.x = -playerSpeed;
}
else if (keys.right)
{
velocity.x = playerSpeed;
}
b2Body_SetLinearVelocity(circleBodyId, velocity);
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
keyboard();
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);
// Read contact events immediately
ProcessContactEvents(worldId, &contactData);
accumulator -= TIME_STEP;
}
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
b2World_Draw(worldId, &debugDrawer);
// Render Touch UI only if mobile/web
if (is_mobile)
{
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 100); // Semi-transparent white
SDL_RenderFillRect(renderer, &btnUp);
SDL_RenderFillRect(renderer, &btnDown);
SDL_RenderFillRect(renderer, &btnLeft);
SDL_RenderFillRect(renderer, &btnRight);
}
// 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/contact-listener.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
| #ifndef CONTACT_LISTENER_H
#define CONTACT_LISTENER_H
#include <box2d/box2d.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
typedef struct
{
char text[128];
bool dirty;
} ContactListenerData;
// Call this every frame after stepping the world
void ProcessContactEvents(b2WorldId worldId, ContactListenerData *listener)
{
b2ContactEvents events = b2World_GetContactEvents(worldId);
// --- Begin contacts ---
for (int i = 0; i < events.beginCount; i++)
{
b2ContactBeginTouchEvent event = events.beginEvents[i];
const char *nameA = (const char *)b2Shape_GetUserData(event.shapeIdA);
const char *nameB = (const char *)b2Shape_GetUserData(event.shapeIdB);
if (!nameA) nameA = "unknown";
if (!nameB) nameB = "unknown";
const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA;
snprintf(listener->text, sizeof(listener->text),
"Начало контакта с %s", wallName);
listener->dirty = true;
printf("%s\n", listener->text);
}
// --- End contacts ---
for (int i = 0; i < events.endCount; i++)
{
b2ContactEndTouchEvent event = events.endEvents[i];
const char *nameA = (const char *)b2Shape_GetUserData(event.shapeIdA);
const char *nameB = (const char *)b2Shape_GetUserData(event.shapeIdB);
if (!nameA) nameA = "unknown";
if (!nameB) nameB = "unknown";
const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA;
snprintf(listener->text, sizeof(listener->text),
"Конец контакта с %s", wallName);
listener->dirty = true;
printf("%s\n", listener->text);
}
}
#endif // CONTACT_LISTENER_H |
|
- После сборки проекта командой "build-web" и обновления вкладки браузера вы можете, управляя главным героем, касаться стен и видеть вывод в консоль - когда произошёл контакт со стеной, имя стены и когда произошёл разрыв контакта со стеной:
| Code | 1
2
3
4
5
6
| Begin contact with left_wall
End contact with left_wall
Begin contact with right_wall
End contact with right_wall
Begin contact with top_wall
End contact with top_wall |
|
Вывод текста на холст при контакте со стенами
Используем библиотеку SDL3_ttf, чтобы выводить текст на холст при событиях коллизий
- Скачайте бесплатный шрифт TTF: LiberationSans-Regular.zip
- Примечание. Бесплатный файл шрифта был взят по ссылке)
- В корне проекта создайте папку "assets", а в внутри этой папки создайте папку "fonts"
- Перенесите извлечённый файл LiberationSans-Regular.ttf в папку "assets/fonts":

- Откройте файл CMakeLists.txt и скопируйте код для загрузки TTF файла в конец файла CMakeLists.txt:
| Code | 1
2
3
4
| if (EMSCRIPTEN)
target_link_options("app" PRIVATE "SHELL:--embed-file ${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/LiberationSans-Regular.ttf@/assets/fonts/LiberationSans-Regular.ttf")
set_property(TARGET "app" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/LiberationSans-Regular.ttf")
endif() |
|
- Примечание. Таким образом можно загружать любые ассеты: картинки, звуки, XML или JSON -файлы и т.д. - просто копируйте код выше и меняйте пути
- Откройте файл "src/main.c" и в начале файла добавьте подключение SDL3_ttf:
| C | 1
| #include <SDL3_ttf/SDL_ttf.h> |
|
- Добавьте глобальные переменные "textTexture", "font" и "textRect":
| C | 1
2
3
| static SDL_Texture *textTexture = NULL;
static TTF_Font *font = NULL;
static SDL_FRect textRect; |
|
- Чтобы не забыть освободить ресурсы "textTexture" и "font". В функции 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
| void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
if (textTexture)
{
SDL_DestroyTexture(textTexture);
textTexture = NULL;
}
if (font)
{
TTF_CloseFont(font);
font = NULL;
}
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
- В функции SDL_AppInit() добавьте инициализацию библиотеки SDL3_ttf после после инициализации библиотеки SDL3, то есть после вызова функции SDL_Init():
| C | 1
2
3
4
5
6
| // Initialize the TTF library
if (!TTF_Init())
{
SDL_Log("Couldn't initialize TTF: %s", SDL_GetError());
return SDL_APP_FAILURE;
} |
|
- Перед инициализаций структуры "contactData" добавьте код открытия файла шрифта:
| C | 1
2
3
4
5
6
| font = TTF_OpenFont("./assets/fonts/LiberationSans-Regular.ttf", 28);
if (!font)
{
SDL_Log("TTF_OpenFont failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
} |
|
- В функции SDL_AppIterate() перед вызовом функции SDL_SetRenderDrawColor() добавьте, который обновляет текстуру с текстом, когда текст обновился:
| 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
| // --- Update text only when dirty ---
if (contactData.dirty)
{
// Destroy old texture
if (textTexture)
{
SDL_DestroyTexture(textTexture);
textTexture = NULL;
}
SDL_Color textColor = { 255, 255, 255 };
// Render text to surface
SDL_Surface *surface = TTF_RenderText_Blended(font, contactData.text,
strlen(contactData.text), textColor);
if (surface)
{
// Create texture from surface
textTexture = SDL_CreateTextureFromSurface(renderer, surface);
// Get texture size
float w, h;
SDL_GetTextureSize(textTexture, &w, &h);
// Set text rectangle
textRect.x = 50.0f;
textRect.y = 50.0f;
textRect.w = (float)w;
textRect.h = (float)h;
SDL_DestroySurface(surface);
}
contactData.dirty = false;
} |
|
- После вызова функции b2World_Draw() добавьте код рисования текстуры с текстом:
| C | 1
2
3
4
5
| // --- Render text every frame ---
if (textTexture)
{
SDL_RenderTexture(renderer, textTexture, NULL, &textRect);
} |
|
- Сравните свой код со следующим рабочим кодом, если возникнут ошибки:
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
29
30
31
32
33
34
35
36
| cmake_minimum_required(VERSION 3.21)
project(start-collision-events-sdl3-c)
# Задаем название будущего приложения (в Windows это был бы app.exe, а в вебе будет app.js / app.wasm)
add_executable(app)
# Устанавливаем стандарт C
set(CMAKE_C_STANDARD 11)
# Указываем точное местоположение конфигурационных файлов библиотек
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")
set(FREETYPE_INCLUDE_DIRS "C:/libs/freetype-2.14.1-wasm/include")
set(FREETYPE_LIBRARY "C:/libs/freetype-2.14.1-wasm/lib/libfreetype.a")
set(SDL3_ttf_DIR "C:/libs/SDL3_ttf-devel-3.2.2-wasm/lib/cmake/SDL3_ttf")
# Проверяем наличие библиотек в системе
# Если она не будет найдены, CMake прервет настройку с ошибкой
# REQUIRED - переводится, как «обязательно» или «требуется»
find_package(SDL3 REQUIRED)
find_package(box2d REQUIRED)
find_package(SDL3_ttf REQUIRED)
# Привязываем библиотеки к нашему приложению (настройка линковки и путей include)
target_link_libraries(app PRIVATE box2d::box2d SDL3_ttf::SDL3_ttf SDL3::SDL3)
# Добавляем исходный код к проекту
target_sources(app
PRIVATE
src/main.c
)
if (EMSCRIPTEN)
target_link_options("app" PRIVATE "SHELL:--embed-file ${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/LiberationSans-Regular.ttf@/assets/fonts/LiberationSans-Regular.ttf")
set_property(TARGET "app" APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/LiberationSans-Regular.ttf")
endif() |
|
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
| #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 <SDL3_ttf/SDL_ttf.h>
#include <box2d/box2d.h>
#include <stdio.h>
#include "contact-listener.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 ContactListenerData contactData;
const SDL_PixelFormatDetails *format = NULL;
float ppm = 30.f; // Pixels per meter
static SDL_Texture *textTexture = NULL;
static TTF_Font *font = NULL;
static SDL_FRect textRect;
static float accumulator = 0.0f;
static Uint64 last_ticks = 0;
typedef struct
{
bool up;
bool down;
bool left;
bool right;
} Keys;
static Keys keys = { 0 };
b2BodyId circleBodyId;
static const float playerSpeed = 5.f;
static bool is_mobile = false;
static const int BTN_SIZE = 60;
static const int BTN_GAP = 5;
static SDL_FRect btnUp, btnDown, btnLeft, btnRight;
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
// This JavaScript function returns true if the user agent matches mobile devices
EM_JS(bool, is_mobile_browser, (),
{
// We use the RegExp constructor to avoid using / / slashes which confuse Clang
var filter = new RegExp('iPhone|iPad|iPod|Android', 'i');
return filter.test(navigator.userAgent);
});
#endif
void updateButtonLayout(int w, int h)
{
// Positioning squares in a cross pattern at the bottom-left
float startX = 20.0f;
float startY = h - (BTN_SIZE * 3) - 20.0f;
btnUp = (SDL_FRect) { startX + BTN_SIZE + BTN_GAP, startY, BTN_SIZE, BTN_SIZE };
btnLeft = (SDL_FRect) { startX, startY + BTN_SIZE + BTN_GAP, BTN_SIZE, BTN_SIZE };
btnRight = (SDL_FRect) { startX + (BTN_SIZE + BTN_GAP) * 2, startY + BTN_SIZE + BTN_GAP, BTN_SIZE, BTN_SIZE };
btnDown = (SDL_FRect) { startX + BTN_SIZE + BTN_GAP, startY + (BTN_SIZE + BTN_GAP) * 2, BTN_SIZE, BTN_SIZE };
}
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;
}
// Initialize the TTF library
if (!TTF_Init())
{
SDL_Log("Couldn't initialize TTF: %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;
}
// Platform detection
#ifdef __EMSCRIPTEN__
is_mobile = is_mobile_browser();
#elif defined(__ANDROID__) || defined(__IPHONEOS__)
is_mobile = true;
#endif
printf("Is Mobile: %s\n", is_mobile ? "Yes" : "No");
SDL_SetRenderVSync(renderer, 1);
// Initial Layout
int w, h;
SDL_GetWindowSize(window, &w, &h);
updateButtonLayout(w, h);
// Создаём физический мир Box2D с гравитацией
b2Vec2 gravity = { 0.f, 0.f };
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);
font = TTF_OpenFont("./assets/fonts/LiberationSans-Regular.ttf", 28);
if (!font)
{
SDL_Log("TTF_OpenFont failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
strcpy(contactData.text, "Коснитесь стены");
contactData.dirty = true;
debugDrawer = b2DefaultDebugDraw();
debugDrawer.drawShapes = true;
debugDrawer.DrawSolidPolygonFcn = drawSolidPolygon;
debugDrawer.DrawSolidCircleFcn = drawSolidCircle;
const float friction = 0.1f;
// Top wall
{
b2BodyDef topWallBodyDef = b2DefaultBodyDef();
topWallBodyDef.type = b2_staticBody;
topWallBodyDef.position = (b2Vec2) { 200.f / ppm, 150.f / ppm };
topWallBodyDef.rotation = b2MakeRot(20.0f * (3.14f / 180.0f));
b2BodyId topWallBodyId = b2CreateBody(worldId, &topWallBodyDef);
b2Polygon topWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef topWallShapeDef = b2DefaultShapeDef();
topWallShapeDef.material.friction = friction;
topWallShapeDef.enableContactEvents = true;
b2ShapeId topWallShapeId = b2CreatePolygonShape(topWallBodyId, &topWallShapeDef, &topWallShape);
// User data
b2Shape_SetUserData(topWallShapeId, "top_wall");
}
// Left wall
{
b2BodyDef leftWallBodyDef = b2DefaultBodyDef();
leftWallBodyDef.type = b2_staticBody;
leftWallBodyDef.position = (b2Vec2) { 120.f / ppm, 230.f / ppm };
leftWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId leftWallBodyId = b2CreateBody(worldId, &leftWallBodyDef);
b2Polygon leftWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef leftWallShapeDef = b2DefaultShapeDef();
leftWallShapeDef.material.friction = friction;
leftWallShapeDef.enableContactEvents = true;
b2ShapeId leftWallShapeId = b2CreatePolygonShape(leftWallBodyId, &leftWallShapeDef, &leftWallShape);
// User data
b2Shape_SetUserData(leftWallShapeId, "left_wall");
}
// Right wall
{
b2BodyDef rightWallBodyDef = b2DefaultBodyDef();
rightWallBodyDef.type = b2_staticBody;
rightWallBodyDef.position = (b2Vec2) { 280.f / ppm, 285.f / ppm };
rightWallBodyDef.rotation = b2MakeRot(90.0f * (3.14f / 180.0f));
b2BodyId rightWallBodyId = b2CreateBody(worldId, &rightWallBodyDef);
b2Polygon rightWallShape = b2MakeBox((200.f / 2.f) / ppm, (20.f / 2.f) / ppm);
b2ShapeDef rightWallShapeDef = b2DefaultShapeDef();
rightWallShapeDef.material.friction = friction;
rightWallShapeDef.enableContactEvents = true;
b2ShapeId rightWallShapeId = b2CreatePolygonShape(rightWallBodyId, &rightWallShapeDef, &rightWallShape);
// User data
b2Shape_SetUserData(rightWallShapeId, "right_wall");
}
// Circle
b2BodyDef circleBodyDef = b2DefaultBodyDef();
circleBodyDef.type = b2_dynamicBody;
circleBodyDef.position = (b2Vec2) { 200.f / ppm, 250.f / ppm };
circleBodyDef.enableSleep = false;
circleBodyDef.fixedRotation = true;
circleBodyId = b2CreateBody(worldId, &circleBodyDef);
b2Circle circleCircle = {
.center = { 0.0f, 0.0f }, // <--- ВАЖНО: 0,0 означает, что центр круга совпадает с центром тела
.radius = 20.f / ppm // Радиус в метрах
};
b2ShapeDef circleShapeDef = b2DefaultShapeDef();
circleShapeDef.density = 1.0f;
circleShapeDef.material.friction = friction;
b2ShapeId circleShapeId = b2CreateCircleShape(circleBodyId, &circleShapeDef, &circleCircle);
// User data
b2Shape_SetUserData(circleShapeId, "player");
// 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;
}
if (event->type == SDL_EVENT_KEY_DOWN)
{
SDL_Scancode sc = event->key.scancode;
if (sc == SDL_SCANCODE_W || sc == SDL_SCANCODE_UP)
keys.up = true;
if (sc == SDL_SCANCODE_S || sc == SDL_SCANCODE_DOWN)
keys.down = true;
if (sc == SDL_SCANCODE_A || sc == SDL_SCANCODE_LEFT)
keys.left = true;
if (sc == SDL_SCANCODE_D || sc == SDL_SCANCODE_RIGHT)
keys.right = true;
}
if (event->type == SDL_EVENT_KEY_UP)
{
SDL_Scancode sc = event->key.scancode;
if (sc == SDL_SCANCODE_W || sc == SDL_SCANCODE_UP)
keys.up = false;
if (sc == SDL_SCANCODE_S || sc == SDL_SCANCODE_DOWN)
keys.down = false;
if (sc == SDL_SCANCODE_A || sc == SDL_SCANCODE_LEFT)
keys.left = false;
if (sc == SDL_SCANCODE_D || sc == SDL_SCANCODE_RIGHT)
keys.right = false;
}
// Touch handling
if (event->type == SDL_EVENT_FINGER_DOWN || event->type == SDL_EVENT_FINGER_UP || event->type == SDL_EVENT_FINGER_MOTION)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
float tx = event->tfinger.x * w;
float ty = event->tfinger.y * h;
bool pressed = (event->type != SDL_EVENT_FINGER_UP);
SDL_Point pt = { (int)tx, (int)ty };
// Check rectangles (Manual check since FRect is float)
if (tx >= btnUp.x && tx <= btnUp.x + btnUp.w && ty >= btnUp.y && ty <= btnUp.y + btnUp.h)
keys.up = pressed;
if (tx >= btnDown.x && tx <= btnDown.x + btnDown.w && ty >= btnDown.y && ty <= btnDown.y + btnDown.h)
keys.down = pressed;
if (tx >= btnLeft.x && tx <= btnLeft.x + btnLeft.w && ty >= btnLeft.y && ty <= btnLeft.y + btnLeft.h)
keys.left = pressed;
if (tx >= btnRight.x && tx <= btnRight.x + btnRight.w && ty >= btnRight.y && ty <= btnRight.y + btnRight.h)
keys.right = pressed;
}
return SDL_APP_CONTINUE;
}
void keyboard(void)
{
b2Vec2 velocity = { 0.0f, 0.0f };
if (keys.up)
{
velocity.y = -playerSpeed;
}
else if (keys.down)
{
velocity.y = playerSpeed;
}
else if (keys.left)
{
velocity.x = -playerSpeed;
}
else if (keys.right)
{
velocity.x = playerSpeed;
}
b2Body_SetLinearVelocity(circleBodyId, velocity);
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
keyboard();
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);
// Read contact events immediately
ProcessContactEvents(worldId, &contactData);
accumulator -= TIME_STEP;
}
// --- Update text only when dirty ---
if (contactData.dirty)
{
// Destroy old texture
if (textTexture)
{
SDL_DestroyTexture(textTexture);
textTexture = NULL;
}
SDL_Color textColor = { 255, 255, 255 };
// Render text to surface
SDL_Surface *surface = TTF_RenderText_Blended(font, contactData.text,
strlen(contactData.text), textColor);
if (surface)
{
// Create texture from surface
textTexture = SDL_CreateTextureFromSurface(renderer, surface);
// Get texture size
float w, h;
SDL_GetTextureSize(textTexture, &w, &h);
// Set text rectangle
textRect.x = 30.0f;
textRect.y = 50.0f;
textRect.w = (float)w;
textRect.h = (float)h;
SDL_DestroySurface(surface);
}
contactData.dirty = false;
}
// Clear the screen
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
SDL_RenderClear(renderer);
b2World_Draw(worldId, &debugDrawer);
// --- Render text every frame ---
if (textTexture)
{
SDL_RenderTexture(renderer, textTexture, NULL, &textRect);
}
// Render Touch UI only if mobile/web
if (is_mobile)
{
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 100); // Semi-transparent white
SDL_RenderFillRect(renderer, &btnUp);
SDL_RenderFillRect(renderer, &btnDown);
SDL_RenderFillRect(renderer, &btnLeft);
SDL_RenderFillRect(renderer, &btnRight);
}
// Update the screen
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
if (textTexture)
{
SDL_DestroyTexture(textTexture);
textTexture = NULL;
}
if (font)
{
TTF_CloseFont(font);
font = NULL;
}
// Удаляем физический мир Box2D
if (b2World_IsValid(worldId))
{
b2DestroyWorld(worldId);
}
// SDL will clean up the window/renderer for us
} |
|
src/contact-listener.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
| #ifndef CONTACT_LISTENER_H
#define CONTACT_LISTENER_H
#include <box2d/box2d.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
typedef struct
{
char text[128];
bool dirty;
} ContactListenerData;
// Call this every frame after stepping the world
void ProcessContactEvents(b2WorldId worldId, ContactListenerData *listener)
{
b2ContactEvents events = b2World_GetContactEvents(worldId);
// --- Begin contacts ---
for (int i = 0; i < events.beginCount; i++)
{
b2ContactBeginTouchEvent event = events.beginEvents[i];
const char *nameA = (const char *)b2Shape_GetUserData(event.shapeIdA);
const char *nameB = (const char *)b2Shape_GetUserData(event.shapeIdB);
if (!nameA) nameA = "unknown";
if (!nameB) nameB = "unknown";
const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA;
snprintf(listener->text, sizeof(listener->text),
"Начало контакта с %s", wallName);
listener->dirty = true;
printf("%s\n", listener->text);
}
// --- End contacts ---
for (int i = 0; i < events.endCount; i++)
{
b2ContactEndTouchEvent event = events.endEvents[i];
const char *nameA = (const char *)b2Shape_GetUserData(event.shapeIdA);
const char *nameB = (const char *)b2Shape_GetUserData(event.shapeIdB);
if (!nameA) nameA = "unknown";
if (!nameB) nameB = "unknown";
const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA;
snprintf(listener->text, sizeof(listener->text),
"Конец контакта с %s", wallName);
listener->dirty = true;
printf("%s\n", listener->text);
}
}
#endif // CONTACT_LISTENER_H |
|
- Итоговый проект в архиве: finish-collision-events-sdl3-c.zip
- Скомпилируйте проект командой "build-web" и обновите страницу браузера по адресу localhost:8080
- Вы увидите результат работы программы: демка в браузере

Сборка приложения в Release
- Откройте файл "config-web.bat" и измените "Debug" на "Release":
| Bash | 1
| emcmake cmake -S . -B dist -DCMAKE_BUILD_TYPE=Release |
|
- Выполните команду конфигурирования в консоли:
- Выполните команду компиляции в консоли:
- Содержимое папки "public" вы можете загрузить на какой-нибудь бесплатный хостинг. Например, на GitHub Pages или Firebase
- Вес приложения в Release:
- app.js - 185 KB
- app.wasm - 1.78 MB
|