Тело как объект состояния и попадание в цель
Прямая задача — куда упадёт снаряд; обратная — с какой скоростью бросить, чтобы попасть. Обе решает симуляция.
Обратная задача баллистики — нахождение параметров запуска (скорости, угла) по требуемой точке попадания.
Тело как структура данных
До сих пор мы держали $x, y, v_x, v_y$ отдельными переменными. В настоящем движке тело — это структура (словарь, объект или кортеж) с этими полями, а шаг симуляции — функция, принимающая тело и возвращающая обновлённое. Такой подход масштабируется на тысячи объектов: список тел, и в цикле каждое продвигается на шаг. Заодно код становится читаемее: «состояние» и «правило перехода» чётко разделены.
Прямая задача в виде функции
Соберём симуляцию полёта в функцию, которая по начальной скорости и углу возвращает дальность. Тело храним в словаре.
import math
def simulate(v0, ang_deg, dt=0.001):
ang = math.radians(ang_deg)
body = {"x": 0.0, "y": 0.0,
"vx": v0*math.cos(ang), "vy": v0*math.sin(ang)}
g = 9.8
while True:
body["vy"] -= g*dt
xn = body["x"] + body["vx"]*dt
yn = body["y"] + body["vy"]*dt
if yn < 0:
frac = body["y"] / (body["y"] - yn)
return body["x"] + frac*(xn - body["x"])
body["x"], body["y"] = xn, yn
print(f"Дальность при v0=22, 45°: {simulate(22, 45):.2f} м")
Вывод:
Дальность при v0=22, 45°: 49.37 м
Обратная задача: попасть в цель
Теперь зададимся вопросом: какую скорость нужно сообщить снаряду, чтобы при угле $45°$ попасть в цель на расстоянии $50$ м? Для случая без сопротивления есть аналитическое решение из формулы дальности: $v_0 = \sqrt{\frac{R\,g}{\sin 2\alpha}}$. Найдём её и сразу проверим прямой симуляцией.
import math
def simulate(v0, ang_deg, dt=0.001):
ang = math.radians(ang_deg)
x = y = 0.0
vx = v0*math.cos(ang); vy = v0*math.sin(ang)
g = 9.8
while True:
vy -= g*dt
xn = x + vx*dt; yn = y + vy*dt
if yn < 0:
frac = y/(y - yn)
return x + frac*(xn - x)
x, y = xn, yn
target, ang, g = 50.0, 45, 9.8
v0 = math.sqrt(target*g / math.sin(2*math.radians(ang)))
print(f"Цель на {target} м, угол {ang}°")
print(f"Нужная скорость v0 = {v0:.3f} м/с")
print(f"Симуляция упала на {simulate(v0, ang):.2f} м")
Вывод:
Цель на 50.0 м, угол 45° Нужная скорость v0 = 22.136 м/с Симуляция упала на 49.98 м
Аналитика и симуляция совпали: подобранная скорость $22.136$ м/с приводит снаряд почти ровно в цель ($49.98$ м, расхождение — артефакт дискретного шага). Так устроено прицеливание в играх с честной баллистикой и системы наведения.
Как работает под капотом
Если добавить сопротивление, аналитической формулы для $v_0$ уже нет, и обратную задачу решают численным поиском: симулируют при нескольких скоростях и ищут ту, при которой дальность равна целевой. Поскольку дальность монотонно растёт со скоростью, отлично работает двоичный поиск (бисекция): берём нижнюю и верхнюю границы скорости и сужаем интервал вдвое, пока не попадём в цель с нужной точностью. Это тот же приём, что для решения уравнений: симуляция играет роль функции, корень которой мы ищем. По такому принципу ИИ-противник в шутере подбирает упреждение и силу броска.
Частые ошибки
- Решать обратную задачу формулой при сопротивлении. С трением нужен численный поиск (бисекция), формула $v_0=\sqrt{Rg/\sin2\alpha}$ не работает.
- Менять состояние тела частично. Если обновить $v_y$, но забыть применить его к $y$ в том же шаге, симуляция рассинхронизируется.
- Слишком грубый шаг при поиске. При обратной задаче ошибка дискретизации складывается с ошибкой поиска — берите $\Delta t$ поменьше.
Итог
- Тело удобно представлять структурой состояния, а шаг — функцией над ней.
- Прямая задача: по запуску найти точку падения (симуляция).
- Обратная задача без трения решается формулой $v_0=\sqrt{Rg/\sin2\alpha}$.
- С трением обратную задачу решает численный поиск (бисекция по скорости).