Сообщество Engee

Моделирование полёта дронов

Автор
avatar-shteinshtein
Notebook

Идеей для данного проекта послужил уже опубликованный проект под авторством yurev и названием "Моделирование полета БПЛА над 3D-ландшафтом".

Представленный код на языке Julia реализует расширенную симуляцию полёта нескольких дронов над 3D-ландшафтом.

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

Проект демонстрирует применение методов:

  • генерации ландшафтов,

  • физического моделирования движения,

  • координации агентов,

  • пространственного анализа видимости,

  • интерактивной визуализации в 2D и 3D.

    Краткий комментарий по используемым библиотекам и их назначению:

    plotlyjs()  # Статическая визуализация с интерактивными элементами

    gr()        # Высокопроизводительная отрисовка для анимации

    • LinearAlgebra – для векторных операций (нормализация расстояния, скалярное произведение).

    • Plots – основная библиотека визуализации.

    • Printf – форматированный вывод таблицы связи.

    • Random – управление случайной генерацией чисел.

    plotlyjs() и gr() – комбинированный подход: plotlyjs для статических графиков, gr – для плавной анимации.

1. Генерация неравномерного 3D-ландшафта

Функция generate_landscape(width_m, length_m, seed=nothing) создаёт рельеф с помощью многослойного шума.

Алгоритм

  • Создаётся равномерная сетка координат (x, y) с шагом 1 метр.

  • Высота z формируется как сумма:

    • двух тригонометрических компонент (sin, cos) с разными масштабами,

    • случайного шума (rand()) для локальных неровностей.

  • Результат инвертируется по осям для асимметричности.

Особенности

  • Поддержка случайности (seed) для воспроизводимости.

  • Высокая детализация рельефа за счёт комбинирования частот.

  • Визуализация через surface() с цветовой схемой terrain.

2. Генерация дронов и их целей

Функция generate_drones(N, x_grid, y_grid, z_grid, target_base=nothing) создаёт N дронов с уникальными стартовыми позициями и общей целью. Число дронов можно задать самостоятельно в коде (это должно быть целое цисло).

Ключевые параметры

Параметр

Значение

Описание

N

Int

Количество дронов

min_dist_between_drones

5.0 м

Минимальное расстояние между стартами

min_altitude

2.0 м

Минимальная высота над рельефом

target_base

[x, y, z]

Общая точка назначения (по умолчанию — центр)

Логика

  • Каждый дрон получает:

    • уникальную стартовую позицию,

    • общую цель (target_pos),

    • точку для возвращения (home_pos),

    • уникальный цвет (из палитры tab10).

  • Проверяется валидность позиции над рельефом и расстояние до других дронов.

  • При неудаче – вывод предупреждения.

3. Реалистичная динамика дрона

Функция update_drone_physics! моделирует физическую динамику с учётом инерции, ускорения и ограничений.

Учёт физики

  • Горизонтальное движение:

    • Желаемое направление – к цели.

    • Ограничение скорости (max_speed).

    • Ограничение поворота (turn_rate_deg) – имитирует инерцию.

    • Ускорение с ограничением (max_acceleration).

  • Вертикальное движение:

    • Поддержание высоты над рельефом (min_altitude).

    • Пропорциональный регулятор высоты.

    • Ограничение вертикальной скорости (max_vertical_speed).

  • Новая позиция вычисляется как position += velocity * dt.

Ключевые параметры

Параметр

Значение

Описание

max_speed

1.5 м/с

Макс. горизонтальная скорость

max_acceleration

0.3 м/с²

Макс. ускорение

max_vertical_speed

1.0 м/с

Макс. подъём/спуск

turn_rate_deg

15°/с

Угловая скорость поворота

dt

1.0 с

Шаг по времени

4. Координация и избегание столкновений

Функция avoid_collisions! реализует избегание столкновений на основе сил отталкивания.

Модель

·                     Для каждой пары дронов вычисляется вектор отталкивания, обратно пропорциональный квадрату расстояния.

·                     Сила применяется как импульс к скорости.

·                     Действует в пределах separation_distance (по умолчанию – 8 м).

Особенности

·                     Каждый дрон реагирует локально.

·                     Не изменяет траекторию цели – только корректирует текущую скорость.

·                     Устойчив к группам дронов.

5. Моделирование связи между дронами

Система связи оценивает качество соединения между всеми парами дронов с учётом:

·                     расстояния,

