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

SDL3 для Web (WebAssembly): Подключение Box2D v3, физика и отрисовка коллайдеров

Запись от 8Observer8 размещена 12.02.2026 в 00:48. Обновил(-а) 8Observer8 03.03.2026 в 00:34
Показов 3346 Комментарии 0

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

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



Box2D - это библиотека для 2D физики для анимаций и игр. С её помощью можно определять были ли коллизии между конкретными объектами и вызывать обработчики событий столкновения. Например, когда главный герой собирает монетки, жизни, касается шипов, врагов и т.д. Пошагово напишем код, в котором по клику левой кнопки мыши (либо одно касание на мобильном) будут создаваться падающие квадраты, а по клику правой кнопки мыши (либо касание двумя пальцами на мобильном) будут создаваться круги. Квадраты и круги падают на пол.

Подключение библиотек Box2D и SDL3 к стартовому примеру



  • Установите Emscripten 4.0.15 и CMake по инструкции: Установка Emscripten SDK (emsdk) и CMake
  • Скачайте стартовый пример: start-box2d-v3-wasm-sdl3-c.zip

    Название: 2774834c-f54d-4d5a-83ff-6a0078869654.png
Просмотров: 2041

Размер: 4.3 Кб
  • Далее представлен код стартового примера. В этом коде в функции SDL_AppEvent() добавлены два обработчика. Первый обработчик для клика левой и правой кнопки мыши. Они выводят в консоль текст "Левая" или "Правая" кнопка нажата и координаты клика. Второй обработчик для Android. Имитация левой кнопки - касание одним пальцем, а имитация правой кнопки - это касание двумя и более пальцами:

    src/main.c

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

Размер: 4.2 Кб
  • Откройте папку "SDL3-devel-3.4.0-wasm", перейдите в папку "lib/cmake/SDL3" и скопируйте абсолютный путь:
  • В файле CMakeLists.txt стартового примера создайте переменную SDL3_DIR и установите ей значение скопированного пути, заменив обратные слеши на прямые:
    Bash
    1
    
    set(SDL3_DIR "C:/libs/SDL3-devel-3.4.0-wasm/lib/cmake/SDL3")
  • Откройте папку "box2d-3.1.1-wasm", перейдите в папку "lib/cmake/box2d" и скопируйте абсолютный путь:
    Название: cb4f28a3-9838-42bd-aeb7-f1725945a6bf.png
Просмотров: 2045

Размер: 8.8 Кб
  • В файле CMakeLists.txt стартового примера создайте переменную box2d_DIR и установите ей значение скопированного пути, заменив обратные слеши на прямые:
    Bash
    1
    
    set(box2d_DIR "C:/libs/box2d-3.1.1-wasm/lib/cmake/box2d")
  • Здесь же в файле CMakeLists.txt добавьте команду для проверки наличия библиотеки box2d в системе, а если пути к этим библиотекам мы задали неверно, то будет выведена ошибка при конфигурировании проекта:
    Bash
    1
    2
    3
    4
    5
    
    # Проверяем наличие библиотек в системе
    # Если она не будет найдены, CMake прервет настройку с ошибкой
    # REQUIRED - переводится, как «обязательно» или «требуется»
    find_package(SDL3 REQUIRED)
    find_package(box2d REQUIRED)
  • Привязываем библиотеку Box2D к нашему приложению путём добавления box2d::box2d:

    Было:

    Bash
    1
    
    target_link_libraries(app PRIVATE SDL3::SDL3)
    Стало:

    Bash
    1
    2
    
    # Привязываем библиотеки к нашему приложению (настройка линковки и путей include)
    target_link_libraries(app PRIVATE box2d::box2d SDL3::SDL3)
  • К этому моменту CMakeLists.txt выглядит так:

    Bash
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    cmake_minimum_required(VERSION 3.21)
    project(start-box2d-v3-wasm-sdl3-c)
     
    # Задаем название будущего приложения (в Windows это был бы app.exe, а в вебе будет app.js / app.wasm)
    add_executable(app)
     
    # Устанавливаем стандарт C
    set(CMAKE_C_STANDARD 11)
     
    # Указываем точное местоположение конфигурационных файлов библиотек
    # Имя переменной должно строго соответствовать: НазваниеБиблиотеки_DIR
    set(SDL3_DIR "C:/libs/SDL3-devel-3.4.0-wasm/lib/cmake/SDL3")
    set(box2d_DIR "C:/libs/box2d-3.1.1-wasm/lib/cmake/box2d")
     
    # Проверяем наличие библиотек в системе
    # Если она не будет найдены, CMake прервет настройку с ошибкой
    # REQUIRED - переводится, как «обязательно» или «требуется»
    find_package(SDL3 REQUIRED)
    find_package(box2d REQUIRED)
     
    # Привязываем библиотеки к нашему приложению (настройка линковки и путей include)
    target_link_libraries(app PRIVATE box2d::box2d SDL3::SDL3)
     
    # Добавляем исходный код к проекту
    target_sources(app
        PRIVATE
        src/main.c
    )
  • Далее создадим тестовую сборку, в которой создадим пустой физический мир с гравитацией и выведем значение гравитации в консоль

