SDL3 Desktop: 2D Spatial Audio (MinGW 13.1) SDL3 для Desktop: Пространственный 2D-звук (MinGW 13.1)

A guide to configuring and implementing 2D spatial audio using the SDL3_mixer extension library with CMake and MinGW. Руководство по настройке и реализации пространственного 2D-звука с использованием библиотеки расширения SDL3_mixer, CMake и MinGW.

Prerequisite: Ensure you have SDL3 and SDL3_mixer already configured. See the SDL3_mixer Setup Guide. Предварительное условие: Убедитесь, что SDL3 и SDL3_mixer уже настроены. См. руководство по настройке SDL3_mixer.

1. Project Structure and Assets 1. Структура проекта и ресурсы

Before creating the files, download the required asset: Перед созданием файлов скачайте необходимый ресурс:

C Project: Create an empty folder named spatial-2d-audio-sdl3-mixer-c and set up the following hierarchy by creating new CMakeLists.txt and main.c files:

Проект на C: Создайте пустую папку с именем spatial-2d-audio-sdl3-mixer-c и подготовьте следующую иерархию, создав файлы CMakeLists.txt и main.c:

spatial-2d-audio-sdl3-mixer-c/
├── CMakeLists.txt
├── assets/audio/
│   └── Picked Coin Echo 2.wav
└── src/
    └── main.c

C++ Project: Create an empty folder named spatial-2d-audio-sdl3-mixer-cpp and set up the following hierarchy by creating new CMakeLists.txt and main.cpp files:

Проект на C++: Создайте пустую папку с именем spatial-2d-audio-sdl3-mixer-cpp и подготовьте следующую иерархию, создав файлы CMakeLists.txt и main.cpp:

spatial-2d-audio-sdl3-mixer-cpp/
├── CMakeLists.txt
├── assets/audio/
│   └── Picked Coin Echo 2.wav
└── src/
    └── main.cpp

2. CMake Configuration 2. Конфигурация CMake

C Project: Copy and paste the following code into the CMakeLists.txt file:

Проект на C: Скопируйте и вставьте следующее содержимое в файл CMakeLists.txt:

set(CMAKE_BUILD_TYPE "Debug")
cmake_minimum_required(VERSION 3.21)
project(mixer-sdl3-c)

set(SDL3_DIR "C:/libs/SDL3-devel-3.4.8-mingw/lib/cmake/SDL3")
set(SDL3_mixer_DIR "C:/libs/SDL3_mixer-devel-3.2.2-mingw/lib/cmake/SDL3_mixer")

find_package(SDL3 REQUIRED)
find_package(SDL3_mixer REQUIRED)

add_executable(app)
target_sources(app PRIVATE src/main.c)

# Copy the assets folder to the dist folder
if(EXISTS "${CMAKE_SOURCE_DIR}/assets")
    add_custom_command(TARGET app POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
                "${CMAKE_SOURCE_DIR}/assets"
                "$<TARGET_FILE_DIR:app>/assets"
        COMMENT "Copying assets directory"
    )
endif()

target_link_libraries(app PRIVATE SDL3_mixer::SDL3_mixer SDL3::SDL3)
target_link_options(app PRIVATE -mconsole)

C++ Project: Copy and paste the following code into the CMakeLists.txt file:

Проект на C++: Скопируйте и вставьте следующее содержимое в файл CMakeLists.txt:

set(CMAKE_BUILD_TYPE "Debug")
cmake_minimum_required(VERSION 3.21)
project(mixer-sdl3-cpp)

set(SDL3_DIR "C:/libs/SDL3-devel-3.4.8-mingw/lib/cmake/SDL3")
set(SDL3_mixer_DIR "C:/libs/SDL3_mixer-devel-3.2.2-mingw/lib/cmake/SDL3_mixer")

find_package(SDL3 REQUIRED)
find_package(SDL3_mixer REQUIRED)

add_executable(app)
target_sources(app PRIVATE src/main.cpp)