·                     прямой видимости,

·                     рельефа.

Функция calculate_connection_quality

·                     Затухание сигнала: линейное уменьшение с расстоянием (до max_range=30 м).

·                     Проверка прямой видимости: 20 точек вдоль линии между дронами.

·                                             Если рельеф выше линии связи (с запасом 1 м), связь = 0.

·                     Возвращает значение от 0.0 (нет связи) до 1.0 (отличная связь).

Функция get_network_status

·                     Возвращает матрицу связности размером N×N.

·                     Используется для анализа сети и визуализации.

6. Основной цикл симуляции

Функция simulate_drones(N, sim_steps=500) управляет всем процессом.

Этапы симуляции

1.                   Генерация ландшафта (150×150 м).

2.                   Создание N дронов с уникальными стартами и общей целью.

3.                   Пошаговое обновление:

·                                             Физика движения к цели (flying_to_target).

·                                             При достижении — возврат домой (flying_to_home).

·                                             При возвращении — приземление (landed).

4.                   На каждом шаге:

·                                             Избегание столкновений.

·                                             Расчёт матрицы связи.

5.                   Завершение при посадке всех дронов.

Логика статусов

·                     flying_to_target → flying_to_home → landed.

·                     Порог достижения: 2 метра.

Вывод

·                     В конце — таблица качества связи между всеми дронами.

·                     Формат: ID строк и столбцов, значения — от 0.00 до 1.00.

·                     Цвета: зелёный (хорошая), оранжевый (средняя), красный (плохая/нет).

7. Визуализация кооперативного полёта

Функция visualize_simulation создаёт анимированное представление полёта.

Графики

1.                   3D-поверхность:

·                                             Рельеф с цветовой схемой :terrain.

·                                             Траектории дронов (цветные линии).

·                                             Текущие позиции (точки).

·                                             Старты (ромбы), база (чёрный круг).

2.                   2D-вид сверху:

·                                             Контурный график рельефа.

·                                             Те же траектории и позиции.

·                                             Линии связи (при активации).

Линии связи

·                     Рисуются между дронами, если quality > 0.1.

·                     Цвет:

·                                             >0.7: зелёный,

·                                             0.3–0.7: оранжевый,

·                                             <0.3: красный.

·                     Толщина и прозрачность зависят от качества.

Анимация

·                     Кадры: по одному на каждый шаг траектории.

·                     Частота: fps=10 (настраивается).

·                     Результат: GIF-файл (multi_drone_flight_simulation.gif).

·                     Макет: два графика рядом в 3D и 2D варианте (layout=(1,2)).

Ключевые особенности проекта

Функция

Описание

Множественные дроны

N дронов с уникальными маршрутами.

Число задаётся пользователем

Физическое моделирование

Учёт инерции, ускорения, поворотов

Избегание столкновений

Локальные силы отталкивания

Модель связи

Затухание + проверка видимости

Динамическая визуализация

3D + 2D + анимация связей

Воспроизводимость

Поддержка случайных координат для ландшафта и старта

Разработанный проект представляет собой симуляцию полёта дронов с учётом ограничений. Он демонстрирует:

·                     Координацию в группе.

·                     Адаптацию к рельефу и динамическим препятствиям (другие дроны).

·                     Индикацию связи, зависящую от рельефа и расстояния.

In [ ]:
# Моделирование полёта дронов
# с избеганием столкновений

using LinearAlgebra
using Plots
using Printf
using Random
plotlyjs() # Используем plotlyjs для статических графиков
gr()       # Используем gr для анимации

# --- 1. Генерация неравномерного ландшафта ---