Тестовая сборка и тестовый запуск стартового примера в браузере на локальном хостинге



  • Прежде чем запустить скомпилировать код и запустить приложение, создадим пустой физический мир с гравитацией и выведем значение гравитации в консоль
  • Откройте файл src/main.c в редакторе кода
  • Подключите заголовочный файл библиотеки Box2D:
    C
    1
    2
    3
    4
    5
    6
    
    #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
     
    #include <SDL3/SDL.h>
    #include <SDL3/SDL_main.h>
    #include <stdio.h>
    #include <box2d/box2d.h>
  • Добавьте объявление идентификатора для физического мира Box2D после объявлений переменных-указателей окна и рисовальщика:
    C
    1
    2
    3
    
    static SDL_Window *window = NULL;
    static SDL_Renderer *renderer = NULL;
    static b2WorldId worldId;
  • Добавьте в конец функции SDL_AppInit() создание физического мира и вывод значения гравитации в консоль:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
        // Создаём физический мир Box2D с гравитацией
        b2Vec2 gravity = { 0.f, 9.8f };
        b2WorldDef worldDef = b2DefaultWorldDef();
        worldDef.gravity = gravity;
        worldId = b2CreateWorld(&worldDef);
     
        // Выводим значение гравитации в консоль
        // Получаем текущее значение гравитации
        b2Vec2 currentGravity = b2World_GetGravity(worldId);
        // Выводим компоненты x и y
        printf("Текущее значение гравитации: x = %.2f, y = %.2f\n", currentGravity.x, currentGravity.y);
  • Добавьте в начало функции SDL_AppQuit() удаление физического мира:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    void SDL_AppQuit(void *appstate, SDL_AppResult result)
    {
        // Удаляем физический мир Box2D
        if (b2World_IsValid(worldId)) 
        {
            b2DestroyWorld(worldId);
        }
     
        // SDL will clean up the window/renderer for us
    }
  • Откройте консоль (CMD) в корне стартового примера. Для этого можете просто в адресной строке папки (там где путь) написать "cmd" (без кавычек) и нажать Enter
  • В CMD введите команду для конфигурирования:
    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"
  • В браузере должен выводиться квадрат тёмного-серого цвета - это холст для рисования. В консоль браузера будет выведено сообщение:
    Bash
    1
    
    Текущее значение гравитации: x = 0.00, y = 9.80
  • Код main.c к этому моменту:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    
    #define SDL_MAIN_USE_CALLBACKS 1 // Use the callbacks instead of main()
     
    #include <SDL3/SDL.h>
    #include <SDL3/SDL_main.h>
    #include <stdio.h>
    #include <box2d/box2d.h>
     
    static SDL_Window *window = NULL;
    static SDL_Renderer *renderer = NULL;
    static b2WorldId worldId;
     
    SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
    {
        if (!SDL_Init(SDL_INIT_VIDEO))
        {
            SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
            return SDL_APP_FAILURE;
        }
     
        if (!SDL_CreateWindowAndRenderer("Example", 400, 400, 0, &window, &renderer))
        {
            SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
            return SDL_APP_FAILURE;
        }
     
        SDL_SetRenderVSync(renderer, 1);
     
        // Создаём физический мир Box2D с гравитацией
        b2Vec2 gravity = { 0.f, 9.8f };
        b2WorldDef worldDef = b2DefaultWorldDef();
        worldDef.gravity = gravity;
        worldId = b2CreateWorld(&worldDef);
     
        // Выводим значение гравитации в консоль
        // Получаем текущее значение гравитации
        b2Vec2 currentGravity = b2World_GetGravity(worldId);
        // Выводим компоненты x и y
        printf("Текущее значение гравитации: x = %.2f, y = %.2f\n", currentGravity.x, currentGravity.y);
     
        return SDL_APP_CONTINUE;
    }
     
    SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
    {
        if (event->type == SDL_EVENT_QUIT)
        {
            return SDL_APP_SUCCESS;
        }
     
        // --- DESKTOP ---
        if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
        {
            if (event->button.which != SDL_TOUCH_MOUSEID)
            {
                const char *btnName = (event->button.button == SDL_BUTTON_LEFT) ? "Левая" : "Правая";
                printf("%s кнопка (Мышь): x=%.0f, y=%.0f\n", btnName, event->button.x, event->button.y);
            }
        }
     
        // --- MOBILE / WASM (Touch) ---
        if (event->type == SDL_EVENT_FINGER_DOWN)
        {
            int w, h;
            SDL_GetWindowSize(window, &w, &h);
     
            float pixelX = event->tfinger.x * w;
            float pixelY = event->tfinger.y * h;
     
            // Проверяем количество пальцев на устройстве (touchId берем из события)
            int numFingers = 0;
            SDL_GetTouchFingers(event->tfinger.touchID, &numFingers);
     
            if (numFingers <= 1)
            {
                printf("Левая кнопка (Тап 1 пальцем): x=%.2f, y=%.2f\n", pixelX, pixelY);
            }
            else
            {
                printf("Правая кнопка (Тап несколькими): x=%.2f, y=%.2f\n", pixelX, pixelY);
            }
        }
     
        return SDL_APP_CONTINUE;
    }
     
    SDL_AppResult SDL_AppIterate(void *appstate)
    {
        // Clear the screen
        SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
        SDL_RenderClear(renderer);
     
        // Draw here...
     
        // Update the screen
        SDL_RenderPresent(renderer);
        return SDL_APP_CONTINUE;
    }
     
    void SDL_AppQuit(void *appstate, SDL_AppResult result)
    {
        // Удаляем физический мир Box2D
        if (b2World_IsValid(worldId)) 
        {
            b2DestroyWorld(worldId);
        }
     
        // SDL will clean up the window/renderer for us
    }

