Все заметки

Ревизия физической системы

Работая над движком, я предпринимал попытки доработать физику чуть ли не каждый год, но как только погружался в эти дебри, понимал, что времени только на осознание необходимой математики и алгоритмов уйдет немало, не говоря уже о том, сколько кода нужно написать.

Конечно, меня посещали мысли перестать страдать и взять стороннюю библиотеку, но и это потребует немалых усилий. Нужно научить библиотеку работать с жизненным циклом и моделью сцены движка, прокинуть все нужные апишки через компоненты, события, формочки и менюшки в редакторе. Первый прототип может и получится собрать за выходные, но на итоговое решение могут уйти месяцы. Поэтому я обычно ограничивался минимальными доработками, чтобы решить текущую задачу и переключался на другие проблемы.

Что нужно сделать

Задач очень много. Пару лет назад я бы побоялся за такое браться, но сегодня, вооружившись ИИ, у меня появилась уверенность, что по крайней мере большую часть из этого сделать мне вполне по силам. Список определил следующий:

  • Улучшение стабильности разрешения столкновений

    Сейчас, если в столкновении участвуют более двух тел, то они начинают заметно дрожать.

  • Новые типы коллайдеров

    Прямоугольника и круга обычно хватало, но для персонажей лучше используют капсулу, да и для геометрии уровня в некоторых случаях лучше подойдет набор линий.

  • Raycasting

    Для реализации геймплейных механик очень полезно иметь возможность кинуть луч и получить список акторов, которые с ним столкнулись.

  • Фильтрация коллизий

    Бывают случаи, когда хочется проигнорировать столкновение между определенными группами объектов.

  • Визуальное отображение коллайдеров в редакторе

    Спрайт зачастую не совпадает по габаритам с коллайдером, плюс иногда его нужно сместить в сторону и это приходится делать вслепую, меняя циферки в инспекторе. Хочется иметь возможность визуально оценить, как коллайдер будет выглядеть.

  • One-way платформы

    Это такой коллайдер, который пропускает сквозь себя другие объекты, но только в определенном направлении. Очень нужная вещь в платформерах, чтобы не биться головой о блоки, когда хочется запрыгнуть на уровень выше.

  • Kinematic тип твердого тела

    Сейчас компонент RigidBody поддерживает только два варианта тела: static и dynamic. Первый для статического окружения, а второй для тел, которые могут перемещаться и к которым можно применять силы и импульсы. Проблема второго типа в том, что он не подходит игровому персонажу. Нужен третий тип тел, которые учитываются в физической симуляции, но на них не действуют силы, поэтому перемещение осуществляется только через скрипты геймплея.

  • Передача импульса при столкновении и вращение

    Без этого вообще сложно сказать, что в движке есть хоть какая-то физическая симуляция.

  • Интерполяция

    Физика обновляется с фиксированной частотой обновления, а рендеринг нет. При этом количество кадров в секунду может быть сильно выше. Как итог — объекты, которые двигаются с помощью физики, перемещаются рывками. Один из вариантов решения — задерживать обновление физики на одну итерацию, а для всех промежуточных кадров отрисовки использовать интерполированные координаты объектов между прошлым состоянием физики и текущим.

Подготовительный этап

Первое, что я сделал, это отрефакторил api взаимодействия с физикой снаружи. Вместо использования событий для передачи силы и импульса объекту, я добавил методы в RigidBody компонент. Как будто бы это императивное действие, а не сигнал о том, что что-то произошло, поэтому методы класса смотрятся лаконичнее. Да и благодаря этому получилось избавиться от кучи объектов в системе, где я маппил айди актора на текущие векторы скорости, ускорения, равнодействующей сил и так далее. Теперь за хранение состояния объекта в симуляции целиком отвечает RigidBody компонент.

// было
actor.dispatchEvent(AddImpulse, { value: new Vector(10, 10) });

// стало
const rigidBody = actor.getComponent(RigidBody);
rigidBody.applyImpulse(new Vector(10, 10));

Задача тривиальная, но это первый раз, когда я попробовал Codex в своем движке.

Работаем над стабильностью столкновений

Текущая логика разрешения столкновений никуда не годится:

  • Cтопки объектов бешено дрожат, потому что за один присест система не успевает разрешить сразу все столкновения
  • Столкновение с преградой во время прыжка не сбрасывает набранную скорость, поэтому можно заметить прилипание персонажа к потолку, пока скорость не затухнет под действием силы тяжести.
  • Перемещение по неровной поверхности приводит к безумным мотаниям объекта из стороны в сторону

