Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Масса вагонов и ускорение.

Бартоломей Вашак (Bartlomiej Waszak) из Ubisoft написал на сайте Gamasutra статью о том, как работает физическая симуляция поездов в Assassin’s Creed Syndicate. Оказывается, система учитывает множество параметров: от массы вагонов и их ускорения до силы трения. Мы выбрали из материала главное.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Вступление

Вашак отмечает, что сейчас в индустрии не принято писать собственные физические движки, однако в некоторых ситуациях это бывает полезным. Именно в такую ситуацию попали авторы Assassin’s Creed Syndicate, разрабатывая систему железнодорожного сообщения для Лондона XIX века.

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

У решения разработчиков было несколько преимуществ.

Изогнутой железнодорожной колеёй легче управлять в одномерном пространстве. Использование промежуточного программного обеспечения для обсчёта физики в 3D может быть рискованным и привести к ошибкам вроде летающих вагонов. Тем не менее разработчики всё равно хотели, чтобы коллизии вагонов обсчитывались в трёхмерном пространстве.

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

Система также поддерживает разъединение вагонов и их столкновения.

Контроль локомотива

Интерфейс контроля локомотива очень прост. Он содержит в себе запрос на желаемую скорость.

Locomotive::SetDesiredSpeed(float DesiredSpeed, float TimeToReachDesiredSpeed)

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

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Здесь F — это вычисляемая сила, m — масса локомотива, Vdiff — это желаемая скорость минус текущая скорость, а t — время, требуемое для того, чтобы достичь желаемой скорости.

Как только F найдена, её значение передаётся физическому состоянию вагона (WagonPhysicsState) как «сила двигателя» для того, чтобы вести локомотив.

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

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

Базовая симуляция

Структура, которая содержит физическую информацию о каждом вагоне и локомотиве, выглядит следующим образом.

struct WagonPhysicsState
{
// Значения меняются в ходе интеграции:
// расстояние пути и импульс.
RailwayTrack m_Track;
float m_LinearMomentum;

// Скорость, рассчитанная от импулься.
float m_LinearSpeed;

// Текущее значение сил.
float m_EngineForce;
float m_FrictionForce;

// Позиция в мире и вращение, полученные напрямую от железнодорожного пути.
Vector m_WorldPosition;
Quaternion m_WorldRotation;

// Постоянная во время симуляции:
float m_Mass;
}

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

В качестве базового шага симуляции для каждого вагона использовался метод Эйлера. Здесь «dt» — это время для одного шага.

void WagonPhysicsState::BasicSimulationStep(float dt)
{
// Рассчёт производных.
float dPosition = m_LinearSpeed;
float dLinearMomentum = m_EngineForce + m_FrictionForce;

// Обновление импульса.
m_LinearMomentum += dLinearMomentum*dt;
m_LinearSpeed = m_LinearMomentum / m_Mass;

// Обновление позиции.
float DistanceToTravelDuringThisStep = dPosition*dt;
m_Track.MoveAlongSpline( DistanceToTravelDuringThisStep );

// Получение новой позиции от железнодорожного пути.
m_WorldPosition = m_Track.GetCurrentWorldPosition();
m_WorldRotation = m_Track.AlignToSpline();
}

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

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Третье уравнение определяет импульс (P) как произведение массы и скорости.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Применение импульса к вагону — это просто ещё одна операция, которая добавляется к текущему импульсу.

void WagonPhysicsState::ApplyImpulse(float AmountOfImpulse)
{
m_LinearMomentum += AmountOfImpulse;
m_LinearSpeed = m_LinearMomentum / m_Mass;
}

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

Высокоуровневые шаги симуляции поезда

Вот так выглядит код полного шага симуляции для одного поезда.

// Часть A
Обновление стартовой скорости поезда

// Часть B
Для всех вагонов поезда
ApplyDeferredImpulses

// Часть C Для всех вагонов поезда
UpdateCouplingChainConstraint

// Часть D
Для всех вагонов поезда
UpdateEngineAndFrictionForces
SimulationStepWithFindCollision
CollisionResponse

Часть А имплементирует определённое поведение поезда на его старте. Часть В отвечает за различные импульсы, получаемые с помощью коллизий. Часть С помогает убедиться, что максимальное растяжение связующей цепи не превышено. Часть D отвечает за двигатель, силу трения, базовую симуляцию и обработку коллизий.