Добавляем пол, падающие квадрат и круг и рисуем коллайдеры



Определение Gemini 3, что такое коллайдер простыми словами:

Коллайдер в Box2D - "физическая оболочка" объекта, которая определяет его форму для столкновений. В цифровом мире картинка (спрайт) — это просто набор пикселей. Без коллайдера два танка на экране просто проедут друг сквозь друга, как привидения. Чтобы они столкнулись, движку нужно знать их границы.
Коллайдеры можно рисовать линиями. В Box2D есть вспомогательные функции, которые помогают рисовать коллайдеры. Далее рассмотрим рисование коллайдера прямоугольной формы и в форме круга. В Box2D есть структура, которую мы создадим позже, под названием b2DefaultDebugDraw. У этой структуры есть два указателя на функции: DrawSolidPolygonFcn (для прямоугольников) и DrawSolidCircleFcn (для круговых коллайдеров). К этим указателям привязываем свои функции, которые будут вызываться, когда нужно будет нарисовать коллайдеры.
  • Добавьте в main.c после объявления идентификатора физического мира объявление структуры b2DebugDraw:

    C
    1
    2
    3
    4
    
    static SDL_Window *window = NULL;
    static SDL_Renderer *renderer = NULL;
    static b2WorldId worldId;
    static b2DebugDraw debugDrawer;
  • В конце функции SDL_AppInit() добавьте код создания структуры типа b2DebugDraw. Две функции: drawSolidPolygon() и drawSolidCircle() мы создадим чуть позже:

    C
    1
    2
    3
    4
    
        debugDrawer = b2DefaultDebugDraw();
        debugDrawer.drawShapes = true;
        debugDrawer.DrawSolidPolygonFcn = drawSolidPolygon;
        debugDrawer.DrawSolidCircleFcn = drawSolidCircle;
  • После создания структуры debugDrawer напишите код создания пола:

    C
    1
    2
    3
    4
    5
    6
    7
    8
    
        // Floor
        b2BodyDef floorBodyDef = b2DefaultBodyDef();
        floorBodyDef.type = b2_staticBody;
        floorBodyDef.position = (b2Vec2) { 200.f / ppm, 300.f / ppm };
        b2BodyId floorBodyId = b2CreateBody(worldId, &floorBodyDef);
        b2Polygon floorShape = b2MakeBox((300.f / 2.f) / ppm, (20.f / 2.f) / ppm);
        b2ShapeDef floorShapeDef = b2DefaultShapeDef();
        b2ShapeId topWallShapeId = b2CreatePolygonShape(floorBodyId, &floorShapeDef, &floorShape);
  • Создайте файл "debug-drawer.h" в папке "src". Скопируйте код определения двух функций: drawSolidPolygon() и drawSolidCircle():

    debug-drawer.h

    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    
    #ifndef DEBUG_DRAWER_H
    #define DEBUG_DRAWER_H
     
    #include <SDL3/SDL.h>
    #include <box2d/box2d.h>
     
    extern SDL_Renderer *renderer;
    extern const SDL_PixelFormatDetails *format;
    extern float ppm;
     
    static void drawSolidPolygon(b2Transform transform,
        const b2Vec2 *vertices, int vertexCount,
        float radius, b2HexColor color, void *context)
    {
        (void)radius;
        (void)context;
     
        // Extract RGB
        Uint8 r, g, b;
        SDL_GetRGB(color, format, NULL, &r, &g, &b);
        // Draw a collider rectangle with lines
        SDL_SetRenderDrawColor(renderer, r, g, b, SDL_ALPHA_OPAQUE);
        for (int i = 0; i < vertexCount; ++i)
        {
            int next_index = (i + 1 == vertexCount) ? 0 : i + 1;
            b2Vec2 p0 = b2TransformPoint(transform, vertices[i]);
            b2Vec2 p1 = b2TransformPoint(transform, vertices[next_index]);
            float x0 = p0.x * ppm;
            float y0 = p0.y * ppm;
            float x1 = p1.x * ppm;
            float y1 = p1.y * ppm;
            SDL_RenderLine(renderer, x0, y0, x1, y1);
        }
     
        // --- Tip: Draw orientation line ---
        // Compute polygon center
        float cx = 0.f, cy = 0.f;
        for (int i = 0; i < vertexCount; ++i)
        {
            b2Vec2 p = b2TransformPoint(transform, vertices[i]);
            cx += p.x;
            cy += p.y;
        }
        cx = (cx / vertexCount) * ppm;
        cy = (cy / vertexCount) * ppm;
     
        // Midpoint of second edge (vertex[1] → vertex[2])
        b2Vec2 p1 = b2TransformPoint(transform, vertices[1]);
        b2Vec2 p2 = b2TransformPoint(transform, vertices[2]);
        float mx = (p1.x + p2.x) * 0.5f * ppm;
        float my = (p1.y + p2.y) * 0.5f * ppm;
     
        // Draw center → edge midpoint line (acts like "direction tip")
        SDL_RenderLine(renderer, cx, cy, mx, my);
    }
     
    static void drawSolidCircle(b2Transform transform, float radius,
        b2HexColor color, void *context)
    {
        (void)radius;
        (void)context;
     
        float angle = 0.f;
        const int numberOfSegments = 20;
        const float angleStep = 360.f / numberOfSegments;
     
        // Extract RGB
        Uint8 r, g, b;
        SDL_GetRGB(color, format, NULL, &r, &g, &b);
        // Draw a collider rectangle with lines
        SDL_SetRenderDrawColor(renderer, r, g, b, SDL_ALPHA_OPAQUE);
     
        float x = radius * cos(angle * SDL_PI_F / 180.f);
        float y = radius * sin(angle * SDL_PI_F / 180.f);
        b2Vec2 p0 = b2TransformPoint(transform, (b2Vec2) { x, y });
        float x0 = p0.x * ppm;
        float y0 = p0.y * ppm;
        angle += angleStep;
     
        for (int i = 0; i < numberOfSegments; ++i)
        {
            float x = radius * cos(angle * SDL_PI_F / 180.f);
            float y = radius * sin(angle * SDL_PI_F / 180.f);
            b2Vec2 p1 = b2TransformPoint(transform, (b2Vec2) { x, y });
            float x1 = p1.x * ppm;
            float y1 = p1.y * ppm;
            SDL_RenderLine(renderer, x0, y0, x1, y1);
            x0 = x1;
            y0 = y1;
            angle += angleStep;
            if (angle >= 360.f)
            {
                angle = 0.f;
            }
        }
     
        // --- Tip: Draw orientation line ---
        // Circle center
        b2Vec2 c = b2TransformPoint(transform, (b2Vec2) { 0.f, 0.f });
        float cx = c.x * ppm;
        float cy = c.y * ppm;
     
        // Direction = transform applied to (radius, 0)
        b2Vec2 orient = b2TransformPoint(transform, (b2Vec2) { radius, 0.f });
        float ox = orient.x * ppm;
        float oy = orient.y * ppm;
     
        // Draw center → orientation marker
        SDL_RenderLine(renderer, cx, cy, ox, oy);
    }
     
    #endif // DEBUG_DRAWER_H
  • Этому файлу требуются глобальные переменные renderer и ppm (pixels per meter). Поэтому нужно в main.c убрать "static" в объявлении "renderer". Добавьте константу "format" и "ppm":

    C
    1
    2
    3
    4
    5
    6
    7
    
    static SDL_Window *window = NULL;
    SDL_Renderer *renderer = NULL;
    static b2WorldId worldId;
    static b2DebugDraw debugDrawer;
     
    const SDL_PixelFormatDetails *format = NULL;
    float ppm = 30.f; // Pixels per meter
  • Примечание. "ppm" (pixel per meter) преобразует координатную сетку из мира пикселей в мир метрики Box2D. В Box2D измерения в метрах
  • Глобальная константа "format" нужна для работы SDL3. Мы объявили её в файле main.c. Необходимо добавить её создание в конце функции SDL_AppInit():

    C
    1
    2
    3
    
        // Get the pixel format
        SDL_Surface *surface = SDL_GetWindowSurface(window);
        format = SDL_GetPixelFormatDetails(surface->format);
  • Рисование коллайдеров происходит при вызове функции b2World_Draw(), которой передаётся идентификатор мира и debugDrawer:

    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    SDL_AppResult SDL_AppIterate(void *appstate)
    {
        // Clear the screen
        SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
        SDL_RenderClear(renderer);
     
        b2World_Draw(worldId, &debugDrawer);
     
        // Update the screen
        SDL_RenderPresent(renderer);
        return SDL_APP_CONTINUE;
    }
  • Соберите проект командой:

    Bash
    1
    
    build-web
  • Перейдите по адресу в браузере:

    Bash
    1
    
    localhost:8080
  • Программа рисует пол:


  • Полный код функции main.c на текущий момент, чтобы свериться, на случай если возникли ошибки:

    main.c

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

