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

SDL3 для Web (WebAssembly): Реализация движения на Box2D v3 - трение и коллизии с повёрнутыми стенами

Запись от 8Observer8 размещена 20.02.2026 в 16:51. Обновил(-а) 8Observer8 02.03.2026 в 14:01
Показов 3507 Комментарии 0

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

Box2D позволяет легко создать главного героя, который не проходит сквозь стены и перемещается с заданным трением о препятствия, которые можно располагать под углом, как верхнее препятствие на этом скриншоте:



Финальная демка этой инструкции. Итоговый код: finish-player-movement-sdl3-c.zip

В физическом мире Box2D перемещать главного героя (ГГ) можно с помощью: установки линейной скорости, импульса или приложения силы. В данной инструкции используем установку линейной скорости с помощью функции b2Body_SetLinearVelocity(). Коллайдером для ГГ будет круг, которому запретим вращение. На Desktop управлять движением будем с помощью клавиш WASD (и клавиш-стрелок). Для браузеров на мобильных устройствах добавим четыре квадрата-кнопки для движения ГГ при касании этих квадратов-кнопок.

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



  • Установите Emscripten 4.0.15 и CMake по инструкции: Установка Emscripten SDK (emsdk) и CMake
  • Скачайте стартовый пример: start-player-movement-sdl3-c.zip
  • Примечание. Этот стартовый пример был получен в результате выполнения пошаговой инструкции: Подключение Box2D v3, физика и отрисовка коллайдеров
  • Так выглядит код стартового примера:

    src/main.c

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

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


  • Откройте папку со стартовым примером в какой-нибудь редакторе кода, например, в 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", скопируйте в неё архивы и извлеките их в текущую папку:
    Название: e8933ad5-0754-45b3-8d63-176ee6e3b7d5.png
Просмотров: 2608

Размер: 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" и скопируйте абсолютный путь:
  • Замените в переменной box2d_DIR путь на свой:
    Bash
    1
    
    set(SDL3_DIR "C:/libs/box2d-3.1.1-wasm/lib/cmake/box2d")

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



  • Откройте консоль (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

Добавляем обработку нажатий клавиш



Управлять героем будет с помощью клавиш WASD и клавиш-стрелок. Цель, на данном этапе, выводить слова в консоль: "вверх", "вниз", "влево", "вправо" при нажатии соответствующих клавиш.

  • В начале файла src/main.c наберите вручную код, для создания глобальной переменной:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    typedef struct
    {
        bool up;
        bool down;
        bool left;
        bool right;
    } Keys;
     
    static Keys keys = { 0 };
  • Наберите код следующий код для обработки нажатий клавиш WASD и клавиш-стрелок. Здесь мы меняет значения переменных (членов структуры keys) на true и false в зависимости от того, нажата ли клавиша или не нажата:
    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
    
    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;
        }
  • Примечание. Код обработки клика мышки можно удалить, но обработку касания экрана не удаляйте, потому что он нужен для управления героем на мобильных браузерах, чтобы отобразить кнопки управления (виртуальный джойстик), которые мы создадим позже
  • Перед функций SDL_AppIterate() наберите код функции с именем keyboard() для обработки нажатий клавиш:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    void keyboard(void)
    {
        if (keys.up)
        {
            printf("вверх\n");
        }
        else if (keys.down)
        {
            printf("вниз\n");
        }
        else if (keys.left)
        {
            printf("влево\n");
        }
        else if (keys.right)
        {
            printf("вправо\n");
        }
    }
  • В начале функции SDL_AppIterate() добавьте вызов функции keyboard():
    C
    1
    2
    3
    
    SDL_AppResult SDL_AppIterate(void *appstate)
    {
        keyboard();
  • Соберите приложение с помощью команды "build-web" и обновите вкладку браузера. Нажимайте клавиши WASD и клавиши-стрелки и вы увидите вывод в консоль:
    Название: 38d21c65-17d7-4cb6-a237-2956a2494ea7.png
Просмотров: 2596

Размер: 3.3 Кб

Добавляем управление героем



Нам нужно будет удалить гравитацию, потому что у нас вид сверху на игровое поле. Цель: управлять героем путём установления скорости. Если кнопки управления не нажаты, то линейная скорость героя должна быть обнулена. Мы будем управлять круговым коллайдером - это коллайдер главного героя.

  • Найдите строку кода задания гравитации и обнулите её:
    C
    1
    
    b2Vec2 gravity = { 0.f, 0.f };
  • Найдите строку, где мы создаём идентификатор кругового коллайдера:
    C
    1
    
    b2BodyId circleBodyId = b2CreateBody(worldId, &circleBodyDef);
  • Идентификатор коллайдера ГГ должен быть глобальным, чтобы он был доступен в функции keyboard() для задания линейной скорости ГГ. Скопируйте "b2BodyId circleBodyId" и удалите объявление перед circleBodyId в указанной выше строке кода.
  • Скопированное объявление идентификатора ("b2BodyId circleBodyId") скопируйте выше функции SDL_AppInit(), то есть сделайте объявление circleBodyId глобальным
  • Добавьте глобальную константу playerSpeed с значениме 5:
    C
    1
    
    static const float playerSpeed = 5.f;
  • В функции keyboard() добавьте задание скорости ГГ:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    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);
    }
  • В первой консоли соберите приложение командой "build-web" и обновите вкладку браузера. Вы можете теперь управлять ГГ клавишами WASD и клавишам-стрелками
  • Обратите внимание, что при управлении клавишами коллайдер ГГ поворачивается. Нужно убрать это ненужное вращение. Добавьте код в функции SDL_AppInit() запрещающий вращение:
    C
    1
    
    circleBodyDef.fixedRotation = true;
  • После пересборки приложения вы видите, что коллайдер не поворачивает при движении и ГГ не проходит через препятствие

