Форум программистов, компьютерный форум, киберфорум
8Observer8
Войти
Регистрация
Восстановить пароль

SDL3 для Web (WebAssembly): Идентификация объектов на Box2D v3 - использование userData и событий коллизий

Запись от 8Observer8 размещена 02.03.2026 в 17:15. Обновил(-а) 8Observer8 05.03.2026 в 18:05
Показов 2064 Комментарии 1

Содержание блога

Финальная демка в браузере. Итоговый код: 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", скопируйте в неё архивы и извлеките их в текущую папку:

    Название: d238d741-b2ce-42a4-88d7-8e0196845580.png
Просмотров: 1546

Размер: 5.7 Кб
  • Откройте папку "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, то есть сначала копируйте пути:

    Название: 744b6134-fd50-4eaa-a765-972879110569.png
Просмотров: 1546

Размер: 8.0 Кб


  • А потом копируйте пути в 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 введите команду для конфигурирования:
    Bash
    1
    
    config-web
  • Примечание. "config-web" - это имя bash-файла (батника), который находится в корне папки стартового примера
  • В CMD введите команду сборки проекта:
    Bash
    1
    
    build-web
  • Собранные файлы (app.js и app.wasm) будут скопированы в папку "public/js" (папка "public" лежит в корне проекта)
  • В корне проекта запустите локальный сервер командой:
    Bash
    1
    
    http-server -c-1
  • Примечание 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
  • Перейдите по адресу:
    Bash
    1
    
    localhost:8080
  • Примечание. Обновлять страницу после повторной сборки проекта лучше с очисткой кэша браузера. Для этого (обязательно с открытой консолью браузера) в браузере 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). Если говорить совсем просто: это «уши» твоей игры, которые слушают, когда один объект касается другого, и записывают это событие текстом.

    Давай разберем его по частям.
    1. Структура данных (Контейнер для информации)
      C
      1
      2
      3
      4
      
      typedef struct {
          char text[128]; // Сюда запишем сообщение, например: "Начало контакта с lef_wall"
          bool dirty;     // Флаг-сигнал: "Эй, текст изменился, обнови его на экране!"
      } ContactListenerData;
      Эта структура нужна, чтобы передавать информацию о столкновениях из физического мира в графическую часть игры (например, чтобы вывести надпись в углу экрана).
    2. Главная функция: ProcessContactEvents

      Она работает в два этапа: обрабатывает начало касания и конец касания.

      Этап А: Начало касания (Begin contacts)

      Когда два объекта ударяются друг о друга, Box2D создает событие.
      1. Получение имен: Код берет два объекта (shapeIdA и shapeIdB) и спрашивает: "Как вас зовут?". Имена хранятся в UserData.
      2. Логика "Кто не игрок?": Программа ищет объект "player". Если первый объект — игрок, значит нам интересно имя второго. Так мы узнаем, обо что именно ударился игрок (о стену, о врага и т.д.).
        C
        1
        
        const char *wallName = strcmp(nameA, "player") == 0 ? nameB : nameA;
      3. Запись: Текст "Начало контакта с [Имя]" копируется в буфер, а флаг dirty ставится в true.

      Этап Б: Конец касания (End contacts)

      Здесь происходит ровно то же самое, но в момент, когда объекты разлетаются в разные стороны. Текст меняется на "End contact with...".

    Как это работает визуально

    Представь, что у тебя есть круг (Player) и квадрат (Box).
    1. Как только круг касается края квадрата, срабатывает первый цикл for, и в listener->text попадает фраза: "Begin contact with Box".
    2. Пока они соприкасаются, ничего не происходит.
    3. Как только круг отлетает от квадрата, срабатывает второй цикл, и текст меняется на: "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":
    Название: 0b3bbfca-a05e-4866-a3d9-9cb4985a81dd.png
Просмотров: 1543

Размер: 3.6 Кб
  • Откройте файл 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
  • Выполните команду конфигурирования в консоли:
    Bash
    1
    
    config-web
  • Выполните команду компиляции в консоли:
    Bash
    1
    
    build-web
  • Содержимое папки "public" вы можете загрузить на какой-нибудь бесплатный хостинг. Например, на GitHub Pages или Firebase
  • Вес приложения в Release:
    • app.js - 185 KB
    • app.wasm - 1.78 MB

Миниатюры

