Найти тему

Space Engineers - Первый автопилот. Выравниваем корабль по горизонту, перпендикулярно вектору гравитации

Оглавление
Демонстрационное видео в конце статьи.

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

Но что означает фраза "по линии горизонта"? Давайте разбираться на примере. Ниже показана картинка, на которой треугольником изображен корабль, как-то повёрнутый в пространстве.

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

Вниз экрана направлен вектор гравитации планеты. Если углы между ним и вектором Left, между ним и вектором Bacward одновременно будут равны 90°, то корабль будет "выровнен по линии горизонта".

Переходим к теории. При написании скрипта нам будут доступны игровые функции получения вектора гравитации и векторов Left и Backward. Но что с ними делать? Погрузимся в теорию.

Нормирование вектора

Вектор на плоскости можно описать тремя параметрами:

  1. Точкой начала;
  2. Углом относительно одной из осей координат плоскости;
  3. Длиной вектора (модулем вектора).

Применительно к вектору гравитации, на Землеподобной планете игра выдаст нам вектор с началом в центре массы корабля, направленным в центр планеты, длинной по модулю, равной 9,8 м.

Нормирование вектора - это приведение его длинны к единице, с сохранением остальных двух свойств. В нашем случае длинна нормированного вектора гравитации (НВГ) будет равна 1 м.

Вектора Left и Backward при получении всегда равны по модулю 1 м и не нуждаются в нормировании. Зачем нам понадобилось нормирование? Разберемся ниже.

Скалярное произведение векторов

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

На рисунке ниже показаны нормированный вектор гравитации (НВГ) и вектор Left, расположенные под разными углами друг относительно друга.

-2

Мы видим, что результат вычисления косинуса угла между ними можно преобразовать в управляющее воздействие на гироскопы! То есть, если корабль перпендикулярен НВГ, то косинус угла равен нулю, и корабль ни куда не кренится. Если корабль накренился, а угол стал равным, например, 45 градусам, то значение косинуса (0,7) можно передать в качестве управляющего воздействия на гироскопы! Если корабль накренился на в другую сторону, например, на -45 градусов, то результат вычисления косинуса станет отрицательным, и гироскопы все равно повернут корабль перпендикулярно.

Чем сильнее корабль отвернут от горизонта, тем больше значение косинуса, тем "реще" гироскопы начнут выравнивание, плавно замедляясь при приближении к горизонту.

Осталось вычислить косинус. Тут-то нам и пригодится скалярное произведение двух векторов:

-3

Как помните, в расчете мы используем нормированные вектора НВГ и Left (модули их длин равен 1), а значит, что функция вернет нам чистое значение косинуса угла, нужное для гироскопов. В игре скалярное произведение уже реализовано разработчиками и доступно нам, как метод Dot() класса Vector3D.

Теперь понятно, почему я использовал вектора Left и Backward вместо Right и Forward? Все дело в знаке косинуса. Либо умножаем результат на -1, либо используем вектора, направленные обратно.

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

Мы рассмотрели пару векторов НВГ-Left. Но корабль находится в трехмерном пространстве, поэтому требуется вторая, перпендикулярная пара НВГ-Backward. Скрипт вычисляет косинусы двух углов и, тем самым, выравнивает наш грид в пространстве.

Код скрипта

Возьмем скрипт из прошлой статьи за основу и дополним его.

Контроллер корабля

Первое, что бросается в глаза - это новый игровой интерфейс IMyShipController. С его помощью мы будем получать вектор гравитации планеты методом GetNaturalGravity(). Кроме того, он позволяет перехватить нажатие игроком кнопок W, A, S, D, Q, E, C, Space и движение мышкой по двум координатам.

В конструкторе класса public Program() мы получаем ссылку на контроллер кодом:

IMyShipController ship_controller = GridTerminalSystem.GetBlockWithName(SHIP_CONTROLLER_NAME) as IMyShipController;

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

В конструкторе класса
public GyroClass(Program prog, ref IMyShipController shipController)
мы получаем ссылку на контроллер и сохраняем ее в переменной
SControl = shipController;

Свойства класса GyroClass

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

  • internal float PitchSensitivity { get; set; } = 1;
  • internal float RollSensitivity { get; set; } = 1;
  • internal float YawSensitivity { get; set; } = 1;

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

internal void GravityAligment()

А вот и наш виновник торжества. Метод, выравнивающий корабль по горизонту. Рассмотрим его код подробно:

internal void GravityAligment()
{
Vector3D gravity = Vector3D.Normalize(SControl.GetNaturalGravity());

float pitch = (float)gravity.Dot(SControl.WorldMatrix.Backward) * PitchSensitivity;
SetPitchRPM(pitch);

float roll = (float)gravity.Dot(SControl.WorldMatrix.Left) * RollSensitivity;
SetRollRPM(roll);

float yaw = SControl.RotationIndicator.Y * YawSensitivity;
SetYawRPM(yaw);
}

SControl - контроллер нашего корабля. Получаем у него вектор гравитации методом SControl.GetNaturalGravity(). Далее нормируем вектор и сохраняем в переменную gravity типа Vector3D. Это будет тот самый НВГ из материала, описанного выше.

В контроллере SControl хранится мировая матрица WorldMatrix, из которой мы можем узнать вектор направления корабля Backward: SControl.WorldMatrix.Backward
С помощью функции Dot() скалярно умножаем НВГ на полученный вектор Backward, получив таким образом значение косинуса угла между НВГ и Backward.

Результат вычисления функции Dot() имеет тип double, но на гироскопах свойства Roll, Pitch и Yaw имеют тип float. Поэтому используем неявное приведение типов: (float)gravity.Dot(). Полученный косинус угла умножаем на коэффициент чувствительности и сохраняем в переменную pitch.

Далее воспользуемся методом SetPitchRPM(pitch), написанном в прошлой статье, чтобы раздать управляющее воздействие на все гироскопы корабля.

Логика работы с вектором Left полностью аналогично.

RotationIndicator

Я заметил, что при работе скрипта очень хочется подруливать корабль влево-вправо. Но, как вы помните, если с передать управление гироскопами скрипту, то игрок "лишается" кнопок Q, E и движения мышкой. Но в процессе выравнивания корабля по горизонту требуются только два свойства гироскопов Pitch и Roll. А значит, свойство Yaw остается не занятым для передачи на него движения мышки.

У контроллера корабля есть класс RotationIndicator, который имеет свойства X и Y. "X" отвечает за движение мышкой вверх-вниз, "Y" - влево-вправо.

Они могут иметь только три значения. Нас интересует "Y":

  • 1 - при движении мышкой вправо;
  • 0 - если мышка неподвижна;
  • -1 - при движении мышкой влево.

Получив от контроллера значение RotationIndicator.Y и умножив его на коэффициент чувствительности YawSensitivity, раздадим полученный результат на все гироскопы с помощью метода SetYawRPM(yaw).

Запуск и остановка скрипта

Вернемся в начало программы к методу Main(). Модифицируем блок switch (argument.ToUpper()) так, чтобы у него осталось только два варианта аргументов:

  • START - запускает скрипт. Передает скрипту управление гироскопами и запускает выполнение функции Main() на каждом тике симуляции.
  • STOP - останавливает скрипт. Возвращает игроку управление кораблем и останавливает "тики" для экономии ресурсов.

Блок switch (updateSource) в случае "тикового" выполнения скрипта, вызывает метод Gyro.GravityAligment(); нашего класса.

Демонстрация

Полный код скрипта доступен на GitHub.

#spaceengineers #автопилот #csharp