Теперь корректировка позиций при столкновении чуть умнее: учитывается масса тел и игнорируются совсем уж незначительные пересечения, чтобы снизить количество подергиваний. Также, в зависимости от направления движения объектов, корректируется вектор скорости каждого участника столкновения. Этот алгоритм выполняется в цикле несколько раз для случаев, когда в столкновении участвуют более двух тел. За счет этого обновление скорости происходит равномернее и это тоже позволяет снизить дрожание объектов.

Новые типы коллайдеров

Добавил два новых типа: capsule и segment. Основная сложность была в количестве возможных сочетаний, так как для каждой пары, будь то капсула против прямоугольника или круг против сегмента, подходы отличаются и алгоритмы используются разные. Сначала я генерировал черновое решение через ИИ, а затем рефакторил до тех пор, пока не понимал, что происходит, и убирал все лишнее. Codex регулярно добавлял слишком много временных объектов или массивов, забывая про то, что этот код потенциально будет запускаться для сотен, а то и тысяч объектов в цикле.

Raycasting

Тут я пошел по тому же принципу, что и с новыми коллайдерами: сначала сгенерировал черновик с ИИ, а потом пару дней изучал код и рефакторил, меняя все, что не нравится.

Теперь физическая система предоставляет движку api, через которое можно бросить луч и в результате получить либо первый попавшийся объект, либо все в рамках указанной дистанции:

const api = this.world.systemApi.get(PhysicsAPI);

// Есть также метод raycastAll, который вернет список всех попаданий
const hit = api.raycast({
  origin: {
    x: 0,
    y: 10
  },
  direction: new Vector2(20, 30),
  maxDistance: 100
});

if (hit) {
  // В информации о попадании содержится ссылка на актор,
  // дальность от исходной точки луча,
  // координаты попадания и вектор нормали столкновения
  console.log(hit.actor, hit.distance, hit.point, hit.normal);
}

Через это же api я добавил запросы на пересечение с указанной областью в виде окружности или прямоугольника, что тоже бывает полезным в реализации геймплейных механик. К примеру, для мгновенного поиска ближайших противников от цели.

const api = this.world.systemApi.get(PhysicsAPI);

// Получаем список акторов, коллайдеры которых пересекаются с указанной областью
const actors = api.overlapBox({
  center: { x: 100, y: 200 },
  size: { x: 25, y: 25 }
});

Подобных инструментов давно не хватало и раньше приходилось выкручиваться добавлением дочерних акторов с коллайдерами, с помощью которых я в скриптах подписывался на все коллизии и сохранял интересующие, чтобы в нужный момент использовать эту информацию по назначению. В Tower Defence игре каждая башня имела такую область отслеживания для выбора цели при стрельбе. Теперь это можно сделать гораздо проще одним вызовом.

Фильтрация коллизий

Иногда нужно, чтобы коллизии между определенными объектами игнорировались. В самом начале своего пути, для решения этой проблемы, я добавил в RigidBody компонент флажки isGhost и isPermeable. Спустя пару месяцев после добавления я забыл, чем они отличаются друг от друга, и так и не смог это вспомнить и по сей день. В коде была запутанная портянка условий, благодаря которым объект мог пролетать через препятствия, но к нему все еще можно было прикладывать силы и импульсы.

Нормальное решение в данном случае — реализация слоев и битовой маски или матрицы, где можно определить, какие слои с какими могут сталкиваться. Реализуется примерно так же просто, как и звучит. Повозиться разве что пришлось только в редакторе, где нужно было добавить новую форму в инспектор.

Отображение коллайдеров в редакторе

С ростом количества коллайдеров становится еще сложнее держать в голове, какие физические габариты имеет тот или иной объект. Решение достаточно очевидное и давно напрашивалось – добавить опциональное отображение отладочной информации в редакторе.

В результате, я написал систему, позволяющую включать и отключать debug слои поверх сцены. Для каждого слоя требуется определить обработчик, который, получает на вход актор и возвращает Shape компонент с фигурой нужной формы. Далее создается технический актор с полученным Shape компонентом и добавляется к актору сцены как дочерний, чтобы изменения координат, угла поворота или масштабирования родителя применялись автоматически.

Что дальше

Из запланированных задач я сделал примерно половину, поэтому работа продолжается. ИИ действительно помогает справиться с затыками, ускорить и упростить разработку движка.

Раньше я мог потратить две недели на поиск и изучение информации, выгореть и отложить решение задачи в долгий ящик.

Сейчас же, через ChatGPT я узнаю, как решается проблема в существующих движках, валидирую результат, глядя в реальную документацию, а затем составляю план действий и набрасываю черновик с помощью Codex. Сложно сказать, стал ли я писать код быстрее, но когнитивная нагрузка заметно снизилась, а для пет-проекта это очень важно, так как после основной работы далеко не всегда есть силы на то, чтобы активно думать.