For small lists of enemies, linearly scanning all of them and computing the distance to the character is sufficient. However, if you have many enemies, a more efficient data structure is needed.
If your list of enemies does not change (or changes less often than you need to find the closest enemy), I would use scipy.spatial.cKDTree
. kd-trees take \$\mathcal{O}(n\log n)\$ time to build, but afterwards each query only takes \$\mathcal{O}(\log n)\$.
from scipy.spatial import cKDTree as KDTree
def find_closest_kdtree(character: tuple, enemies: KDTree) -> (int, int):
"""
Finds the closest enemy in enemies
:param character: An (x, y) representing the position of the character\n
:param enemies: A KDTree that represent enemies
:return: A tuple (x, y) of the closest enemy
"""
_, i = enemies.query([character], 1)
return i[0]
if __name__ == "__main__":
# Test Case #
character = (5, 6)
enemies = [(1, 2), (3, 4), (7, 6), (11, 4)]
enemies_tree = KDTree(enemies)
closest = enemies[find_closest_kdtree(character, enemies_tree)]
print(closest)
If you do regularly need to update the list of enemies (because they are spawned, get killed, move off-screen, etc), you might be able to use a R* tree instead:
from rtree.index import Rtree
def find_closest_rtree(character: tuple, enemies) -> (int, int):
"""
Finds the closest enemy in enemies
:param character: An (x, y) representing the position of the character\n
:param enemies: A KDTree that represent enemies
:return: A tuple (x, y) of the closest enemy
"""
return next(enemies.nearest(character, 1, objects='raw'))
if __name__ == "__main__":
# Test Case #
character = (5, 6)
enemies = [(1, 2), (3, 4), (7, 6), (11, 4)]
enemies_tree = Rtree()
for i, p in enumerate(enemies):
enemies_tree.insert(i, p+p, p)
closest = find_closest_rtree(character, enemies_tree)
print(closest)
Here is how the different methods compare performance wise, including the implementation using min
and your original implementation:

Note that building all the R* trees for this took multiple minutes, while building all the KDTrees took only a couple of seconds. So you would probably have to rebuild the KDTree quite often for it to be worth it to switch to the R* tree.
In case you are interested, this is how I generated that graph:
import numpy as np
import pandas as pd
from functools import partial
import timeit
from scipy.spatial import cKDTree as KDTree
from rtree.index import Rtree
def get_time(func, *x):
timer = timeit.Timer(partial(func, *x))
t = timer.repeat(repeat=5, number=1)
return np.min(t), np.std(t) / np.sqrt(len(t))
def get_times(func, inputs):
return np.array(list(map(partial(get_time, func), inputs))
def find_closest(character: tuple, enemies: list) -> (int, int):
closest_enemy = None
smallest_distance = 100_000_000 # Set to large number to ensure values can be less #
for enemy in enemies:
distance = math.sqrt(math.pow(character[0] - enemy[0], 2) + (math.pow(character[1] - enemy[1], 2)))
if distance < smallest_distance:
closest_enemy = (enemy[0], enemy[1])
smallest_distance = distance
return closest_enemy
def find_closest_min(character: tuple, enemies: list) -> (int, int):
def find_enemy_distance(enemy):
return math.hypot((character[0] - enemy[0]), (character[1] - enemy[1]))
return min(enemies, key=find_enemy_distance)
def find_closest_kdtree(character: tuple, enemies: KDTree) -> (int, int):
_, i = enemies.query([character], 1)
return i[0]
def find_closest_rtree(character: tuple, enemies) -> (int, int):
return next(enemies.nearest(character, 1, objects='raw'))
def find_closest_kdtree_with_build(character: tuple, enemies) -> (int, int):
enemies_tree = KDTree(enemies)
_, i = enemies_tree.query([character], 1)
return enemies[i[0]]
if __name__ == "__main__":
character = 5, 6
x = [list(map(tuple, np.random.randint(-n, n, (n, 2))))
for n in np.logspace(1, 6, dtype=int)]
kdtrees = [KDTree(v) for v in x]
rtrees = []
for y in x:
rtrees.append(Rtree())
for i, v in enumerate(y):
rtrees[-1].insert(i, v+v, v)
df = pd.DataFrame(list(map(len, x)), columns=["x"])
df["find_closest"], df["find_closest_err"] = get_times(partial(find_closest, character), x).T
df["find_closest_min"], df["find_closest_min_err"] = get_times(partial(find_closest_min, character), x).T
df["find_closest_kdtree"], df["find_closest_kdtree_err"] = get_times(partial(find_closest_kdtree, character), kdtrees).T
df["find_closest_rtree"], df["find_closest_rtree_err"] = get_times(partial(find_closest_rtree, character), rtrees).T
df["find_closest_kdtree_with_build"], df["find_closest_kdtree_with_build_err"] = get_times(partial(find_closest_kdtree_with_build, character), x).T
for label in df.columns[1::2]:
plt.errorbar(df["x"], df[label], yerr=df[label + "_err"], fmt='o-', label=label)
plt.xlabel("Number of enemies")
plt.ylabel("Time [s]")
plt.xscale("log")
plt.yscale("log")
plt.legend()
plt.show()
return "code"
;P \$\endgroup\$