# Copy the assets folder to the dist folder
if(EXISTS "${CMAKE_SOURCE_DIR}/assets")
    add_custom_command(TARGET app POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
                "${CMAKE_SOURCE_DIR}/assets"
                "$<TARGET_FILE_DIR:app>/assets"
        COMMENT "Copying assets directory"
    )
endif()

target_link_libraries(app PRIVATE SDL3_mixer::SDL3_mixer SDL3::SDL3)
target_link_options(app PRIVATE -mconsole)

3. Source Code 3. Исходный код

Copy and paste the following code into the src/main.c (or src/main.cpp) file:

Скопируйте и вставьте следующее содержимое в файл src/main.c (или src/main.cpp):

#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_mixer/SDL_mixer.h>

// Window dimensions
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600

// Structures for game objects
typedef struct
{
    float x, y;
    float size;
    float speed;
    float angle; // Player's facing direction (in radians)
} Player;

typedef struct
{
    float x, y;
    float size;
    float maxSoundDistance;
} SoundSource;

// Global variables (in a production project, it is better to wrap these into appstate)
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static MIX_Mixer *mixer = NULL;
static MIX_Audio *audio = NULL;
static MIX_Track *track = NULL;

static Player player;
static SoundSource coin;

// Keyboard state tracking
static bool keys[SDL_SCANCODE_COUNT] = { false };

// Function to update audio parameters based on 2D coordinates
void Update2DSound(MIX_Track *track, Player p, SoundSource src)
{
    // Calculate distance and distance-based volume attenuation
    float dx = src.x - p.x;
    float dy = src.y - p.y;
    float distance = sqrtf(dx * dx + dy * dy);

    float volume = 0.0f;
    if (distance < src.maxSoundDistance)
    {
        volume = 1.0f - (distance / src.maxSoundDistance);
    }

    // Prevent the track from entering a hard stop/sleep state.
    // If volume drops to 0, we set it to a tiny value instead.
    // This keeps the track active and looping in the background.
    if (volume <= 0.0f)
    {
        volume = 0.0001f;
    }

    // Apply global track volume (gain)
    MIX_SetTrackGain(track, volume);

    // Before returning, reset the stereo panning to the center
    // so the mixer handles the silent loop properly.
    // Check against the safety cutoff value.
    if (volume <= 0.0001f || distance < 0.1f)
    {
        MIX_StereoGains flatGains = { 1.0f, 1.0f };
        MIX_SetTrackStereo(track, &flatGains);
        return;
    }

    // Calculate left and right channel balance
    float viewX = cosf(p.angle);
    float viewY = sinf(p.angle);
    float soundX = dx / distance;
    float soundY = dy / distance;

    // Cross product: < 0 (left side), > 0 (right side)
    float cross = (viewX * soundY) - (viewY * soundX);

    // Structure to hold stereo channel gain coefficients
    MIX_StereoGains gains;

    // Minimum volume threshold for the opposite ear (20%)
    // This ensures the opposite channel never drops into complete silence
    float minStereoLeak = 0.2f; // Sound leakage
    // Note: The minStereoLeak value can be adjusted between 
    // 0.1f and 0.3f to find the most natural acoustic balance.

    if (cross > 0.0f)
    {
        // Source is on the right: fade out the left ear but keep it above minStereoLeak,
        // while the right ear stays at maximum volume (1.0)
        // cross peaks at 1.0 when the object is strictly at a 90-degree angle to the right
        gains.left = 1.0f - (cross * (1.0f - minStereoLeak));
        gains.right = 1.0f;
    }
    else
    {
        // Source is on the left: fade out the right ear but keep it above minStereoLeak,
        // while the left ear stays at maximum volume
        // Since cross is negative, we add it to subtract the value correctly
        gains.left = 1.0f;
        gains.right = 1.0f + (cross * (1.0f - minStereoLeak));
    }

    // Clamp values to the [0.0, 1.0] range
    if (gains.left < 0.0f)
        gains.left = 0.0f;
    if (gains.right < 0.0f)
        gains.right = 0.0f;

    // Pass the gains structure pointer to the SDL3 Mixer function
    MIX_SetTrackStereo(track, &gains);
}