Симуляция с коллизиями

Так выглядит код для функции SimulationStepWithFindCollision.

WagonPhysicsState SimulationStepWithFindCollision(WagonPhysicsState InitialState, float dt)
{
WagonPhysicsState NewState = InitialState;
NewState.BasicSimulationStep( dt );
bool IsCollision = IsCollisionWithWagonAheadOrBehind( NewState );
if (!IsCollision)
{
return NewState;
}
return FindCollision(InitialState, dt);
}

Cперва система проводит пробный шаг симуляции, обращаясь к функции NewState.BasicSimulationStep( dt ); и проверяя, зафиксированы ли в новом состоянии какие-то коллизии: bool IsCollision = IsCollisionWithWagonAheadOrBehind( NewState ). Если ответ будет отрицательным, то игра рассчитывает новое состояние. Однако если коллизия всё-таки есть, то применяется функция FindCollision, чтобы обнаружить точные время и физические данные до того, как коллизия произошла. Для этого используется бинарная система поиска.

Так выглядит луп поиска времени и физического состояния коллизии.

{
WagonPhysicsState Result = CurrentPhysicsState;
float MinTime = 0.0f; float MaxTime = TimeToSimulate;
for (int step = 0 ; step<MAX_STEPS ; ++step)
{
float TestedTime = (MinTime + MaxTime) * 0.5f;
WagonPhysicsState TestedPhysicsState = CurrentPhysicsState; TestedPhysicsState.BasicSimulationStep(TestedTime);
if (IsCollisionWithWagonAheadOrBehind(TestedPhysicsState))
{
MaxTime = TestedTime;
}
else { MinTime = TestedTime;
Result = TestedPhysicsState;
}
}

return Result;
}

Коллизии проверяются в трёхмерном пространстве при помощи m_WorldPosition и m_WorldRotation, полученных из WagonPhysicsState. Метод IsCollisionWithWagonAheadOrBehind позволяет проводить тест коллизий между двумя oriented bounding boxes (OBB).

Ответ коллизий

После того, как разработчики обнаружат состояние физики до того, как случилась коллизия, системе нужно рассчитать правильный импульс-реакцию (j), чтобы применить её к паровозу и вагонам. Сперва игра рассчитывает относительную скорость между вагонами перед коллизией (скорость локомотива минус скорость прицепа).

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

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

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Затем определяется коэффициент реституции (r). Он показывает, насколько «упругим» будет ответ коллизии. Если r=0, то энергия теряется, если же r=1 — не происходит никаких потерь энергии. Таким образом формула расчета выглядит следующим образом.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

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

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Само значение j рассчитывается следующим образом.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

В Assassin’s Creed Syndicate значение r равно 0,35.

Система применяет импульс +j к локомотиву и –j к прицепам. Тем не менее, к прицепам применяются «отсроченные» импульсы. Так как интеграция для локомотива уже отработана, разработчики не хотели, чтобы менялась его текущая скорость, поэтому импульс «сдвигается» на следующий кадр симуляции. Это не создаёт каких-либо видимых визуальных изменений, так как разницу в один кадр тяжело заметить. «Отсроченные» импульсы собираются и применяются к вагону в части В на следующем кадре симуляции.

Сцепка вагонов

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

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

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Здесь X — это дистанция пути, V — текущая скорость, а t — время, которое потребуется на шаг симуляции.

Будущая длина сцепной цепи (FutureChainLength) — это сумма текущей длины и разности между дистанциями, которую проедут за шаг симуляции локомотив и прицеп.

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

d = будущая длина цепи — максимальная длина цепи

Если ограничение нарушено, то коэффициент d окажется положительным. В таком случае системе надо применить такой импульс, чтобы d оказался равен нулю. Разработчики использовали массу вагона, чтобы рассчитать подходящий импульс. Они хотели сделать так, чтобы лёгкие вагоны двигались дальше, а тяжёлые проходили меньший путь.

Сами импульсы рассчитываются с помощью коэффициента С.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Сумма обоих С равна 1. Разработчики хотели, чтобы прицепы проезжали дополнительную дистанцию равную d умноженное на С, а локомотивы — -d умноженное на С. Для этого нужно умножить дистанции на массу, поделённую на время шага симуляции.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Таким образом, формула для расчета C выглядит так.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

