Модель живучести роя БПЛА
ОСНОВНЫЕ ТЕРМИНЫ
- Дрон (он же агент или БПЛА) - это не просто радиоуправляемый коптер, а самостоятельная вычислительная единица. Каждый дрон имеет свои координаты, скорость, зону видимости и набор правил поведения. Он способен сам принимать локальные решения - например, притормозить, чтобы не врезаться в соседа, или сменить курс.
- Рой дронов - это группа дронов, объединенная в сеть. Главная фишка роевого интеллекта в том, что у него нет центрального управляющего элемента. Дроны обмениваются данными только с ближайшими соседями. Получается, что каждый выполняет простые правила на своем уровне, но в итоге вся группа движется, перестраивается и реагирует на угрозы как единый сложный организм.
МОДЕЛЬ ЖИВУЧЕСТИ РОЯ БПЛА
В самом начале симуляции дроны просто летят к своей цели - выполняют обычное крейсерское движение, держат строй и стараются не врезаться друг в друга. На этом этапе они работают как единая слаженная команда.
Но самое интересное начинается, когда рой натыкается на засаду и внезапно теряет одного из своих. В коде заложена логика, которая позволяет группе мгновенно отреагировать на потерю. Наличие ролей позволяет определить функционал каждого отдельного агента в рое.
Дроны не разбегаются хаотично, а сами оценивают обстановку и делятся на три группы в зависимости от того, кто где находился:
-
Ударная группа. Те, кто оказался ближе всего к врагу, берут огонь на себя и идут на таран, чтобы защитить остальных.
-
Связные. Дроны, летевшие в хвосте, разворачиваются и на максимальной скорости мчат обратно на базу, чтобы передать координаты угрозы.
-
Основная группа. Оставшиеся аппараты быстро вычисляют безопасный маршрут и по дуге обходят опасную зону, чтобы всё-таки добраться до главной цели.
В итоге получается довольно наглядная и реалистичная картина того, как рой может пожертвовать малым, чтобы спасти основную группу и выполнить задачу.
Блок 1: Подключение зависимостей
Импорт базовых библиотек Julia. LinearAlgebra и Statistics используются для векторной математики и расчета дистанций, Random - для симуляции случайностей, а Plots с бэкендом gr() отвечает за высокопроизводительную отрисовку кадров анимации.
using Random
using LinearAlgebra
using Statistics
using Plots
gr()
Блок 2: Структуры данных и состояния
Этот блок — фундамент всей программы. Здесь не описываются сами действия (как лететь или как уклоняться), а задаются «чертежи» для объектов. Проще говоря, здесь закладывается структура того, из чего состоит виртуальный мир и сами дроны.
Содержимое блока делится на несколько логических частей:
-
@enum DroneState(Состояния) Это список того, что дрон делает прямо сейчас. Можно сказать, его текущее занятие. Например:-
CRUISING— спокойно летит по маршруту. -
ALERT— перешел в режим тревоги, ожидает команд. -
RECON/ATTACKING— идет на сближение с угрозой (таран). -
EVADING— выполняет маневр уклонения. -
RETURNING— возвращается в строй после выполнения задачи. -
REPORTING— летит на базу с докладом.
-
-
@enum DroneRole(Роли) В отличие от состояния, роль — это то, кем дрон является в данный момент, его специализация в группе.-
MAIN_GROUP— обычный участник стаи. -
RECON_GROUP— разведчик-камикадзе, который должен нейтрализовать врага. -
EVASIVE_GROUP— участник группы, совершающей обходной маневр. -
COURIER— связной, чья единственная цель — выжить и добраться до базы. Разделение на роли и состояния позволяет сделать логику более гибкой: например, дрон может быть курьером (роль), но при этом находиться в состоянии уклонения от столкновения (состояние).
-
-
mutable struct Drone(Паспорт и телеметрия дрона) Это контейнер информации о каждом конкретном аппарате. Словоmutableозначает, что эти данные могут изменяться прямо на лету, в процессе работы программы. Внутри хранится набор параметров:-
Физика: координаты (
pos) и текущий вектор скорости (vel). -
Идентификация: уникальный номер (
id). -
Память и связь: списки соседей, которых видно прямо сейчас (
neighbors), и точки на карте (где база, где конечная цель, а где обнаружена угроза). -
Статусы: жив ли аппарат (
alive), сколько врагов уничтожил (kills), а также его текущие роль и состояние.
-
-
struct SwarmConfig(Глобальные настройки мира) Это единый пульт управления симуляцией. Вместо того чтобы разбрасывать важные числовые значения по всему коду, они собраны в одной структуре. Здесь задается размер карты (world_size), количество дронов и врагов, скорости (cruise_speed,max_speed), дальность связи радаров (comm_range), безопасная дистанция между самими дронами и проценты распределения аппаратов при возникновении угрозы. Если потребуется провести другой эксперимент (например, сделать дронов более быстрыми, но с меньшим радиусом связи), достаточно изменить настройки только в этой структуре, не затрагивая остальную логику.
@enum DroneState begin
CRUISING
ALERT
RECON
ATTACKING
RETURNING
EVADING
REPORTING
end
@enum DroneRole begin
MAIN_GROUP
RECON_GROUP
EVASIVE_GROUP
COURIER
end
mutable struct Drone
id::Int
pos::Vector{Float64}
vel::Vector{Float64}
state::DroneState
role::DroneRole
neighbors::Vector{Int}
last_contact::Dict{Int, Int}
target_point::Vector{Float64}
base_point::Vector{Float64}
cruise_speed::Float64
max_speed::Float64
command_delay::Int
delay_counter::Int
threat_point::Union{Nothing, Vector{Float64}}
attack_target::Union{Nothing, Vector{Float64}}
alive::Bool
kills::Int
recon_target::Union{Nothing, Vector{Float64}}
evading_direction::Union{Nothing, Vector{Float64}}
end
struct SwarmConfig
world_size::Tuple{Int, Int}
n_drones::Int
n_threats::Int
dt::Float64
total_time::Int
comm_range::Float64
cruise_speed::Float64
max_speed::Float64
command_delay::Int
formation_spacing::Float64
recon_ratio::Float64
evasive_ratio::Float64
courier_count::Int
threat_timeout::Int
safe_distance::Float64
threat_radius::Float64
seed::Int
end
Блок 3: Инициализация параметров
Создание конкретного экземпляра SwarmConfig и вывод стартового экрана в консоль. Это информационное табло, которое логирует начальные условия эксперимента (размер роя, радиусы опасности, дальность связи).
const config = SwarmConfig(
(200, 200), # world_size
12, # n_drones
2, # n_threats
0.5, # dt
600, # total_time
40.0, # comm_range
1.5, # cruise_speed
5.0, # max_speed
10, # command_delay
25.0, # formation_spacing
0.25, # recon_ratio
0.35, # evasive_ratio
1, # courier_count
20, # threat_timeout
10.0, # safe_distance
30.0, # threat_radius
42 # seed
)
println("╔══════════════════════════════════════╗")
println("║ МОДЕЛЬ ЖИВУЧЕСТИ РОЯ ДРОНОВ ║")
println("╚══════════════════════════════════════╝")
println(" Дроны: $(config.n_drones)")
println(" Угрозы: $(config.n_threats)")
println(" Разведгруппа: $(round(config.recon_ratio * 100))%")
println(" Группа обхода: $(round(config.evasive_ratio * 100))%")
println(" Связные: $(config.courier_count)")
println(" Дальность связи: $(config.comm_range)")
println(" Безопасная дистанция: $(config.safe_distance)")
println(" Радиус опасной зоны: $(config.threat_radius)")
println(" Крейсерская скорость: $(config.cruise_speed)")
println(" Максимальная скорость: $(config.max_speed)")
Блок 4: Развертывание формации (initialize_drones)
Функция спавна роя. Дроны не просто появляются в одной точке, а выстраиваются в линию, перпендикулярную вектору движения к цели. Математика здесь рассчитывает смещение каждого аппарата относительно центра, задавая правильный начальный пеленг и крейсерскую скорость.
function initialize_drones(config::SwarmConfig)
drones = Vector{Drone}(undef, config.n_drones)
start_x = 20.0
start_y = 20.0
destination = [config.world_size[1] - 20.0, config.world_size[2] - 20.0]
base_point = [start_x, start_y] # база в точке старта
direction = normalize(destination - [start_x, start_y])
perpendicular = [-direction[2], direction[1]]
for i in 1:config.n_drones
offset = (i - 1 - (config.n_drones - 1) / 2) * config.formation_spacing * 0.5
pos = [
start_x + perpendicular[1] * offset,
start_y + perpendicular[2] * offset
]
vel = direction * config.cruise_speed
# Все начинают как основная группа
role = MAIN_GROUP
drones[i] = Drone(
i,
pos,
vel,
CRUISING,
role,
Int[],
Dict{Int, Int}(),
destination,
base_point,
config.cruise_speed,
config.max_speed,
config.command_delay,
0,
nothing,
nothing,
true,
0,
nothing,
nothing
)
end
return drones
end
Блок 5: Сетевая топология и угрозы
-
create_threats: Генерирует вражеские объекты вокруг заданной точки. -
update_network!: Динамически пересчитывает граф связи на каждом кадре. Дроны «видят» только тех соседей, которые находятся в пределахcomm_range, что критически важно для реалистичной симуляции передачи данных.
mutable struct Threat
id::Int
pos::Vector{Float64}
alive::Bool
end
function create_threats(n::Int, center_pos::Vector{Float64})
threats = Vector{Threat}(undef, n)
for i in 1:n
angle = 2π * (i-1) / n
offset = [cos(angle), sin(angle)] * 10.0
pos = center_pos + offset
threats[i] = Threat(i, pos, true)
end
return threats
end
function update_network!(drones::Vector{Drone}, frame::Int, config::SwarmConfig)
n = length(drones)
for drone in drones
empty!(drone.neighbors)
end
for i in 1:n
for j in i+1:n
if drones[i].alive && drones[j].alive
dist = norm(drones[i].pos - drones[j].pos)
if dist <= config.comm_range
push!(drones[i].neighbors, j)
push!(drones[j].neighbors, i)
drones[i].last_contact[j] = frame
drones[j].last_contact[i] = frame
end
end
end
end
end
Блок 6: Детектирование угроз и тактическое перераспределение
Этот блок является интеллектуальным ядром симуляции. Здесь реализована логика реакции на внештатные ситуации и алгоритм принятия решений по спасению группы. Блок состоит из двух взаимосвязанных функций.
-
Функция
detect_threat!(Триггер кризисной ситуации) В рамках данной симуляции кризис наступает по жесткому таймингу — на 100-м кадре. Программа эмулирует внезапное нападение:-
Случайный дрон из основной группы получает статус уничтоженного.
-
В точке его гибели генерируются вражеские объекты (
Threat). -
Потеря связи с юнитом становится сигналом для всего роя — немедленно вызывается алгоритм перераспределения ролей.
-
-
Функция
assign_roles!(Мозг роя) Это самый сложный тактический алгоритм в модели. Он отвечает за то, чтобы рой не разлетелся хаотично, а принял математически обоснованное решение. Процесс делится на несколько этапов:-
Пространственный анализ: Сначала вычисляется текущий вектор движения всего роя. Затем строится перпендикуляр к этому вектору. С помощью скалярного произведения математика определяет, с какой стороны находится угроза относительно курса (слева, справа или прямо по фронту).
-
Оценка рисков (сортировка): Все выжившие аппараты выстраиваются в список на основе их расстояния до эпицентра угрозы — от самых близких до самых отдаленных.
-
Формирование ударной группы: Указанный в настройках процент дронов (те, что оказались ближе всего к врагу) получает роль
RECON_GROUP(разведка боем). Их задача — отделиться от стаи и пойти на сближение с угрозой. -
Выделение курьера: Аппарат, находящийся дальше всех от опасности, имеет максимальные шансы на выживание. Он получает роль
COURIERи приказ немедленно возвращаться на стартовую точку (базу). -
Вектор уклонения: Оставшиеся дроны формируют основную группу (
MAIN_GROUP) и переходят в режим уклонения (EVADING). Алгоритм использует вычисленную ранее сторону угрозы, чтобы направить этих дронов строго в противоположную сторону (например, если потеря зафиксирована слева, рой синхронно берет правее), выходя из радиуса возможного поражения.
-
Такой подход гарантирует, что система реагирует на угрозы децентрализованно, опираясь исключительно на физическое расположение юнитов в пространстве на момент атаки.
global_threats = Threat[]
function detect_threat!(drones::Vector{Drone}, frame::Int, config::SwarmConfig)
global global_threats
# Симуляция угрозы на кадре 100
if frame == 100 && any(d -> d.alive && d.state == CRUISING, drones)
victim = rand([d for d in drones if d.alive && d.state == CRUISING])
println(" Кадр $frame: Дрон $(victim.id) потерян!")
victim.alive = false
# Координаты угрозы
threat_x = 100.0
threat_y = 100.0
lost_pos = [threat_x, threat_y]
global_threats = create_threats(config.n_threats, lost_pos)
println(" Обнаружено $(config.n_threats) целей в точке ($(threat_x), $(threat_y))")
assign_roles!(drones, lost_pos, frame, config)
return true
end
return false
end
function assign_roles!(drones::Vector{Drone}, threat_pos::Vector{Float64}, frame::Int, config::SwarmConfig)
available = [d for d in drones if d.alive && d.state == CRUISING]
if isempty(available)
return
end
destination = [config.world_size[1] - 20.0, config.world_size[2] - 20.0]
# Направление движения роя
course_dir = normalize(destination - threat_pos)
# Перпендикуляр к курсу (для определения слева/справа)
perp = [-course_dir[2], course_dir[1]] # правый перпендикуляр
# Определяем, с какой стороны угроза относительно курса роя
# Находим центр роя
swarm_center = sum([d.pos for d in available]) / length(available)
to_threat = threat_pos - swarm_center
side = dot(to_threat, perp) # положительное = справа, отрицательное = слева
println(" Угроза относительно курса: ", side > 10 ? "СПРАВА" : side < -10 ? "СЛЕВА" : "ПРЯМО ПО КУРСУ")
# Сортируем по расстоянию до угрозы
sorted_by_distance = sort(available, by = d -> norm(d.pos - threat_pos))
n_total = length(sorted_by_distance)
n_recon = max(1, floor(Int, n_total * config.recon_ratio))
n_couriers = min(config.courier_count, n_total - n_recon)
# Разведчики - самые близкие к угрозе (идут на таран)
recon_drones = sorted_by_distance[1:n_recon]
remaining = sorted_by_distance[n_recon + 1:end]
# Связной - самый дальний от угрозы
courier_drone = nothing
if n_couriers > 0 && length(remaining) > n_couriers
courier_drone = remaining[end]
remaining = remaining[1:end-1]
end
# ОСНОВНАЯ ГРУППА - все оставшиеся уходят в противоположную от угрозы сторону
main_drones = remaining
# Направление обхода для основной группы
if abs(side) > 10
# Угроза слева или справа - уходим в противоположную сторону
evade_dir = side > 0 ? -perp : perp # противоположная сторона
else
# Угроза прямо по курсу - уходим в случайную сторону
evade_dir = rand([-1, 1]) * perp
end
# Точка сбора для основной группы - смещение в безопасную сторону
rally_point = swarm_center + evade_dir * config.threat_radius * 2
println(" Распределение ролей (кадр $frame):")
println(" Разведгруппа: $(length(recon_drones)) дронов (таран)")
println(" Связные: $(courier_drone !== nothing ? 1 : 0) дронов")
println(" Основная группа: $(length(main_drones)) дронов (обход)")
# Назначаем разведчиков
for drone in recon_drones
drone.role = RECON_GROUP
drone.state = ALERT
drone.delay_counter = rand(0:config.command_delay)
drone.threat_point = threat_pos
drone.recon_target = threat_pos
println(" Дрон $(drone.id) → разведгруппа (дист: $(round(norm(drone.pos - threat_pos), digits=1)))")
end
# Назначаем связного
if courier_drone !== nothing
courier_drone.role = COURIER
courier_drone.state = ALERT
courier_drone.delay_counter = rand(0:config.command_delay)
courier_drone.threat_point = threat_pos
println(" Дрон $(courier_drone.id) → связной (на базу)")
end
# Основная группа - ВСЕ уходят в сторону
for drone in main_drones
drone.role = MAIN_GROUP
drone.state = EVADING # СРАЗУ обход, без ALERT
drone.delay_counter = 0
drone.threat_point = threat_pos
drone.evading_direction = evade_dir # запоминаем направление обхода
println(" Дрон $(drone.id) → основная группа (обход)")
end
end
Блок 7: Физический движок (update_position!)
Базовая кинематика. Пересчитывает координаты агентов на основе их векторов скорости. Также включает простую логику границ карты — если дрон вылетает за пределы world_size, он отскакивает от невидимой стены, меняя знак скорости.
function update_position!(agent::Drone, dt::Float64, world_size::Tuple{Int, Int})
if !agent.alive
return
end
agent.pos .+= agent.vel .* dt
for i in 1:2
if agent.pos[i] < 0
agent.pos[i] = 0
agent.vel[i] = abs(agent.vel[i])
elseif agent.pos[i] > world_size[i]
agent.pos[i] = world_size[i]
agent.vel[i] = -abs(agent.vel[i])
end
end
end
Блок 8: Поведенческая логика и конечный автомат (drone_behavior!)
Этот блок — «мозг» каждого отдельного аппарата. Именно здесь структуры данных и тактические роли превращаются в реальные физические действия. Функция вызывается для каждого дрона на каждом кадре симуляции и работает как конечный автомат (State Machine), где поведение жестко зависит от текущего состояния.
Внутри блока можно выделить несколько ключевых механизмов:
-
Локальное избегание столкновений (
avoid_collisions) Это базовая внутренняя функция, которая работает почти во всех режимах полета. Она проверяет дистанцию до соседей. Если другой аппарат оказывается ближе безопасного радиуса, вычисляется вектор отталкивания. Сила отталкивания растет по закону обратных квадратов — чем ближе соседи, тем резче дрон пытается уклониться. Это обеспечивает красивое и плавное поддержание формации без централизованного управления. -
Обработка состояний (State Machine): В зависимости от текущего статуса, дрон выполняет строго определенный набор инструкций:
-
CRUISING(Крейсерский полет): Основной режим. Дрон летит к глобальной цели (destination), постоянно сканируя пространство. Если радар замечает врага ближе определенного радиуса, аппарат может автономно принять решение выйти из строя и пойти на таран (переход вRECON), даже если команда на это не поступала. По прибытии к конечной цели дроны занимают круговую орбиту ожидания (барражируют). -
ALERT(Режим тревоги и задержки связи): Вводит в симуляцию реалистичный фактор — задержку прохождения команд. После получения новой роли дрон не меняет курс мгновенно. Он запускает внутренний таймер (delay_counter). Только когда счетчик обнуляется, аппарат приступает к выполнению задачи (атаке, отступлению или уклонению). -
EVADING(Уклонение): Режим работы основной группы при угрозе. Алгоритм вычисляет комбинированный вектор: 70% тяги направляется на обход опасной зоны (перпендикулярно старом курсу) и 30% — в сторону конечной цели. Как только дрон удаляется на безопасное расстояние от эпицентра, он самостоятельно возвращается в нормальный режимCRUISING. -
RECON/ATTACKING(Атака / Разведка боем): Режим «камикадзе». Аппарат находит ближайшую цель и устремляется к ней на максимальной скорости. В этом режиме отключается избегание столкновений — дрон идет по прямой траектории. При достижении дистанции поражения (< 2.0) происходит взаимное уничтожение: флаги жизни врага и самого дрона переводятся вfalse, а в статистику записывается успешное поражение цели (kills). -
RETURNING(Возвращение в строй): Если аппарат шел на таран, но цель уже была уничтожена другим дроном, он не продолжает лететь в пустоту. Он находит геометрический центр живых союзников и устремляется к ним на максимальной скорости. Как только он догоняет основную группу, он плавно встраивается в строй и возвращается в режимCRUISING. -
REPORTING(Передача донесения): Режим для связных. Дрон прокладывает прямой курс к стартовой точке (базе). Как только дистанция до базы становится минимальной, он глушит двигатели (скорость падает до нуля) — миссия курьера считается успешно выполненной.
-
Этот блок наглядно показывает, как из набора простых локальных правил рождается сложное, адаптивное и высокоэффективное поведение целой группы беспилотников.
function drone_behavior!(drone::Drone, frame::Int, drones::Vector{Drone}, config::SwarmConfig, destination::Vector{Float64})
global global_threats
if !drone.alive
return
end
# Связной на базе
if drone.role == COURIER && drone.state == REPORTING && norm(drone.pos - drone.base_point) <= 5.0
drone.vel = [0.0, 0.0]
return
end
# Общая функция избегания столкновений (используется во всех состояниях)
function avoid_collisions(current_vel, speed_limit)
avoidance = zeros(2)
for other in drones
if other.id != drone.id && other.alive
dist = norm(drone.pos - other.pos)
if dist < config.safe_distance && dist > 0.01
away_dir = normalize(drone.pos - other.pos)
force = (config.safe_distance / max(dist, 0.1))^2 * config.max_speed
avoidance .+= away_dir * force
end
end
end
if norm(avoidance) > 0
if norm(avoidance) > speed_limit * 2
avoidance = normalize(avoidance) * speed_limit * 2
end
return current_vel + avoidance
end
return current_vel
end
if drone.state == CRUISING
# Проверяем: есть ли живые враги поблизости?
alive_threats = [t for t in global_threats if t.alive]
if !isempty(alive_threats) && drone.role == MAIN_GROUP
nearest = alive_threats[argmin([norm(drone.pos - t.pos) for t in alive_threats])]
dist = norm(drone.pos - nearest.pos)
if dist < config.threat_radius * 1.5
println(" Дрон $(drone.id) засёк врага радаром (дист: $(round(dist, digits=1))), иду на таран!")
drone.role = RECON_GROUP
drone.state = RECON
drone.threat_point = nearest.pos
drone.recon_target = nearest.pos
end
end
# Летим к цели
direction_to_target = destination - drone.pos
dist_to_target = norm(direction_to_target)
if dist_to_target > config.safe_distance * 2
target_dir = normalize(direction_to_target)
desired_vel = target_dir * config.cruise_speed
desired_vel = avoid_collisions(desired_vel, config.max_speed)
if norm(desired_vel) > config.max_speed
desired_vel = normalize(desired_vel) * config.max_speed
end
drone.vel = desired_vel
else
angle = 2π * (drone.id - 1) / config.n_drones
target_pos = destination + [cos(angle), sin(angle)] * config.safe_distance * 1.5
direction = target_pos - drone.pos
if norm(direction) > 1.0
drone.vel = normalize(direction) * config.cruise_speed * 0.3
else
drone.vel = [0.0, 0.0]
end
end
elseif drone.state == ALERT
if drone.threat_point === nothing
drone.state = CRUISING
drone.role = MAIN_GROUP
drone.delay_counter = 0
else
drone.delay_counter -= 1
if drone.delay_counter <= 0
if drone.role == RECON_GROUP
drone.state = RECON
println(" Дрон $(drone.id) начинает разведку боем!")
elseif drone.role == COURIER
drone.state = REPORTING
println(" Дрон $(drone.id) летит на базу!")
else
drone.state = CRUISING
drone.role = MAIN_GROUP
end
end
end
elseif drone.state == EVADING
alive_threats = [t for t in global_threats if t.alive]
if !isempty(alive_threats)
nearest = alive_threats[argmin([norm(drone.pos - t.pos) for t in alive_threats])]
dist = norm(drone.pos - nearest.pos)
if dist < config.threat_radius
println(" Дрон $(drone.id) обнаружил врага при обходе, иду на таран!")
drone.role = RECON_GROUP
drone.state = RECON
drone.threat_point = nearest.pos
drone.recon_target = nearest.pos
end
end
if isempty(alive_threats)
println(" Дрон $(drone.id): угроза уничтожена, лечу к цели!")
drone.state = CRUISING
drone.role = MAIN_GROUP
drone.threat_point = nothing
drone.evading_direction = nothing
else
if drone.evading_direction !== nothing
direction_to_dest = normalize(destination - drone.pos)
combined = 0.7 * drone.evading_direction + 0.3 * direction_to_dest
desired_vel = normalize(combined) * config.max_speed
desired_vel = avoid_collisions(desired_vel, config.max_speed)
drone.vel = normalize(desired_vel) * config.max_speed
dist_to_threat = norm(drone.pos - drone.threat_point)
if dist_to_threat > config.threat_radius * 2.5
println(" Дрон $(drone.id): отошёл достаточно, корректирую курс к цели")
drone.state = CRUISING
drone.role = MAIN_GROUP
drone.threat_point = nothing
drone.evading_direction = nothing
end
else
drone.state = CRUISING
drone.role = MAIN_GROUP
end
end
elseif drone.state == RECON
alive_threats = [t for t in global_threats if t.alive]
if isempty(alive_threats)
println(" Дрон $(drone.id): враги кончились, возвращаюсь!")
drone.state = RETURNING
drone.recon_target = nothing
else
nearest = alive_threats[argmin([norm(drone.pos - t.pos) for t in alive_threats])]
drone.recon_target = nearest.pos
direction = drone.recon_target - drone.pos
dist = norm(direction)
if dist > 2.0
# Камикадзе летит прямо на цель, без избегания столкновений!
drone.vel = normalize(direction) * config.max_speed
else
if nearest.alive
nearest.alive = false
drone.alive = false
drone.kills += 1
println(" Дрон $(drone.id) ТАРАНИТ врага $(nearest.id) и самоуничтожается!")
else
println(" Дрон $(drone.id): враг уже уничтожен, возвращаюсь!")
drone.state = RETURNING
drone.recon_target = nothing
end
end
end
elseif drone.state == ATTACKING
alive_threats = [t for t in global_threats if t.alive]
if isempty(alive_threats)
println(" Дрон $(drone.id): враги кончились, возвращаюсь!")
drone.state = RETURNING
drone.attack_target = nothing
else
nearest = alive_threats[argmin([norm(drone.pos - t.pos) for t in alive_threats])]
drone.attack_target = nearest.pos
direction = drone.attack_target - drone.pos
dist = norm(direction)
if dist > 2.0
# Камикадзе летит прямо на цель, без избегания столкновений!
drone.vel = normalize(direction) * config.max_speed
else
if nearest.alive
nearest.alive = false
drone.alive = false
drone.kills += 1
println(" Дрон $(drone.id) ТАРАНИТ врага $(nearest.id) и самоуничтожается!")
else
println(" Дрон $(drone.id): враг уже уничтожен другим дроном!")
drone.state = RETURNING
drone.attack_target = nothing
end
end
end
elseif drone.state == RETURNING
# Ищем группу для возвращения
all_cruising = [d for d in drones if d.alive && d.state in [CRUISING, RETURNING] && d.id != drone.id]
if !isempty(all_cruising)
center = sum([d.pos for d in all_cruising]) / length(all_cruising)
direction = center - drone.pos
dist = norm(direction)
if dist > config.safe_distance * 2
# Летим к группе на максимальной скорости
desired_vel = normalize(direction) * config.max_speed
desired_vel = avoid_collisions(desired_vel, config.max_speed)
drone.vel = normalize(desired_vel) * config.max_speed
if frame % 50 == 0
println(" Дрон $(drone.id) догоняет группу (дистанция: $(round(dist, digits=1)))")
end
else
# Подлетели близко — ищем свободное место и сразу переходим в CRUISING
println(" Дрон $(drone.id) вернулся в строй!")
drone.state = CRUISING
drone.role = MAIN_GROUP
drone.threat_point = nothing
drone.attack_target = nothing
drone.recon_target = nothing
end
else
# Нет группы — летим к цели
direction = destination - drone.pos
if norm(direction) > config.safe_distance
desired_vel = normalize(direction) * config.max_speed
desired_vel = avoid_collisions(desired_vel, config.max_speed)
drone.vel = normalize(desired_vel) * config.max_speed
else
drone.state = CRUISING
drone.role = MAIN_GROUP
end
end
elseif drone.state == REPORTING
direction = drone.base_point - drone.pos
dist = norm(direction)
if dist > 5.0
drone.vel = normalize(direction) * config.max_speed
else
if drone.vel != [0.0, 0.0]
println(" Связной $(drone.id) достиг базы с донесением!")
drone.vel = [0.0, 0.0]
end
end
end
end
Блок 9: Визуализация и интерфейс наблюдения (create_plot)
Этот блок не влияет на логику поведения роя, но он критически важен для анализа результатов. Функция create_plot работает как камера-регистратор: она берет сухие математические данные (координаты, статусы) и на каждом кадре превращает их в наглядную графическую картину. Впоследствии из этих кадров склеивается итоговая GIF-анимация.
Блок решает сразу несколько задач по визуализации:
-
Отрисовка глобальной карты и маршрута В начале функции создается «холст» (координатная плоскость). На ней сразу отмечаются ключевые точки: зеленая точка старта (база), красная точка назначения (цель) и пунктирная линия идеального маршрута между ними. Это дает понимание того, насколько сильно рой отклоняется от курса при обходе опасности.
-
Динамическая сетевая топология (линии связи) Один из самых красивых элементов визуализации. Программа проходит по списку соседей каждого дрона и рисует между ними тонкие полупрозрачные линии. На анимации это выглядит как пульсирующая паутина. Зритель может в реальном времени наблюдать, как перестраивается сеть: при растягивании строя линии рвутся, а при сближении — восстанавливаются.
-
Многоуровневая кодировка агентов Чтобы зритель мог с первого взгляда понять, что делает каждый дрон, используется тройная система обозначений:
-
Цвет: Показывает роль. Синий — основная группа, фиолетовый — идут в обход, красный/оранжевый — камикадзе, голубой — связной.
-
Форма (маркер): Показывает текущее действие. Треугольники указывают направление полета, звездочки — момент атаки, шестиугольник — передачу данных на базе.
-
Текстовые метки: Рядом с каждым аппаратом рендерится его номер и буква роли (например, «О1» — основной дрон №1, «Р3» — разведчик №3). Кроме того, вокруг аппаратов, находящихся в спокойном полете, рисуется полупрозрачный круг, наглядно демонстрирующий радиус работы их систем связи (
comm_range).
-
-
Отображение угрозы и поля боя Вражеские объекты тоже меняют свой вид. Активные угрозы обозначаются яркими красными ромбами, привлекая внимание. Когда дрон-камикадзе успешно таранит цель, ромб превращается в серый крестик — это позволяет оценить итоги столкновения и увидеть «поле боя» после того, как рой полетит дальше.
-
Информационная панель (HUD - Heads-Up Display) В левом нижнем углу кадра функция выводит текстовый блок статистики (используя
annotate!). Эта панель работает как приборная доска:-
Считает количество дронов в каждой функциональной группе.
-
Показывает общие потери роя и количество уничтоженных врагов.
-
Отслеживает статус доставки донесения (меняет цвет на красный, когда связной успешно добирается до базы).
-
Благодаря такому детальному подходу к графике, блок визуализации позволяет не просто увидеть «летающие точки», а проанализировать внутреннюю логику работы роевого интеллекта.
function create_plot(drones::Vector{Drone}, frame::Int, config::SwarmConfig, start_point, end_point, threat_activated::Bool, threat_reported::Bool)
global global_threats
p = plot(
size=(800, 800),
xlim=(0, config.world_size[1]),
ylim=(0, config.world_size[2]),
legend=:outertopright,
title="Модель живучести роя - Кадр: $frame/$(config.total_time)",
xlabel="X",
ylabel="Y"
)
# Маршрут
plot!([start_point[1], end_point[1]], [start_point[2], end_point[2]],
color=:purple, linewidth=2, linestyle=:dash, alpha=0.5, label="Маршрут")
scatter!([start_point[1]], [start_point[2]], color=:green, markersize=10, marker=:circle, label="База", alpha=0.7)
scatter!([end_point[1]], [end_point[2]], color=:red, markersize=10, marker=:circle, label="Цель", alpha=0.7)
# Угрозы
if threat_activated && !isempty(global_threats)
for threat in global_threats
if threat.alive
scatter!([threat.pos[1]], [threat.pos[2]],
color=:red, markersize=12, marker=:diamond,
label="Враг", alpha=0.9)
else
scatter!([threat.pos[1]], [threat.pos[2]],
color=:gray, markersize=8, marker=:xcross,
label="Уничтожен", alpha=0.5)
end
end
end
# Статистика
n_alive = count(d -> d.alive, drones)
n_main = count(d -> d.role == MAIN_GROUP && d.alive, drones)
n_recon = count(d -> d.role == RECON_GROUP && d.alive, drones)
n_evasive = count(d -> d.role == EVASIVE_GROUP && d.alive, drones)
n_couriers = count(d -> d.role == COURIER && d.alive, drones)
total_kills = sum(d -> d.kills, drones)
n_threats_alive = count(t -> t.alive, global_threats)
# Линии связи
for i in 1:length(drones)
if drones[i].alive
for j in drones[i].neighbors
if i < j
plot!([drones[i].pos[1], drones[j].pos[1]],
[drones[i].pos[2], drones[j].pos[2]],
color=:gray, alpha=0.3, linewidth=1, linestyle=:dash, label=nothing)
end
end
end
end
# Дроны
for drone in drones
if !drone.alive
scatter!([drone.pos[1]], [drone.pos[2]],
color=:red, markersize=10, marker=:xcross, label=nothing, alpha=0.7)
else
color = if drone.role == MAIN_GROUP
drone.state == EVADING ? :purple : :blue
elseif drone.role == RECON_GROUP
drone.state == ATTACKING ? :red : :orange
elseif drone.role == EVASIVE_GROUP
:purple
elseif drone.role == COURIER
:cyan
end
marker = if drone.state == RECON
:star5
elseif drone.state == ATTACKING
:star5
elseif drone.state == RETURNING
:ltriangle
elseif drone.state == EVADING
:rtriangle
elseif drone.state == REPORTING
:hexagon
else
:utriangle
end
scatter!([drone.pos[1]], [drone.pos[2]],
color=color, markersize=8, marker=marker, label=nothing, alpha=0.9)
role_label = if drone.role == MAIN_GROUP
"О"
elseif drone.role == RECON_GROUP
"Р"
elseif drone.role == EVASIVE_GROUP
"У"
elseif drone.role == COURIER
"С"
end
annotate!(drone.pos[1] - 8, drone.pos[2] - 8,
text("$(role_label)$(drone.id)", 7, color))
if drone.state == CRUISING
plot!([drone.pos[1] + config.comm_range * cos(θ) for θ in 0:0.2:2π],
[drone.pos[2] + config.comm_range * sin(θ) for θ in 0:0.2:2π],
color=:blue, alpha=0.1, linewidth=1, label=nothing)
end
end
end
# Статус угрозы
threat_status = if !threat_activated
"Нет"
elseif threat_reported
"ОБНАРУЖЕНА (донесение доставлено)"
else
"ОБНАРУЖЕНА (связной в пути...)"
end
threat_color = if !threat_activated
:green
elseif threat_reported
:red
else
:orange
end
# Инфопанель
annotate!(10, config.world_size[2] - 5,
text("Осн: $n_main | Разв: $n_recon | Обх: $n_evasive | Связ: $n_couriers", 10, :black))
annotate!(10, config.world_size[2] - 15,
text("Активных: $n_alive | Уничтожено целей: $total_kills/$(config.n_threats) | Осталось врагов: $n_threats_alive", 10, :black))
annotate!(10, config.world_size[2] - 25,
text("Угроза: $threat_status", 10, threat_color))
return p
end
Блок 10: Главный цикл симуляции (run_simulation)
Этот блок выполняет роль дирижера. Здесь все разрозненные математические функции, физика и логика собираются в единый работающий механизм. Функция запускает таймер и прогоняет рой через все заданные кадры, попутно записывая процесс для итоговой анимации.
Порядок работы внутри главного цикла выглядит так:
-
Этап подготовки: Сначала фиксируется генератор случайных чисел (
Random.seed!). Это делается для того, чтобы результаты симуляции были воспроизводимыми при одних и тех же настройках. Затем создаются сами дроны и задаются координаты старта и финиша. Также здесь инициализируются два важных переключателя (флага): появилась ли на карте угроза и доставлено ли донесение на базу. -
Покадровый просчет (
@animate): Это сердце программы. Цикл проходит по каждому кадру времени от первого до конца симуляции, выполняя на каждом шаге строгую последовательность действий:-
Обновление сети: Первым делом вызывается перерасчет графа связи. Дроны «оглядываются» и обновляют списки видимых союзников.
-
Проверка событий: Алгоритм проверяет триггер угрозы. Если наступило время засады, система генерирует врагов на карте и запускает протокол распределения ролей.
-
Мониторинг курьера: Отдельный блок кода постоянно следит за успехами связного дрона. Если курьер приблизился к базе на дистанцию менее пяти метров, статус
threat_reportedменяется на положительный, фиксируя успешную доставку разведданных. -
Принятие решений: Для каждого дрона вызывается функция поведения. Аппараты анализируют обстановку и решают, куда лететь, кого таранить или как уклоняться в данный момент.
-
Шаг физики: На основе принятых на предыдущем этапе решений обновляются реальные координаты дронов на плоскости.
-
Снимок состояния: Вызывается функция визуализации, которая фиксирует все объекты на графике для текущего кадра.
-
В самом конце макрос собирает каждый второй нарисованный кадр (условие every 2) в единый поток и возвращает готовый объект анимации, который в следующем блоке будет сохранен в виде файла на диске.
function run_simulation()
Random.seed!(config.seed)
drones = initialize_drones(config)
threat_activated = false
threat_reported = false
destination = [config.world_size[1] - 20.0, config.world_size[2] - 20.0]
start_point = [20.0, 20.0]
println("\n Начало симуляции...")
println(" Маршрут: ($(start_point[1]), $(start_point[2])) → ($(destination[1]), $(destination[2]))")
anim = @animate for frame in 1:config.total_time
update_network!(drones, frame, config)
if !threat_activated
threat_activated = detect_threat!(drones, frame, config)
end
# Проверяем, долетел ли связной до базы
if threat_activated && !threat_reported
for drone in drones
if drone.role == COURIER && drone.state == REPORTING && norm(drone.pos - drone.base_point) <= 5.0
threat_reported = true
println(" БАЗА ПОЛУЧИЛА ДОНЕСЕНИЕ ОБ УГРОЗЕ!")
break
end
end
end
for drone in drones
drone_behavior!(drone, frame, drones, config, destination)
end
for drone in drones
update_position!(drone, config.dt, config.world_size)
end
create_plot(drones, frame, config, start_point, destination, threat_activated, threat_reported)
end every 2
return anim
end
Блок 11: Запуск и рендеринг
Вызывает главный цикл и компилирует сохраненные кадры в файл swarm_survivability.gif с частотой 15 кадров в секунду.
println("\n Запуск анимации...")
animation = run_simulation()
println(" Сохранение GIF...")
gif(animation, "swarm_survivability.gif", fps=15) # Сильно замедленно, для наглядности, лучше ставить 60
println(" Готово! Файл сохранён как 'swarm_survivability.gif'")
Заключение
Представленная модель наглядно демонстрирует главное преимущество роевого интеллекта - его невероятную живучесть и гибкость. Если классическая централизованная эскадрилья при потере командира или канала связи может полностью потерять управление, то рой работает по совершенно иным законам.
Написанный алгоритм показывает, что потеря отдельного юнита не приводит к провалу миссии. Наоборот, кризисная ситуация становится триггером для мгновенной реорганизации. Опираясь только на базовые правила геометрии и локальный обмен данными, система успевает сделать сразу три критически важные вещи:
-
Нейтрализовать источник опасности силами группы перехвата.
-
Сохранить основную массу аппаратов, своевременно уведя их из зоны поражения.
-
Гарантированно предупредить базу о засаде с помощью выделенного связного.
В итоге получилась не просто красивая анимация движущихся точек, а полноценный цифровой полигон. Своего рода базой для проделанной работы послужила модель роя дронов из документации Engee. Взятая оттуда механика группового полета была интегрирована в проект и существенно расширена тактической логикой, распределением ролей и автономной реакцией на угрозы.
Текущий код представляет собой отличный фундамент для дальнейших исследований. Базовую логику конечных автоматов можно легко масштабировать: например, перевести физику в полноценное 3D, добавить учет расхода батареи, усложнить аэродинамику или внедрить алгоритмы машинного обучения, чтобы система самостоятельно искала идеальные тактики выживания.