Определяем в коде запущено ли приложение в Desktop-браузере или в браузере мобильного устройства



Цель: при запуске приложения вывести в консоль информацию на каком устройстве было запущено веб-приложение - из браузера ПК или из браузера мобильного устройства.

  • Создайте глобальную переменную is_mobile:
    C
    1
    
    static bool is_mobile = false;
  • Создайте функцию is_mobile_browser():
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    #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
  • В функции SDL_AppInit() после функции создания окна и рисовальщика SDL_CreateWindowAndRenderer() добавляем код проверки на какой платформе был запуск веб-приложения (на Desktop или Mobile):
    C
    1
    2
    3
    4
    5
    6
    7
    8
    
        // 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");
  • Примечание. Для того, чтобы увидеть вывод в консоль с мобильного браузера через USB-кабель или Wi-Fi выполните инструкцию: Основы отладки веб-приложений на SDL3 по USB и Wi-Fi, запущенных в браузере мобильных устройств
  • При запуске в браузере на Desktop вы увидите в консоле браузера:
    Code
    1
    
    Is Mobile: No
  • При запуске в браузере мобильного устройства вы увидите в консоле браузера:
    Code
    1
    
    Is Mobile: Yes

Запрет реакции смартфона на долгое удержание пальца на холсте



Если удерживать палец на холсте в мобильном браузере, то смартфон реагирует выделением текста, вызовом контекстного меню и т.д. Запрещаем это. Добавьте в public/index.html в тег <head> следующий код:

PHP/HTML
1
2
3
4
5
6
7
8
    <style>
        canvas {
            touch-action: none;
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            user-select: none;
        }
    </style>

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