Анимация падения. Независимость скорости анимации от частоты обновления экрана



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

Сборка в релиз



  • Для сборки в релиз нужно открыть файл "config-web.bat" в редакторе кода и исправить Debug на Release:

    config-web.bat

    Bash
    1
    
    emcmake cmake -S . -B dist -DCMAKE_BUILD_TYPE=Release
  • Выполните команды конфигурирования и сборки:
    Bash
    1
    2
    
    config-web
    build-web
  • Примечание. Вес файлов после сборки в Debug:
    Code
    1
    2
    
    app.js - 375 KB
    app.wasm - 2.05 MB
    Вес файлов после сборки в Release:
    Code
    1
    2
    
    app.js - 183 KB
    app.wasm - 1.03 MB

Развёртывание на бесплатном хостинге Vercel



  • Скачайте установщик Node.js и установите: https://nodejs.org/en/download
  • Установите Vercel следующей командой из консоли глобально:
    Bash
    1
    
    npm i vercel -g
  • Зарегистрируйтесь на Vercel: https://vercel.com/
  • Примечание. В стартовом прмер, который скачали в начале, уже есть файл vercel.json для решения проблемы с загрузкой wasm-файла
  • В консоле в корне проекта выполните команду:
    Bash
    1
    
    vercel login
  • Выполните команду:
    Bash
    1
    
    vercel
  • Примечание. Будут заданы вопросы в консоли - просто нажимайте всегда Enter
  • В консоль будет выведен адрес вашего приложения: https://start-box2d-v3-wasm-sdl3-c.vercel.app
  • Примечание. Когда вы что-то измените в проекте и захотите загрузить изменённое приложение на сервер, то введите команду:
    Bash
    1
    
    vercel --prod
  • Результат работы приложения:
    Название: finish-box2d-v3-wasm-sdl3-c.gif
