Векторы на кортежах: сложение, длина, нормировка

Весь движок построен на векторах, а вектор в 2D — это всего лишь пара чисел и горстка функций над ней.

Вектор — величина, имеющая длину и направление; в 2D он задаётся парой компонент $(x, y)$.

Почему векторы — язык физики

Скорость, ускорение, сила, импульс — всё это векторы: у них есть и величина, и направление. Записывать каждую как две отдельные переменные ($v_x$, $v_y$) утомительно и чревато ошибками. Гораздо чище завести вектор как сущность и операции над ним. В Python для этого даже не нужны библиотеки: вектор — это кортеж (x, y), а операции — обычные функции. Такой подход прозрачен, не тянет зависимостей и идеально подходит для учебных движков.

Базовые операции

Определим сложение, вычитание, умножение на число, длину и нормировку. Длина вектора — это теорема Пифагора:

$$|\vec a| = \sqrt{a_x^2 + a_y^2}.$$

Нормировка превращает вектор в единичный (длины $1$), сохраняя направление: $\hat a = \frac{\vec a}{|\vec a|}$. Единичные векторы нужны повсюду — чтобы задать «куда», отделив направление от величины.

import math
def add(a, b):   return (a[0]+b[0], a[1]+b[1])
def sub(a, b):   return (a[0]-b[0], a[1]-b[1])
def mul(a, s):   return (a[0]*s, a[1]*s)
def length(a):   return math.hypot(a[0], a[1])
def normalize(a):
    L = length(a)
    return (a[0]/L, a[1]/L)

a = (3, 4)
b = (1, 2)
print("a + b      =", add(a, b))
print("a - b      =", sub(a, b))
print("a * 2      =", mul(a, 2))
print("|a|        =", length(a))
print("normalize a=", normalize(a))
print("|norm a|   =", round(length(normalize(a)), 6))

Вывод:

a + b      = (4, 6)
a - b      = (2, 2)
a * 2      = (6, 8)
|a|        = 5.0
normalize a= (0.6, 0.8)
|norm a|   = 1.0

Вектор $(3, 4)$ имеет длину ровно $5$ — это знаменитый египетский треугольник. После нормировки получаем $(0.6, 0.8)$, и его длина равна $1$, как и положено единичному вектору. Эти пять функций — фундамент: на них мы построим скорость, силы и столкновения.

Вектор скорости и движение

С векторными операциями шаг симуляции записывается компактно: pos = add(pos, mul(vel, dt)), vel = add(vel, mul(acc, dt)). Никаких отдельных $x$ и $y$ — одна строка обновляет обе оси. Это не только короче, но и надёжнее: невозможно случайно обновить одну ось и забыть другую.

Как работает под капотом

Функция math.hypot(x, y) вычисляет $\sqrt{x^2 + y^2}$ устойчиво — без промежуточного переполнения для очень больших чисел, в отличие от наивного math.sqrt(x*x + y*y). Нормировка опасна одним: если длина вектора равна нулю (нулевой вектор не имеет направления), деление на неё даст ошибку. В движках это частый источник падений — например, когда две точки совпали и вектор между ними нулевой. Поэтому в боевом коде нормировку всегда защищают проверкой на нулевую длину.

Частые ошибки

  • Нормировка нулевого вектора. Деление на нулевую длину — деление на ноль; защищайте проверкой.
  • Путать длину и квадрат длины. Для сравнения расстояний часто хватает $|\vec a|^2$ без корня — это быстрее и точнее.
  • Менять кортеж «на месте». Кортежи неизменяемы; операции возвращают новый кортеж, а не правят старый.

Итог

  • Вектор в 2D — пара чисел; операции реализуются обычными функциями без библиотек.
  • Длина: $|\vec a|=\sqrt{a_x^2+a_y^2}$; нормировка даёт единичный вектор направления.
  • Векторная запись шага короче и надёжнее покомпонентной.
  • Нормировку нулевого вектора надо защищать — частая причина падений.
Проверьте себя
1. Чему равна длина вектора (3, 4)?
A7
B5
C12
D25
2. Что делает нормировка вектора?
AДелает его нулевым
BПревращает в единичный вектор той же длины 1, сохраняя направление
CУдваивает длину
DМеняет направление на противоположное
3. Почему нормировку нулевого вектора нужно защищать проверкой?
AОна медленная
BДлина нулевого вектора равна нулю, и происходит деление на ноль
CКортежи неизменяемы
DИз-за переполнения