С её помощью можно легко вывести значения импульсов.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

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

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

Коррекция на резких участках дороги

Из-за того, что симуляция происходит в одномерном пространстве, у авторов игры возникали проблемы с подгонкой сцепки на резких поворотах. Сами сцепки находятся в трёхмерном «мире», но импульсы передаются только в одномерном. Для коррекции положения сцепки пришлось модифицировать её максимальную длину в зависимости от относительного угла между направлением локомотива и прицепа. Чем больше угол, тем меньше максимально возможная длина цепи. Сперва вычисляется точка между двумя нормализованными векторами.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Здесь С ос стрелкой — направление цепи, а Т со стрелкой — прямое направление ведущего вагона. Следующая формула используется для расчёта дистанции, которую нужно вычесть из физической длинны связующей цепи.

float DistanceConvertedFromCosAngle = 2.0f*clamp( (1.0f-s)-0.001f, 0.0f, 1.0f );
float DistanceSubtract = clamp( DistanceConvertedFromCosAngle, 0.0f, 0.9f );

Система не высчитывает точное значение угла, так как используется его косинус. Это позволяет сэкономить время на обработке данных. Кроме того, разработчики использовали ещё несколько чисел, которые были получены в ходе эмпирических тестов — всё для того, чтобы держать значения в разумных пределах.

Старт поезда

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

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

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Разработчики расширили формулу.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Здесь mi — это масса вагона i (m первое — масса локомотива), VA — текущая скорость локомотива (допускается, что все вагоны имеют одну и ту же скорость). Вводится также дополнительный символ mx.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Формулу можно упростить.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Искомое VB рассчитывается следующим образом.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

С помощью этой формулы новое значение скорости присваивается локомотиву и всем вагонам (начиная со второго). Вот как это выглядит на схеме.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Трение

Чтобы рассчитать силу трения (переменная m_FrictionForce в WagonPhysicsState) разработчики использовали формулы, которые были отобраны после серии экспериментов так, чтобы они хорошо поддерживали геймплей. Значение силы трения — это константа, которая масштабируется в зависимости от текущей скорости. На графике изображена стандартная сила трения для вагонов.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

У отсоединённых вагонов сила трения меняется.

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

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

Настоящие паровозы: как устроена физика поездов в Assassin's Creed Syndicate

Здесь t — время, прошедшее после отсоединения вагонов (измеряется в секундах).

8282
47 комментариев

В это время Бесезда:

94
Ответить

Всё гениальное - просто!

5
Ответить

Это все очень интерестно , но есть 1 вопрос. Нахера?! Нахера такая сложная физика транспорта в Assasin creed-e? По моему время потраченное на проработку физики поезда , можно было потратить на проработку более качественного геймплея.

6
Ответить

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

Вот ты сейчас прочитал за поезд, а это даже не верхушка айсберга того труда, который вкладывается в подобные проекты:)

27
Ответить

Вот меня просто убивают такие комментарии. Парень, довольно таки давно человечество додумалось до такой шьуки как разделение труда. Это значит что один человек делает одну определенную работу, а другой иную. Когда результат их труда соединяется воедино, получается система, по качеству превосходящая ту, которая была бы создана каждым из участников ее создания, по отдельности. Довольно таки не сложная концепция, как мне кажется. Ну так к чему это я... Ах, да. Как ни странно, физикой занимаются программисты, а геймпреем - геймдизайнеры. В очень маленьких студиях один человек может одновременно заниматься этими двумя видами работ, но это не случай такого гиганта как Ubisoft. Следовательно, говорить что лучше бы они занимались гейсплеем а не физикой, такой же идиотизм как говорить, что американцы не должны строить небоскребы, пока не добьются полной эмансипации женщин. В смысле, чего блядь? Где архитекторы и где SJW? Это разные миры. Настолько же разные как и миры геймдизайнеров и программистов.

17
Ответить

Открою тебе маленький секрет: всё что тут написано сделано максимально кратко, просто и лаконично.

16
Ответить

Ну, за пару месяцев разработчики перенесли Юнити на новые текстурки, надо было чем-то заниматься остаток года.

1
Ответить