void DrawCircle(SDL_Renderer *r, float centerX, float centerY, float radius, int segments)
{
    float angleStep = (2.0f * (float)M_PI) / (float)segments;

    // Set the initial starting point (at angle = 0)
    float prevX = centerX + radius;
    float prevY = centerY;

    for (int i = 1; i <= segments; i++)
    {
        float angle = (float)i * angleStep;
        float nextX = centerX + cosf(angle) * radius;
        float nextY = centerY + sinf(angle) * radius;

        // Draw a line segment from the previous point to the current one
        SDL_RenderLine(r, prevX, prevY, nextX, nextY);

        prevX = nextX;
        prevY = nextY;
    }
}

SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO))
    {
        return SDL_APP_FAILURE;
    }

    if (!MIX_Init())
    {
        return SDL_APP_FAILURE;
    }

    // Create the window and renderer in a single call using SDL3
    if (!SDL_CreateWindowAndRenderer("SDL3 2D Spatial Audio", SCREEN_WIDTH, SCREEN_HEIGHT, 0, &window, &renderer))
    {
        return SDL_APP_FAILURE;
    }

    mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
    if (!mixer)
    {
        return SDL_APP_FAILURE;
    }

    // Load a looping sound effect (e.g., ambient hum from an object or ticking clock)
    audio = MIX_LoadAudio(mixer, "assets/audio/Picked Coin Echo 2.wav", true);
    if (!audio)
    {
        SDL_Log("Load failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    track = MIX_CreateTrack(mixer);
    if (!track)
        return SDL_APP_FAILURE;

    MIX_SetTrackAudio(track, audio);
    MIX_SetTrackLoops(track, -1); // -1 specifies infinite looping

    // Initialize player state
    player.x = 150.0f;
    player.y = 250.0f;
    player.size = 20.0f;
    player.speed = 150.0f;  // Pixels per second
    player.angle = -M_PI_2; // Facing straight up by default (-90 degrees)

    // Initialize sound source state (located in the upper right section)
    coin.x = 600.0f;
    coin.y = 150.0f;
    coin.size = 15.0f;
    coin.maxSoundDistance = 400.0f; // Audio audibility radius

    // Enforce an immediate spatial audio update matching initial positions
    Update2DSound(track, player, coin);

    // Start playing the audio track
    MIX_PlayTrack(track, 0);

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
    if (event->type == SDL_EVENT_QUIT)
    {
        return SDL_APP_SUCCESS;
    }

    // Process keyboard inputs
    if (event->type == SDL_EVENT_KEY_DOWN)
    {
        keys[event->key.scancode] = true;
    }
    if (event->type == SDL_EVENT_KEY_UP)
    {
        keys[event->key.scancode] = false;
    }

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    // Retrieve delta_time directly in SDL3 to ensure smooth frame-rate independent updates
    static Uint64 last_time = 0;
    if (last_time == 0)
        last_time = SDL_GetTicks();
    Uint64 current_time = SDL_GetTicks();
    float dt = (current_time - last_time) / 1000.0f;
    last_time = current_time;

    // --- 1. INPUT PROCESSING & MOVEMENT ---
    // Movement translation (WASD and arrow keys)
    if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP])
        player.y -= player.speed * dt;
    if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN])
        player.y += player.speed * dt;
    if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT])
        player.x -= player.speed * dt;
    if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT])
        player.x += player.speed * dt;

    // Head rotation / view orientation (Q and E keys)
    // Allows testing spatial audio panning adjustments on rotation
    if (keys[SDL_SCANCODE_Q])
        player.angle -= 3.0f * dt; // Rotate counter-clockwise
    if (keys[SDL_SCANCODE_E])
        player.angle += 3.0f * dt; // Rotate clockwise

    // Check if the short sound effect sample has finished playing.
    // If it has stopped, restart it manually to create a seamless loop effect.
    if (!MIX_TrackPlaying(track))
    {
        MIX_PlayTrack(track, 0);
    }

    // --- 2. AUDIO UPDATE ---
    Update2DSound(track, player, coin);

    // --- 3. RENDER PASS ---
    // Clear the screen buffer with a flat black color
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);

    // Draw the sound audibility boundary (a gray circle built from 64 segments)
    // This provides a visual representation of the maxSoundDistance range
    SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
    DrawCircle(renderer, coin.x, coin.y, coin.maxSoundDistance, 64);

    // Draw the sound source (Green square)
    SDL_FRect coinRect = { coin.x - coin.size / 2, coin.y - coin.size / 2, coin.size, coin.size };
    SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
    SDL_RenderFillRect(renderer, &coinRect);

    // Draw the Player (Blue square)
    SDL_FRect playerRect = { player.x - player.size / 2, player.y - player.size / 2, player.size, player.size };
    SDL_SetRenderDrawColor(renderer, 0, 100, 255, 255);
    SDL_RenderFillRect(renderer, &playerRect);

    // Draw the player's vector line of sight (Red line) to visualize facing direction
    float lineLength = 25.0f;
    float targetX = player.x + cosf(player.angle) * lineLength;
    float targetY = player.y + sinf(player.angle) * lineLength;
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
    SDL_RenderLine(renderer, player.x, player.y, targetX, targetY);

    SDL_RenderPresent(renderer);

    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    MIX_Quit();
}
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_mixer/SDL_mixer.h>