Нажмите на изображение для увеличения
Название: e7bf756b-507f-4f04-baa6-a3324a400077.png
Просмотров: 1583
Размер:	2.0 Кб
ID:	11664
Нажмите на изображение для увеличения
Название: 57dbceff-46da-40c7-aa54-c50078e1864e.png
Просмотров: 1582
Размер:	12.5 Кб
ID:	11673
Нажмите на изображение для увеличения
Название: f15e5863-b692-4a6b-8cb4-1bd622ae8e68.png
Просмотров: 1572
Размер:	9.1 Кб
ID:	11674
Нажмите на изображение для увеличения
Название: 7e494e53-39f7-42b8-8668-9823a8b5719a.png
Просмотров: 1578
Размер:	13.5 Кб
ID:	11677
Нажмите на изображение для увеличения
Название: finish-collision-events-sdl3-c.gif
Просмотров: 3807
Размер:	26.7 Кб
ID:	11680
Вложения
Тип файла: zip start-collision-events-sdl3-c.zip (7.7 Кб, 74 просмотров)
Тип файла: zip SDL3-devel-3.4.2-wasm.zip (1.22 Мб, 39 просмотров)
Тип файла: zip box2d-3.1.1-wasm.zip (149.2 Кб, 34 просмотров)
Тип файла: zip freetype-2.14.1-wasm.zip (553.5 Кб, 39 просмотров)
Тип файла: zip SDL3_ttf-devel-3.2.2-wasm.zip (62.0 Кб, 32 просмотров)
Тип файла: zip LiberationSans-Regular.zip (206.0 Кб, 32 просмотров)
Тип файла: zip finish-collision-events-sdl3-c.zip (215.7 Кб, 61 просмотров)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 1
Комментарии
  1. Старый комментарий
    QR-код для запуска демо в браузерах мобильных устройств на базе Android и iOS. В этом случае отображается джойстик для управления главным героем.

    Запись от 8Observer8 размещена 02.03.2026 в 18:44 8Observer8 вне форума
    Обновил(-а) 8Observer8 02.03.2026 в 18:50
 
Новые блоги и статьи
SDL3 для Desktop (MinGW): Создаём пустое окно с нуля для 2D-графики на SDL3, Си и C++
8Observer8 10.03.2026
Содержание блога Финальные проекты на Си и на C++: hello-sdl3-c. zip hello-sdl3-cpp. zip Результат:
Установка CMake и MinGW 13.1 для сборки С и C++ приложений из консоли и из Qt Creator в EXE
8Observer8 10.03.2026
Содержание блога MinGW - это коллекция инструментов для сборки приложений в EXE. CMake - это система сборки приложений. Здесь описаны базовые шаги для старта программирования с помощью CMake и. . .
Как дизайн сайта влияет на конверсию: 7 решений, которые реально повышают заявки
Neotwalker 08.03.2026
Многие до сих пор воспринимают дизайн сайта как “красивую оболочку”. На практике всё иначе: дизайн напрямую влияет на то, оставит человек заявку или уйдёт через несколько секунд. Даже если у вас. . .
Модульная разработка через nuget packages
DevAlt 07.03.2026
Сложившийся в . Net-среде способ разработки чаще всего предполагает монорепозиторий в котором находятся все исходники. При создании нового решения, мы просто добавляем нужные проекты и имеем. . .
Модульный подход на примере F#
DevAlt 06.03.2026
В блоге дяди Боба наткнулся на такое определение: В этой книге («Подход, основанный на вариантах использования») Ивар утверждает, что архитектура программного обеспечения — это структуры,. . .
Управление камерой с помощью скрипта OrbitControls.js на Three.js: Вращение, зум и панорамирование
8Observer8 05.03.2026
Содержание блога Финальная демка в браузере работает на Desktop и мобильных браузерах. Итоговый код: orbit-controls-threejs-js. zip. Сканируйте QR-код на мобильном. Вращайте камеру одним пальцем,. . .
SDL3 для Web (WebAssembly): Синхронизация спрайтов SDL3 и тел Box2D
8Observer8 04.03.2026
Содержание блога Финальная демка в браузере. Итоговый код: finish-sync-physics-sprites-sdl3-c. zip На первой гифке отладочные линии отключены, а на второй включены:. . .
SDL3 для Web (WebAssembly): Идентификация объектов на Box2D v3 - использование userData и событий коллизий
8Observer8 02.03.2026
Содержание блога Финальная демка в браузере. Итоговый код: finish-collision-events-sdl3-c. zip Сканируйте QR-код на мобильном и вы увидите, что появится джойстик для управления главным героем. . . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru