Недавно посмотрел фильм про Соника и вспомнил, какой революционной игрой он казался когда-то: бег по кольцевой тропинке, прыжки и отскоки. И появился вопрос, а как в игре можно было бы реализовать углы отражения?
Уверен, что в самой игре были зафиксированы правила отражения (помните, там были такие круглые штуки, которые откидывали его в сторону под углом), однако мне стало интересно решить эту задачу в более общем виде: избыточное для 16 битной платформы, но для более современных - вполне рабочее для современного железа.
Общие сведения и теория
Сама идея отражения объекта от препятствия довольно старая. Взять, например, Арканоид: мяч отражается от поверхностей, но если присмотреться, набор возможных углов падения и отражения - фиксирован (вероятно, возможные углы падения и отражения - предопределены заранее).
Иными словами, идея отражения какого-то объекта - не нова. Но мы попробуем продвинуться чуть-чуть далее. Давайте сделаем некоторую универсальную модель отражения и воспользуемся правилом: угол падения равен углу отражения. Это, разумеется, не то, чтобы идеальное решение, однако его, если что, можно и изменить.
Математика вопроса
Падение и отражение, по сути, сводится к пересечению двух прямых. Неважно, какая траектория у отражаемого объекта - её можно свести к набору отрезков. Неважно, какая форма у отражающей поверхности - её, также, можно свести к набору отрезков.
Формула прямой - y = kx + b, где k - коэффициент наклона прямой, b - смещение. У нас, напомню, два отрезка - две прямые, ограниченные с двух сторон точками, координаты которых нам известны. Это позволяет найти коэффициенты (расчёты тут).
Найти точку пересечения двух отрезков - задача уже решённая (вот она), поэтому я не буду углубляться.
Следующий шаг - это найти угол падения. В этом нам поможет теорема косинусов. Фактически, мы просто получаем два треугольника с известными длинами сторон и надо просто выяснить падения.
И под конец - вычисления угла отражения. Тут я решил пойти по пути наименьшего сопротивления: падающий луч, чтобы отразиться, должен "заступить" за линию зоны отражения; поэтому мы просто повернём "залезший внутрь" отрезок. Угол поворота будет равен двум углам падения против часовой стрелки.
Это, полагаю, довольно сложно прочитать и представить, поэтому прилагаю рисунок, как это должно выглядеть.
Зелёным - выделены углы падения, оранжевым - угол отражения и равен двум углам падения. Тут, разумеется, можно поуправлять углом отражения, но пока остановимся на классике.
Проблема подхода и её преодоление
На самом деле в этой бочке мёда математического удовлетворения есть один нюанс, который всё ломает - это вертикальные и горизонтальные линии. Проблема в том, что формула y=kx+b для вертикальных и горизонтальных отрезков не работает, потому что для вертикальных линий k всегда равно 0; с горизонтальными всё ещё хуже. Это нетрудно определить: если x-координаты отрезка одинаковы - значит у нас вертикальная линия; если y-координаты одинаковы - значит горизонтальная.
Короче говоря, наши формулы прекращают работать. Поэтому мы пойдём на небольшую хитрость - намеренно введём небольшую ошибку: у второй координаты отрезка сместим "одинаковую" величину координат точки на какое-нибудь небольшое значение, скажем, 0.0000001. С точки зрения конечного результата - всё будет приемлемо, с точки зрения математики - формулы всегда будут работать, с точки зрения конечного результата - незначительное неверное отклонение создаст ошибку только на очень большом расстоянии, что для нашей задачи - несущественно.
Решение
Пришло время написать немного кода. Изначально я всё сделал на Rust, потому что мне хотелось опробовать подход с неизменными (иммутабельными) структурами, которые возвращают каждый раз новые структуры. Но когда дело дошло до визуализации, я вдруг понял, что масштаб задачи не сопоставим с хлопотами на сборку. Поэтому решил просто переписать всё на JS.
Однако, результат получился не совсем : неверно выглядят как угол отражения, так и длина отражённого отрезка.
Оранжевый - луч падения, серый - отражающая поверхность, фиолетовый - луч отражения. Причина такого результата почти всегда предопределена: где-то мы напутали в вычислении углов. Отладчик нам в помощь!
А проблема была не в вычислениях, а в тестовых данных; поправили данные - всё стало выглядеть хорошо.
Расширенное решение
Давайте немного усложним задачу, а именно сделаем отражение более интерактивным. Для начала перестанем отображать прохождение линии через преграду.
Я немного изменил исходный код добавил небольшой обработчик мышки.
Исходник тестовых данных немного усложнился.
Результат получился приемлемым.
Заключение
Как видите, задача несложная; можно много где использовать. Например, написать игру вроде Арканоида, но на стероидах.
Вас, наверное, удивляет, причём тут деревья, камни и прочее. Признаться, я и сам озадачен. Попросил Алису от Яндекса сгенерировать мне изображение с углами падения и отражения, но получил это. Если у вас получилось разгадать этот ребус от нейросетки, не откажите в любезности, поделитесь открытием.