// Размеры окна
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600

// Структуры для игровых объектов
typedef struct
{
    float x, y;
    float size;
    float speed;
    float angle; // Куда смотрит игрок (в радианах)
} Player;

typedef struct
{
    float x, y;
    float size;
    float maxSoundDistance;
} SoundSource;

// Глобальные переменные (в реальном проекте лучше упаковать в appstate)
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static MIX_Mixer *mixer = NULL;
static MIX_Audio *audio = NULL;
static MIX_Track *track = NULL;

static Player player;
static SoundSource coin;

// Состояние нажатых клавиш
static bool keys[SDL_SCANCODE_COUNT] = { false };

// Функция обновления звука на основе 2D координат
void Update2DSound(MIX_Track *track, Player p, SoundSource src)
{
    // Расчет расстояния и громкости
    float dx = src.x - p.x;
    float dy = src.y - p.y;
    float distance = sqrtf(dx * dx + dy * dy);

    float volume = 0.0f;
    if (distance < src.maxSoundDistance)
    {
        volume = 1.0f - (distance / src.maxSoundDistance);
    }

    // Защита от "засыпания" трека.
    // Если volume равен 0, ставим ничтожно малое число.
    // Трек продолжит крутиться в фоне.
    if (volume <= 0.0f)
    {
        volume = 0.0001f;
    }

    // Общая громкость (усиление) трека
    MIX_SetTrackGain(track, volume);

    // Перед return сначала сбрасываем стерео-баланс в центр,
    // чтобы микшер корректно обрабатывал «тишину» в цикле.
    // Сравниваем со значением отсечки безопасности.
    if (volume <= 0.0001f || distance < 0.1f)
    {
        MIX_StereoGains flatGains = { 1.0f, 1.0f };
        MIX_SetTrackStereo(track, &flatGains);
        return;
    }

    // Расчет баланса левого и правого каналов
    float viewX = cosf(p.angle);
    float viewY = sinf(p.angle);
    float soundX = dx / distance;
    float soundY = dy / distance;

    // Косое произведение: < 0 (слева), > 0 (справа)
    float cross = (viewX * soundY) - (viewY * soundX);

    // Создаем структуру для хранения коэффициентов громкости каналов
    MIX_StereoGains gains;

    // Минимальный порог звука для противоположного уха (20%)
    // Благодаря этому противоположное ухо никогда не замолкнет полностью
    float minStereoLeak = 0.2f; // Утечка звука
    // Замечание. Значение minStereoLeak можно крутить в пределах от
    // 0.1f до 0.3f, подбирая наиболее приятный на слух баланс

    if (cross > 0.0f)
    {
        // Источник справа: левое ухо затухает, но не ниже порога minStereoLeak,
        // а правое горит на максимум (1.0)
        // cross на максимуме равен 1.0 (когда объект строго справа под 90 градусов)
        gains.left = 1.0f - (cross * (1.0f - minStereoLeak));
        gains.right = 1.0f;
    }
    else
    {
        // Источник слева: правое ухо затухает, но не ниже порога minStereoLeak,
        // а левое на максимум
        // Поскольку cross отрицательный, используем fabsf() или + cross
        gains.left = 1.0f;
        gains.right = 1.0f + (cross * (1.0f - minStereoLeak));
    }

    // Защита от выхода за пределы [0.0, 1.0]
    if (gains.left < 0.0f)
        gains.left = 0.0f;
    if (gains.right < 0.0f)
        gains.right = 0.0f;

    // Передаем указатель на структуру в функцию SDL3 Mixer
    MIX_SetTrackStereo(track, &gains);
}

void DrawCircle(SDL_Renderer *r, float centerX, float centerY, float radius, int segments)
{
    float angleStep = (2.0f * (float)M_PI) / (float)segments;

    // Задаем начальную точку (при угле = 0)
    float prevX = centerX + radius;
    float prevY = centerY;

    for (int i = 1; i <= segments; i++)
    {
        float angle = (float)i * angleStep;
        float nextX = centerX + cosf(angle) * radius;
        float nextY = centerY + sinf(angle) * radius;

        // Соединяем предыдущую точку с текущей
        SDL_RenderLine(r, prevX, prevY, nextX, nextY);

        prevX = nextX;
        prevY = nextY;
    }
}

SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO))
    {
        return SDL_APP_FAILURE;
    }

    if (!MIX_Init())
    {
        return SDL_APP_FAILURE;
    }

    // Создаем окно и рендерер одной функцией в SDL3
    if (!SDL_CreateWindowAndRenderer("SDL3 2D Spatial Audio", SCREEN_WIDTH, SCREEN_HEIGHT, 0, &window, &renderer))
    {
        return SDL_APP_FAILURE;
    }

    mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
    if (!mixer)
    {
        return SDL_APP_FAILURE;
    }

    // Загружаем зацикленный звук (например, фоновый гул от объекта или тиканье)
    audio = MIX_LoadAudio(mixer, "assets/audio/Picked Coin Echo 2.wav", true);
    if (!audio)
    {
        SDL_Log("Load failed: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    track = MIX_CreateTrack(mixer);
    if (!track)
        return SDL_APP_FAILURE;

    MIX_SetTrackAudio(track, audio);
    MIX_SetTrackLoops(track, -1); // -1 означает бесконечный повтор звука

    // Инициализация игрока
    player.x = 150.0f;
    player.y = 250.0f;
    player.size = 20.0f;
    player.speed = 150.0f;  // пикселей в секунду
    player.angle = -M_PI_2; // смотрит вверх по умолчанию (-90 градусов)

    // Инициализация источника звука (в правой верхней части)
    coin.x = 600.0f;
    coin.y = 150.0f;
    coin.size = 15.0f;
    coin.maxSoundDistance = 400.0f; // Радиус слышимости звука

    // Принудительно обновляем 3D параметры звука под стартовые позиции
    Update2DSound(track, player, coin);

    // Запускаем трек
    MIX_PlayTrack(track, 0);

    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)
    {
        keys[event->key.scancode] = true;
    }
    if (event->type == SDL_EVENT_KEY_UP)
    {
        keys[event->key.scancode] = false;
    }

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void *appstate)
{
    // В SDL3 для плавной анимации мы получаем delta_time (время кадра) напрямую
    static Uint64 last_time = 0;
    if (last_time == 0)
        last_time = SDL_GetTicks();
    Uint64 current_time = SDL_GetTicks();
    float dt = (current_time - last_time) / 1000.0f;
    last_time = current_time;

    // Обработка ввода и движение
    // Движение (WASD и стрелки)
    if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP])
        player.y -= player.speed * dt;
    if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN])
        player.y += player.speed * dt;
    if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT])
        player.x -= player.speed * dt;
    if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT])
        player.x += player.speed * dt;

    // Вращение взгляда игрока (например, на клавиши Q и E)
    // Чтобы вы могли проверить, как меняется панорама при повороте "головы"
    if (keys[SDL_SCANCODE_Q])
        player.angle -= 3.0f * dt; // поворот против часовой
    if (keys[SDL_SCANCODE_E])
        player.angle += 3.0f * dt; // поворот по часовой

    // Проверяем, не закончился ли наш короткий сэмпл.
    // Если он остановился — запускаем его заново, создавая эффект петли (loop)
    if (!MIX_TrackPlaying(track))
    {
        MIX_PlayTrack(track, 0);
    }

    // Обновление звука
    Update2DSound(track, player, coin);

    // Рендеринг (отрисовка)
    // Очищаем экран (черный цвет)
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);

    // Рисуем радиус слышимости источника звука (серая окружность из 64 отрезков)
    SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
    DrawCircle(renderer, coin.x, coin.y, coin.maxSoundDistance, 64);

    // Рисуем источник звука (Зеленый квадрат)
    SDL_FRect coinRect = { coin.x - coin.size / 2, coin.y - coin.size / 2, coin.size, coin.size };
    SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
    SDL_RenderFillRect(renderer, &coinRect);

    // Рисуем Игрока (Синий квадрат)
    SDL_FRect playerRect = { player.x - player.size / 2, player.y - player.size / 2, player.size, player.size };
    SDL_SetRenderDrawColor(renderer, 0, 100, 255, 255);
    SDL_RenderFillRect(renderer, &playerRect);

    // Рисуем "линию взгляда" игрока (Красная линия), чтобы видеть куда он повернут
    float lineLength = 25.0f;
    float targetX = player.x + cosf(player.angle) * lineLength;
    float targetY = player.y + sinf(player.angle) * lineLength;
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
    SDL_RenderLine(renderer, player.x, player.y, targetX, targetY);

    SDL_RenderPresent(renderer);

    return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    MIX_Quit();
}

4. Opening the Project in IDEs 4. Открытие проекта в IDE

Open the CMakeLists.txt file in CLion or Qt Creator. CMake will handle the rest. Откройте файл CMakeLists.txt в CLion или Qt Creator. CMake позаботится об остальном.

5. Automation Scripts (.bat) 5. Скрипты автоматизации (.bat)

You can open the project folder in Sublime Text 4 (or Notepad++). Create the following .bat scripts in the project root directory to automate the configuration, building, and running of your application:

Вы можете открыть папку проекта в Sublime Text 4 (или Notepad++). Создайте следующие .bat скрипты в корневой директории проекта для автоматизации конфигурации, сборки и запуска вашего приложения:

1. config-exe.bat
cmake -G "MinGW Makefiles" -S . -B dist/exe
2. build-exe.bat
cd dist\exe
cmake --build .
cd ..\..
3. run-exe.bat
dist\exe\app

To build and launch the application, run these scripts in the terminal in the following order:

Чтобы собрать и запустить приложение, выполните эти скрипты в терминале в следующем порядке:

config-exe
build-exe
run-exe

6. Source Code & Downloads 6. Исходный код и загрузки

You can download the complete configured project as a ZIP archive or explore the source code directly on GitHub: Вы можете скачать готовую настроенную сборку проекта в виде ZIP-архива или изучить исходный код прямо на GitHub:


Support My Work Поддержать проект

If these tutorials helped you, consider buying me a coffee! Если эти туториалы вам помогли, вы можете поддержать автора.

Sberbank

Sberbank SBP QR Code

Direct transfer via phone number Перевод по номеру телефона

+7 (917) 212-29-59

Bybit (USDT TRC20)

Bybit USDT TRC20 QR Code

Support via Cryptocurrency Поддержка криптовалютой

TMtY1YifNf6FKvgeFmqKGQR4NStKr3csGp