"""
    generate_landscape(width_m, length_m; seed=nothing)

Генерирует 3D-ландшафт с неравномерным рельефом.
"""
function generate_landscape(width_m, length_m; seed=nothing)
    if seed !== nothing
        Random.seed!(seed)
    end
    x = 0:1:width_m
    y = 0:1:length_m

    z = zeros(length(x), length(y))
    for (i, xi) in enumerate(x)
        for (j, yi) in enumerate(y)
            # Комбинируем несколько слоев шума для неравномерности
            z1 = 15 * sin(xi / 20) * cos(yi / 25)
            z2 = 10 * sin(xi / 10 + yi / 15)
            z3 = 5 * cos(xi / 30) * sin(yi / 20)
            z4 = 3 * (rand() - 0.5) # Мелкий шум
            z[i, j] = z1 + z2 + z3 + z4 + 10 # Базовая высота
        end
    end
    z = reverse(z, dims=(1, 2))

    plt = surface(x, y, z', c=:terrain, xlabel="X (м)", ylabel="Y (м)", zlabel="Z (м)", title="3D Ландшафт", legend=false)
    display(plt)
    return x, y, z
end

# --- 2. Генерация дронов ---

"""
    is_position_valid(x_pos, y_pos, z_pos, x_grid, y_grid, z_grid, min_altitude=1.0)

Проверяет, является ли позиция над ландшафтом и в допустимых границах.
"""
function is_position_valid(x_pos, y_pos, z_pos, x_grid, y_grid, z_grid, min_altitude=1.0)
    if x_pos < x_grid[1] || x_pos > x_grid[end] || y_pos < y_grid[1] || y_pos > y_grid[end]
        return false
    end
    xi = argmin(abs.(x_grid .- x_pos))
    yi = argmin(abs.(y_grid .- y_pos))
    terrain_height = z_grid[xi, yi]
    return z_pos >= (terrain_height + min_altitude)
end

"""
    generate_drones(N, x_grid, y_grid, z_grid, target_base=nothing)

Генерирует N дронов в случайных уникальных точках и определяет общую базу.
"""
function generate_drones(N, x_grid, y_grid, z_grid, target_base=nothing)
    drones = Vector{Dict{Symbol, Any}}()
    used_positions = Set{Tuple{Float64, Float64, Float64}}()
    min_dist_between_drones = 5.0 # Минимальное расстояние между стартовыми точками
    min_altitude = 2.0
    max_attempts = N * 100 # Ограничение попыток для избежания бесконечного цикла

    # Определяем общую базу (Target)
    if target_base === nothing
        base_x = x_grid[div(length(x_grid), 2)]
        base_y = y_grid[div(length(y_grid), 2)]
        base_xi = argmin(abs.(x_grid .- base_x))
        base_yi = argmin(abs.(y_grid .- base_y))
        base_z = z_grid[base_xi, base_yi] + min_altitude + 5.0 # Немного выше рельефа
        target_pos = [base_x, base_y, base_z]
    else
        target_pos = target_base
    end

    for i in 1:N
        pos = nothing
        attempts = 0
        while pos === nothing && attempts < max_attempts
            attempts += 1
            # Генерируем случайные координаты
            rand_x = x_grid[1] + (x_grid[end] - x_grid[1]) * rand()
            rand_y = y_grid[1] + (y_grid[end] - y_grid[1]) * rand()
            rand_z = 5.0 + (20.0 - 5.0) * rand() # Высота от 5 до 20 метров над "нулем"
            
            # Проверяем валидность позиции
            if is_position_valid(rand_x, rand_y, rand_z, x_grid, y_grid, z_grid, min_altitude)
                # Проверяем минимальное расстояние до уже созданных дронов
                is_too_close = false
                for existing_drone in drones
                    existing_pos = existing_drone[:start_pos]
                    dist = norm([rand_x, rand_y, rand_z] .- existing_pos)
                    if dist < min_dist_between_drones
                        is_too_close = true
                        break
                    end
                end
                if !is_too_close
                    pos = [rand_x, rand_y, rand_z]
                end
            end
        end

        if pos !== nothing
            drone = Dict(
                :id => i,
                :start_pos => pos,
                :target_pos => target_pos, # Общая цель для полета туда
                :home_pos => pos,          # Дом для полета обратно
                :position => copy(pos),
                :velocity => zeros(3),
                :path => [copy(pos)],
                :status => "flying_to_target", # flying_to_target, flying_to_home, landed
                :color => palette(:tab10)[i] # Уникальный цвет
            )
            push!(drones, drone)
            println("Дрон $i создан в точке $(round.(pos, digits=2))")
        else
            println("Не удалось создать дрон $i после $max_attempts попыток.")
        end
    end

    if length(drones) < N
        println("Предупреждение: создано только $(length(drones)) из $N дронов.")
    end

    println("Общая база (цель): $(round.(target_pos, digits=2))")
    return drones, target_pos
end

# --- 3. Динамика дрона ---

"""
    update_drone_physics!(drone, target, x_grid, y_grid, z_grid, dt=1.0;
                          max_speed=1.5, max_acceleration=0.3, max_vertical_speed=1.0,
                          turn_rate_deg=15.0, min_altitude=2.0)
                          
Обновляет физику дрона (позиция, скорость) с учетом динамики.
"""
function update_drone_physics!(drone, target, x_grid, y_grid, z_grid, dt=1.0;
                              max_speed=1.5, max_acceleration=0.3, max_vertical_speed=1.0,
                              turn_rate_deg=15.0, min_altitude=2.0)
    
    current_pos = drone[:position]
    velocity = drone[:velocity]
    turn_rate_rad = deg2rad(turn_rate_deg)

    # --- Желаемое направление и скорость ---
    direction_to_target_xy = normalize(target[1:2] .- current_pos[1:2])
    desired_speed_xy = max_speed
    desired_velocity_xy = direction_to_target_xy * desired_speed_xy

    # --- Ограничение на поворот ---
    current_speed_xy = norm(velocity[1:2])
    if current_speed_xy > 1e-6
        current_direction_xy = velocity[1:2] / current_speed_xy
        dot_product = clamp(dot(current_direction_xy, direction_to_target_xy), -1.0, 1.0)
        angle_to_target = acos(dot_product)

        if angle_to_target > turn_rate_rad * dt # turn_rate_rad уже в рад/сек
            cross_product_z = current_direction_xy[1] * direction_to_target_xy[2] - current_direction_xy[2] * direction_to_target_xy[1]
            sign = cross_product_z > 0 ? 1 : -1
            angle_to_turn = turn_rate_rad * dt
            cos_turn = cos(angle_to_turn)
            sin_turn = sin(angle_to_turn) * sign
            new_vx = current_direction_xy[1] * cos_turn - current_direction_xy[2] * sin_turn
            new_vy = current_direction_xy[1] * sin_turn + current_direction_xy[2] * cos_turn
            desired_velocity_xy = [new_vx, new_vy] * current_speed_xy
        end
    else
        desired_velocity_xy = direction_to_target_xy * min(max_acceleration * dt, desired_speed_xy)
    end

    # --- Применение ускорения к горизонтальной скорости ---
    delta_v_xy = desired_velocity_xy .- velocity[1:2]
    acceleration_magnitude = norm(delta_v_xy) / dt # a = dv/dt
    if acceleration_magnitude > 1e-10
        unit_accel = delta_v_xy / norm(delta_v_xy)
        applied_accel_magnitude = min(acceleration_magnitude, max_acceleration)
        applied_accel = unit_accel * applied_accel_magnitude
        velocity[1:2] .+= applied_accel * dt
    end

    # --- Ограничение максимальной скорости ---
    speed_xy = norm(velocity[1:2])
    if speed_xy > max_speed
        velocity[1:2] .*= (max_speed / speed_xy)
    end

    # --- Вертикальное движение ---
    next_x = current_pos[1] + velocity[1] * dt
    next_y = current_pos[2] + velocity[2] * dt
    next_xi = argmin(abs.(x_grid .- next_x))
    next_yi = argmin(abs.(y_grid .- next_y))
    next_xi = clamp(next_xi, 1, length(x_grid))
    next_yi = clamp(next_yi, 1, length(y_grid))
    terrain_height_next = z_grid[next_xi, next_yi]

    desired_altitude = min_altitude
    desired_z = terrain_height_next + desired_altitude
    desired_vz = (desired_z - current_pos[3]) / dt # Пропорциональный регулятор
    desired_vz = clamp(desired_vz, -max_vertical_speed, max_vertical_speed)
    
    # Простая модель вертикального ускорения
    vz_change = desired_vz - velocity[3]
    max_vz_change = max_acceleration * dt
    vz_change = clamp(vz_change, -max_vz_change, max_vz_change)
    velocity[3] += vz_change

    # Ограничиваем вертикальную скорость
    velocity[3] = clamp(velocity[3], -max_vertical_speed, max_vertical_speed)

    # --- Вычисляем новую позицию ---
    new_pos = current_pos .+ velocity * dt
    new_pos[1] = clamp(new_pos[1], x_grid[1], x_grid[end])
    new_pos[2] = clamp(new_pos[2], y_grid[1], y_grid[end])
    # Вертикальная коррекция уже учтена в vz

    drone[:position] = new_pos
    push!(drone[:path], copy(new_pos))
end

# --- 4. Координация и избегание столкновений ---

"""
    avoid_collisions!(drones, separation_distance=8.0, avoidance_strength=0.5)

Простая логика избегания столкновений на уровне изменения желаемой скорости.
"""
function avoid_collisions!(drones, separation_distance=8.0, avoidance_strength=0.5)
    N = length(drones)
    if N < 2
        return
    end

    # Рассчитываем силы отталкивания для каждого дрона
    repulsion_forces = [zeros(3) for _ in 1:N]

    for i in 1:N
        for j in 1:N
            if i != j
                pos_i = drones[i][:position]
                pos_j = drones[j][:position]
                vec_ij = pos_i .- pos_j
                dist = norm(vec_ij)

                if 0.1 < dist < separation_distance # Избегаем деления на ноль и действуем на расстоянии
                    # Сила отталкивания обратно пропорциональна квадрату расстояния
                    force_magnitude = avoidance_strength / (dist^2 + 1e-6) 
                    force_direction = vec_ij / (dist + 1e-6)
                    repulsion_forces[i] .+= force_direction * force_magnitude
                end
            end
        end
    end

    # Применяем силы как импульсы к скорости
    for i in 1:N
        # Ограничиваем величину изменения скорости
        force_clamped = clamp(norm(repulsion_forces[i]), 0.0, 1.0) * 
                        (repulsion_forces[i] / (norm(repulsion_forces[i]) + 1e-6))
        drones[i][:velocity] .+= force_clamped
    end
end

# --- 5. Моделирование связи ---

"""
    calculate_connection_quality(drone1, drone2, x_grid, y_grid, z_grid;
                                max_range=30.0, path_checks=20)

Рассчитывает качество связи между двумя дронами.
"""
function calculate_connection_quality(drone1, drone2, x_grid, y_grid, z_grid;
                                    max_range=30.0, path_checks=20)
    pos1 = drone1[:position]
    pos2 = drone2[:position]
    
    dx = pos1[1] - pos2[1]
    dy = pos1[2] - pos2[2]
    dz = pos1[3] - pos2[3]
    distance = sqrt(dx^2 + dy^2 + dz^2)

    # 1. Затухание сигнала с расстоянием
    if distance > max_range
        return 0.0
    end
    quality = 1.0 - (distance / max_range)

    # 2. Проверка на препятствия (прямая видимость)
    if quality > 0.01 # Проверяем только если сигнал ещё не нулевой
        for j in 1:path_checks
            t = j / (path_checks + 1) # Не проверяем концы
            check_x = pos1[1] + (pos2[1] - pos1[1]) * t
            check_y = pos1[2] + (pos2[2] - pos1[2]) * t
            xi_check = argmin(abs.(x_grid .- check_x))
            yi_check = argmin(abs.(y_grid .- check_y))
            if 1  xi_check  size(z_grid, 1) && 1  yi_check  size(z_grid, 2)
                terrain_z = z_grid[xi_check, yi_check]
                # Линейная интерполяция высоты линии связи
                line_z = pos1[3] + (pos2[3] - pos1[3]) * t
                if terrain_z > line_z - 1.0 # Если рельеф выше линии связи (с запасом)
                    return 0.0 # Полная потеря сигнала
                end
            end
        end
    end
    return max(0.0, quality)
end

"""
    get_network_status(drones, x_grid, y_grid, z_grid)

Возвращает матрицу качества связи между всеми парами дронов.
"""
function get_network_status(drones, x_grid, y_grid, z_grid)
    N = length(drones)
    connection_matrix = zeros(N, N)
    for i in 1:N
        for j in 1:N
            if i != j
                q = calculate_connection_quality(drones[i], drones[j], x_grid, y_grid, z_grid)
                connection_matrix[i, j] = q
            end
        end
    end
    return connection_matrix
end

# --- 6. Основной цикл симуляции ---

"""
    simulate_drones(N, sim_steps=500)
"""
function simulate_drones(N, sim_steps=500)
    println("=== Генерация ландшафта ===")
    # --- Изменено: увеличен размер ландшафта ---
    x_grid, y_grid, z_grid = generate_landscape(150, 150)

    println("\n=== Генерация дронов ===")
    drones, target_base = generate_drones(N, x_grid, y_grid, z_grid)

    if length(drones) == 0
        println("Ошибка: Не создано ни одного дрона. Завершение симуляции.")
        return nothing, nothing, nothing, nothing, nothing
    end

    println("\n=== Начало симуляции ===")
    connection_history = []
    all_paths = []

    # Для визуализации стартов, базы и домов
    start_positions = [d[:start_pos] for d in drones]
    home_positions = [d[:home_pos] for d in drones]

    for step in 1:sim_steps
        if step % 50 == 0
            println("Шаг симуляции: $step")
        end

        all_landed = true
        for drone in drones
            if drone[:status] == "flying_to_target"
                update_drone_physics!(drone, drone[:target_pos], x_grid, y_grid, z_grid)
                # Проверка достижения цели
                dist_to_target = norm(drone[:position][1:2] .- drone[:target_pos][1:2])
                if dist_to_target < 2.0 # Порог достижения
                    println("  Дрон $(drone[:id]) достиг базы. Начинаю возврат домой.")
                    drone[:status] = "flying_to_home"
                end
                all_landed = false
            elseif drone[:status] == "flying_to_home"
                update_drone_physics!(drone, drone[:home_pos], x_grid, y_grid, z_grid)
                # Проверка возвращения домой
                dist_to_home = norm(drone[:position][1:2] .- drone[:home_pos][1:2])
                if dist_to_home < 2.0 # Порог возвращения
                    println("  Дрон $(drone[:id]) вернулся домой и приземлился.")
                    drone[:status] = "landed"
                    # Добавим последнюю точку на дом
                    push!(drone[:path], copy(drone[:home_pos]))
                else
                    all_landed = false
                end
            end # если landed, ничего не делаем
        end

        # Координация: избегание столкновений
        avoid_collisions!(drones, 10.0, 0.8)

        # Моделирование связи
        conn_matrix = get_network_status(drones, x_grid, y_grid, z_grid)
        push!(connection_history, conn_matrix)

        # Проверка завершения
        if all_landed
            println("Все дроны завершили полет на шаге $step.")
            break
        end
    end

    println("\n=== Симуляция завершена ===")
    
    # --- Вывод таблицы связи ---
    if length(drones) > 0
        final_conn_matrix = get_network_status(drones, x_grid, y_grid, z_grid)
        println("\n=== Таблица качества связи между дронами (в конце симуляции) ===")
        print("ID  ")
        for j in 1:length(drones)
            @printf("%6d ", drones[j][:id])
        end
        println()
        for i in 1:length(drones)
            @printf("%3d ", drones[i][:id])
            for j in 1:length(drones)
                if i == j
                    print("   -   ")
                else
                    @printf("%6.2f ", final_conn_matrix[i, j])
                end
            end
            println()
        end
        println("================================================================")
    end
    
    return x_grid, y_grid, z_grid, drones, connection_history
end

# --- 7. Визуализация ---

"""
    visualize_simulation(x_grid, y_grid, z_grid, drones, connection_history, target_base;
                        fps=5, draw_connections=true)
"""
function visualize_simulation(x_grid, y_grid, z_grid, drones, connection_history, target_base;
                            fps=5, draw_connections=true)
    
    println("\n=== Создание визуализации ===")
    
    if drones === nothing
        println("Нет данных для визуализации.")
        return
    end

    N = length(drones)
    # --- Обновлено: создаем два графика ---
    base_surface = surface(x_grid, y_grid, z_grid', c=:terrain, legend=false, alpha=0.7,
                           xlabel="X (м)", ylabel="Y (м)", zlabel="Z (м)", title="3D Вид")

    base_topdown = plot(xlabel="X (м)", ylabel="Y (м)", title="Вид сверху", aspect_ratio=:equal, legend=false)
    contour!(base_topdown, x_grid, y_grid, z_grid', c=:terrain, alpha=0.5, levels=10)
    
    # Отметим общую базу на обоих графиках
    scatter!(base_surface, [target_base[1]], [target_base[2]], [target_base[3]], 
             marker=:circle, markercolor=:black, markersize=8, label="База (B)")
    scatter!(base_topdown, [target_base[1]], [target_base[2]], 
             marker=:circle, markercolor=:black, markersize=8, label="База (B)")

    # Отметим стартовые позиции на обоих графиках
    for drone in drones
        scatter!(base_surface, [drone[:start_pos][1]], [drone[:start_pos][2]], [drone[:start_pos][3]], 
                 marker=:diamond, markercolor=drone[:color], markersize=6, label="Старт $(drone[:id])")
        scatter!(base_topdown, [drone[:start_pos][1]], [drone[:start_pos][2]], 
                 marker=:diamond, markercolor=drone[:color], markersize=6, label=false)
    end

    # Определим общее количество кадров
    max_path_length = maximum(length(drone[:path]) for drone in drones)
    
    # Предварительно вычислим пути для эффективности
    all_paths = [drone[:path] for drone in drones]

    anim = @animate for frame in 1:max_path_length
        plt3d = deepcopy(base_surface)
        plt2d = deepcopy(base_topdown)
        
        current_positions = []
        current_ids = []

        # Рисуем траектории и текущие позиции
        for (i, drone) in enumerate(drones)
            path = all_paths[i]
            path_len = length(path)
            
            if frame <= path_len
                # Текущая позиция
                current_pos = path[frame]
                push!(current_positions, current_pos)
                push!(current_ids, drone[:id])
                
                scatter!(plt3d, [current_pos[1]], [current_pos[2]], [current_pos[3]], 
                         marker=:circle, markercolor=drone[:color], markersize=6, label=false)
                scatter!(plt2d, [current_pos[1]], [current_pos[2]], 
                         marker=:circle, markercolor=drone[:color], markersize=6, label=false)
                
                # Рисуем пройденную часть пути
                if frame > 1
                    path_to_draw = path[1:frame]
                    plot!(plt3d, [p[1] for p in path_to_draw], [p[2] for p in path_to_draw], [p[3] for p in path_to_draw],
                          linecolor=drone[:color], linewidth=2, label=false)
                    plot!(plt2d, [p[1] for p in path_to_draw], [p[2] for p in path_to_draw],
                          linecolor=drone[:color], linewidth=2, label=false)
                end
            else
                # Если дрон уже закончил путь, показываем его финальную позицию
                final_pos = path[end]
                scatter!(plt3d, [final_pos[1]], [final_pos[2]], [final_pos[3]], 
                         marker=:circle, markercolor=drone[:color], markersize=6, label=false)
                scatter!(plt2d, [final_pos[1]], [final_pos[2]], 
                         marker=:circle, markercolor=drone[:color], markersize=6, label=false)
                plot!(plt3d, [p[1] for p in path], [p[2] for p in path], [p[3] for p in path],
                      linecolor=drone[:color], linewidth=2, label=false)
                plot!(plt2d, [p[1] for p in path], [p[2] for p in path],
                      linecolor=drone[:color], linewidth=2, label=false)
            end
        end

        # Рисуем линии связи
        if draw_connections && frame <= length(connection_history)
            conn_matrix = connection_history[frame]
            for i in 1:N
                for j in (i+1):N
                    if frame <= length(all_paths[i]) && frame <= length(all_paths[j])
                        q = conn_matrix[i, j]
                        if q > 0.1 # Рисуем только если есть связь
                            pos1 = all_paths[i][frame]
                            pos2 = all_paths[j][frame]
                            conn_color = :green
                            if q < 0.3
                                conn_color = :red
                            elseif q < 0.7
                                conn_color = :orange
                            end
                            plot!(plt3d, [pos1[1], pos2[1]], [pos1[2], pos2[2]], [pos1[3], pos2[3]],
                                  linecolor=conn_color, linewidth=2*q, alpha=q, label=false)
                            plot!(plt2d, [pos1[1], pos2[1]], [pos1[2], pos2[2]],
                                  linecolor=conn_color, linewidth=2*q, alpha=q, label=false)
                        end
                    end
                end
            end
        end

        # Фокусировка на первом дроне для начала, затем на центре группы
        # Для 3D графика
        #if length(current_positions) > 0
        #     focus_drone_pos = current_positions[1]
        #     xlims!(plt3d, focus_drone_pos[1] - 25, focus_drone_pos[1] + 25)
        #     ylims!(plt3d, focus_drone_pos[2] - 25, focus_drone_pos[2] + 25)
             # Для 2D графика также фокусируемся
        #     xlims!(plt2d, focus_drone_pos[1] - 25, focus_drone_pos[1] + 25)
        #     ylims!(plt2d, focus_drone_pos[2] - 25, focus_drone_pos[2] + 25)
        #end
        
        # --- Обновлено: используем layout для размещения графиков ---
        plot(plt3d, plt2d, layout=(1, 2), size=(1200, 600))
        
    end every 1 # Каждый кадр для плавности

    filename = "multi_drone_flight_simulation.gif"
    gif(anim, filename, fps=fps)
    println("Анимация сохранена в файл: $filename")
    return filename
end

# --- Основной блок выполнения ---
function main()
    NUMBER_OF_DRONES = 10 # <--- ИЗМЕНИТЕ ЭТО ЗНАЧЕНИЕ ДЛЯ ДРУГОГО КОЛИЧЕСТВА ДРОНОВ
    SIMULATION_STEPS = 1000

    x_grid, y_grid, z_grid, drones, connection_history = simulate_drones(NUMBER_OF_DRONES, SIMULATION_STEPS)
    if x_grid !== nothing
        # Найдем target_base из данных дронов для визуализации
        target_base = drones[1][:target_pos] 
        visualize_simulation(x_grid, y_grid, z_grid, drones, connection_history, target_base, fps=10)
    end
    println("\n=== Проект завершен ===")
end

# Запустить симуляцию
main()
=== Генерация ландшафта ===
=== Генерация дронов ===
Дрон 1 создан в точке [80.71, 0.3, 18.59]
Дрон 2 создан в точке [41.28, 58.93, 19.46]
Дрон 3 создан в точке [2.44, 30.15, 7.16]
Дрон 4 создан в точке [52.12, 120.93, 9.76]
Дрон 5 создан в точке [46.44, 130.13, 8.9]
Дрон 6 создан в точке [142.58, 50.13, 19.75]
Дрон 7 создан в точке [79.75, 23.07, 14.18]
Дрон 8 создан в точке [149.95, 78.77, 9.95]
Дрон 9 создан в точке [4.03, 94.43, 10.11]
Дрон 10 создан в точке [75.05, 98.34, 7.64]
Общая база (цель): [74.0, 74.0, 29.28]

=== Начало симуляции ===
  Дрон 10 достиг базы. Начинаю возврат домой.
  Дрон 2 достиг базы. Начинаю возврат домой.
  Дрон 7 достиг базы. Начинаю возврат домой.
  Дрон 4 достиг базы. Начинаю возврат домой.
  Дрон 5 достиг базы. Начинаю возврат домой.
  Дрон 10 вернулся домой и приземлился.
Шаг симуляции: 50
  Дрон 6 достиг базы. Начинаю возврат домой.
  Дрон 9 достиг базы. Начинаю возврат домой.
  Дрон 1 достиг базы. Начинаю возврат домой.
  Дрон 8 достиг базы. Начинаю возврат домой.
  Дрон 3 достиг базы. Начинаю возврат домой.
  Дрон 2 вернулся домой и приземлился.
  Дрон 7 вернулся домой и приземлился.
  Дрон 4 вернулся домой и приземлился.
  Дрон 5 вернулся домой и приземлился.
Шаг симуляции: 100
  Дрон 9 вернулся домой и приземлился.
  Дрон 6 вернулся домой и приземлился.
  Дрон 1 вернулся домой и приземлился.
  Дрон 8 вернулся домой и приземлился.
  Дрон 3 вернулся домой и приземлился.
Все дроны завершили полет на шаге 127.

=== Симуляция завершена ===

=== Таблица качества связи между дронами (в конце симуляции) ===
ID       1      2      3      4      5      6      7      8      9     10 
  1    -     0.00   0.00   0.00   0.00   0.00   0.01   0.00   0.00   0.00 
  2   0.00    -     0.00   0.00   0.00   0.00   0.00   0.00   0.00   0.00 
  3   0.00   0.00    -     0.00   0.00   0.00   0.00   0.00   0.00   0.00 
  4   0.00   0.00   0.00    -     0.59   0.00   0.00   0.00   0.00   0.00 
  5   0.00   0.00   0.00   0.59    -     0.00   0.00   0.00   0.00   0.00 
  6   0.00   0.00   0.00   0.00   0.00    -     0.00   0.00   0.00   0.00 
  7   0.01   0.00   0.00   0.00   0.00   0.00    -     0.00   0.00   0.00 
  8   0.00   0.00   0.00   0.00   0.00   0.00   0.00    -     0.00   0.00 
  9   0.00   0.00   0.00   0.00   0.00   0.00   0.00   0.00    -     0.00 
 10   0.00   0.00   0.00   0.00   0.00   0.00   0.00   0.00   0.00    -   
================================================================

=== Создание визуализации ===
Анимация сохранена в файл: multi_drone_flight_simulation.gif

=== Проект завершен ===
[ Info: Saved animation to /user/multi_drone_flight_simulation.gif

drone5.gif

drone6.gif

drone8.gif