Добавим четыре квадрата, чтобы управлять ГГ на мобильном браузере.

  • Создайте глобальные константы для размера кнопок (BTN_SIZE), расстояния между кнопками (BTN_GAP) и структуры SDL_FRect для хранения координат и размеров кнопок:
    C
    1
    2
    3
    4
    
    static bool is_mobile = false;
    static const int BTN_SIZE = 60;
    static const int BTN_GAP = 5;
    static SDL_FRect btnUp, btnDown, btnLeft, btnRight;
  • Добавьте функцию updateButtonLayout() для расчёта позиций и размеров кнопок перед функций SDL_AppInit():
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    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 };
    }
  • Добавьте вызов функции updateButtonLayout() в функции SDL_AppInit() после строки включения вертикальной синхронизации:
    C
    1
    2
    3
    4
    5
    6
    
        SDL_SetRenderVSync(renderer, 1);
     
        // Initial Layout
        int w, h;
        SDL_GetWindowSize(window, &w, &h);
        updateButtonLayout(w, h);
  • В конце функции SDL_AppEvent() добавьте код обработки касаний клавиш джойстика:
    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
    
    SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
    {
        // ...
     
        // 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;
    }
  • В функцию SDL_AppIterate() добавьте код рисования кнопок джойстика после вызова функции b2World_Draw():
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
        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);
        }
  • После сборки "build-web" и обновления вкладки в Desktop-браузере вы не увидите джойстика, так как он появляется только в браузере мобильных устройств и вы можете управлять героем с клавиатуры клавишами WASD и клавишами-стрелками
  • На мобильном браузере вы появляется джойстик из четырёх кнопок с помощью которых вы можете управлять ГГ
  • Текущий код:

    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
    
    #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;
     
        // 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;
        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;
        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
    }
    public/index.html

    PHP/HTML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    <!DOCTYPE html>
     
    <html>
     
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
            canvas {
                touch-action: none;
                -webkit-touch-callout: none;
                -webkit-user-select: none;
                user-select: none;
            }
        </style>
    </head>
     
    <body>
        <canvas id="canvas"></canvas>
     
        <script async src="./js/app.js"></script>
    </body>

Добавляет стены, трение для стен и поворачиваем верхнюю стену



  • Удалите код создания пола и квадрата:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
        // 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);
  • Вместо этого удалённого кода выше добавьте код создания трёх стен. Здесь фигурные скобки используются для группировки для удобства чтения кода:
    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
    
        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);
        }
  • Зададим новые координаты главному герою:
    C
    1
    
    circleBodyDef.position = (b2Vec2) { 200.f / ppm, 250.f / ppm };
  • Установим трение для ГГ:
    C
    1
    
    circleShapeDef.material.friction = friction;
  • Проведите сборку "build-web" и обновите страницы на Desktop и мобильном браузере, чтобы увидеть результат. Главный герой теперь движется вдоль стен с заданным трением

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



  • Для сборки в релиз нужно открыть файл "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 - 376 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-player-movement-sdl3-c.vercel.app
  • Примечание. Иногда при первом запуске Vercel может долго загружать приложение - бывает около минуты, поэтому лучше использовать для хостинга бесплатный GitHub Pages: ссылка
  • Примечание. Когда вы что-то измените в проекте и захотите загрузить изменённое приложение на сервер, то введите команду:
    Bash
    1
    
    vercel --prod

Миниатюры

Нажмите на изображение для увеличения
Название: b07733d3-add5-4ae0-b7bd-937222fe6af9.png
Просмотров: 7334
Размер:	2.0 Кб
ID:	11616
Нажмите на изображение для увеличения
Название: 61f99572-0a7d-4270-a599-74c45b884feb.png
Просмотров: 2640
Размер:	971 байт
ID:	11619
Нажмите на изображение для увеличения
Название: 47e98e58-f94e-4904-b77d-1452c9490b3b.png
Просмотров: 2646
Размер:	14.2 Кб
ID:	11622
Нажмите на изображение для увеличения
Название: ea00cd6e-b9d4-4ea3-a4d8-48e16284c9c5.png
Просмотров: 2653
Размер:	9.1 Кб
ID:	11623
Вложения
Тип файла: zip start-player-movement-sdl3-c.zip (6.6 Кб, 66 просмотров)
Тип файла: zip SDL3-devel-3.4.0-wasm.zip (1.22 Мб, 39 просмотров)
Тип файла: zip box2d-3.1.1-wasm.zip (555.4 Кб, 37 просмотров)
Тип файла: zip finish-player-movement-sdl3-c.zip (7.7 Кб, 98 просмотров)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Новые блоги и статьи
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