Просмотров: 764

Размер: 387.2 Кб

Миниатюры

Нажмите на изображение для увеличения
Название: 8fd6087b-0ff8-4941-ae56-ab555bd1bf5b.png
Просмотров: 2080
Размер:	14.2 Кб
ID:	11541
Нажмите на изображение для увеличения
Название: 93204352-f460-4144-bf2a-dbe5904fc2c5.png
Просмотров: 2081
Размер:	854 байт
ID:	11543
Нажмите на изображение для увеличения
Название: 047ca334-6727-45ce-b9a5-7eb9fd1dbd1b.png
Просмотров: 2082
Размер:	1.3 Кб
ID:	11544
Вложения
Тип файла: zip start-box2d-v3-wasm-sdl3-c.zip (4.2 Кб, 55 просмотров)
Тип файла: zip SDL3-devel-3.4.0-wasm.zip (1.22 Мб, 65 просмотров)
Тип файла: zip box2d-3.1.1-wasm.zip (555.4 Кб, 68 просмотров)
Тип файла: zip finish-box2d-v3-wasm-sdl3-c.zip (6.5 Кб, 81 просмотров)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Новые блоги и статьи
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 https:/ / www. cyberforum. ru/ blog_attachment. php?attachmentid=11680&amp;d=1772460536 Одним из. . .
Реалии
Hrethgir 01.03.2026
Нет, я не закончил до сих пор симулятор. Эта задача сложнее. Не получилось уйти в плавсостав, но оно и к лучшему, возможно. Точнее получалось - но сварщиком в палубную команду, а это значит, в моём. . .
Ритм жизни
kumehtar 27.02.2026
Иногда приходится жить в ритме, где дел становится всё больше, а вовлечения в происходящее — всё меньше. Плотный график не даёт вниманию закрепиться ни на одном событии. Утро начинается с быстрых,. . .
SDL3 для Web (WebAssembly): Сборка библиотек: SDL3, Box2D, FreeType, SDL3_ttf, SDL3_mixer и SDL3_image из исходников с помощью CMake и Emscripten
8Observer8 27.02.2026
Недавно вышла версия 3. 4. 2 библиотеки SDL3. На странице официальной релиза доступны исходники, готовые DLL (для x86, x64, arm64), а также библиотеки для разработки под Android, MinGW и Visual Studio. . . .
SDL3 для Web (WebAssembly): Реализация движения на Box2D v3 - трение и коллизии с повёрнутыми стенами
8Observer8 20.02.2026
Содержание блога Box2D позволяет легко создать главного героя, который не проходит сквозь стены и перемещается с заданным трением о препятствия, которые можно располагать под углом, как верхнее. . .
Конвертировать закладки radiotray-ng в m3u-плейлист
damix 19.02.2026
Это можно сделать скриптом для PowerShell. Использование . \СonvertRadiotrayToM3U. ps1 <path_to_bookmarks. json> Рядом с файлом bookmarks. json появится файл bookmarks. m3u с результатом. # Check if. . .
Семь CDC на одном интерфейсе: 5 U[S]ARTов, 1 CAN и 1 SSI
Eddy_Em 18.02.2026
Постепенно допиливаю свою "многоинтерфейсную плату". Выглядит вот так: https:/ / www. cyberforum. ru/ blog_attachment. php?attachmentid=11617&stc=1&d=1771445347 Основана на STM32F303RBT6. На борту пять. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru