Not drones but Suzannes, only 8 of them, only moving in 2D, very unimaginative formation, but the key principles are here. Run the script, move mouse over 3D viewport, press Space.

import bpy, copy
from bpy import context as C, data as D
from mathutils import Vector
sim_start_frame = 1 # when to start the simulation
sim_end_frame = 2000 # save CPU cycles if you go get a coffee after starting the sim
substeps = 5 # it's common in simulations to allow more than one step per frame
seconds_per_frame = 1 / C.scene.render.fps
drones = [d for d in D.collections['drones'].objects]
mem = {d.name: {"a": Vector(), "v": Vector()} for d in drones}
frame_data = {"mem": mem, "loc": {d.name: d.location.copy() for d in drones}}
cache = {sim_start_frame-1: frame_data}
def brain(dt, memory, gps):
"""
This is drone logic. Notice how I didn't properly encapsulate it, and so
for example you can set memory["v"] to a huge number. Normally, you want
to only be able to send signals to engines here, and have the outside
of this function take care of simulating engines accelerating etc.
"""
gps[0].z, gps[1].z, gps[2].z = 0, 0, 0 # 2D
ideal_dist_from_center = 3
goal1 = gps[0].normalized() * (ideal_dist_from_center - gps[0].length)
to_d1 = gps[0] - gps[1]
dist_d1 = to_d1.length
goal2 = to_d1.normalized()*2.5 / dist_d1
to_d2 = gps[0] - gps[1]
dist_d2 = to_d2.length
goal3 = to_d2.normalized()*2.5 / dist_d2
jerk = (goal1 + goal2 + goal3) / 3000
new_a = memory["a"]*.9 + jerk
max_a = new_a.normalized() / 1000
memory["a"] = new_a if new_a < max_a else max_a
memory["v"] *= .95 # damping, air resistance etc.
memory["v"] += memory["a"]
def frame_change_pre(scene, depsgraph=None):
frame = C.scene.frame_current
if frame in cache:
# restore positions from cache
for name, loc in cache[frame]["loc"].items():
D.objects[name].location = loc.copy()
return
if frame < sim_start_frame or frame > sim_end_frame:
return
if frame-1 not in cache:
return # don't compute if a frame was skipped
frame_data = copy.deepcopy(cache[frame-1])
cache[frame] = frame_data
dt = seconds_per_frame / substeps
for _ in range(substeps):
for d in drones:
distance = lambda x: (x-d.location).length
# get this and 2 nearest drones locations:
gps = sorted([l for l in frame_data["loc"].values()], key=distance)[:3]
brain(dt, mem[d.name], gps)
d.location += mem[d.name]["v"]
frame_data["loc"][d.name] = d.location.copy()
listeners = bpy.app.handlers.frame_change_post
listeners.clear() # remove old handlers before adding updated versions
listeners.append(frame_change_pre)
C.scene.frame_current = sim_start_frame