Unity3d Tank Tutorial: Ходовая часть (Урок 1. Основы механики транспортного средства)
Итак вы уже не новички в Unity, вы умеете создавать сцену, расставлять объекты на сцене, создавать terrain, делать префабы и тд… и главное — работать со скриптами, либо вы уже изучили статьи: Unity3D для начинающих — Туториал 1 и Unity3d script basics. Все эти знания вам пригодятся ведь в этом уроке мы двинемся намного дальше.
А именно: в первой части этого урока вы узнаете что такое Wheel Collider, как с помощью него управлять транспортным средством и создать реалистичное поведение подвески автомобиля, во второй части урока, на основе всего этого я покажу как сделать ходовую часть танка с соответствующей физикой и контроллерами.
Собственно то что вы видите на изображении сверху (это будет результат данного урока) вы можете опробовать «вживую» прямо в браузере. Хотите сделать это сами? Тогда добро пожаловать под кат
Перед тем как начать рассказывать об основах моделирования механики автомобиля на Unity я не могу не отметить что на официальном сайте уже есть уроки на эту тему например здесь. Правда на протяжении всего этого даются лишь указания на то, какой скрипт прикрепить к какому объекту и лишь изредка объясняют чтонибудь полезное, в третьей части урока наконец то объясняют как работает главный скрипт, хотя по моему это надо было делать в самой первой части, и начинать объяснять с того момента как работают wheel collider’ы и как они взаимодействуют с rigidbody, в итоге я сделал вывод что данный урок не рассчитан на новичков в Unity и хочу это исправить.
Но если вы уже знаете что такое Wheel Collider и с чем его едят, то вы можете пропустить этот урок.
1. Создаем «Автомобиль»
Для начала проделайте стандартные процедуры, создайте сцену, создайте terrain, либо используйте уже готовые, затем создайте пустой Game Object(GO), (GameObject->Create Empty), назовем его какнибудь оригинально, например Car.
Затем создайте куб (GameObject->Create Other -> Cube) примените к нему какой нибудь материал из стандартных, чтобы его было лучше видно. Представим себе что это кузов нашего автомобиля, и растянем его с помощью Scale по ширине и длине так чтобы передняя часть этого кузова смотрела вдоль оси Z (синяя ось, именно она официально считается передом в мире Unity) затем перетянем наш Cube на объект Car во вкладке Hierarchy, чтобы он стал дочерним объекту Car. Перейдем на сам объект Car и добавим ему Rigidbody (Component -> Physics -> Rigidbody). Назначим ему соответствующий вес, примерно 1500 кг., кузов автомобиля готов.
Помимо кузова у автомобиля должны быть колеса, теперь создайте цилиндр (GameObject->Create Other -> Cylinder), используйте все тот же Scale, придайте ему вид колеса, затем разверните его по оси Z на 90 градусов (важно! Только по оси Z!).
Переименуем наш только что созданный цилиндр в Wheel Forward Left, установим колесо на свое место, тоесть спереди и слева от нашего куба, (повторяю, передом в Unity считается положительная чать оси Z).
Перетянем Wheel Forward Left на объект Car. Назначим колесу текстуру по детальнее чтобы лучше видеть вращение колеса. Теперь скопируем наше колесо (Правой кнопкой по Wheel Forward Left и Duplicate), назовем Wheel Back Left и поставим на законное место, по аналогии поступите с остальными двумя колесами.
Итак, наш суперкар почти готов! Поднимите его немного над поверхностью terrain’а и нажмите Play, автомобиль упадет на землю и встанет колом на колеса, о мягкой подвеске ему еще только мечтать, выходите из Play mode. Как видите у наших колес имеется компонент под названием Capsule Collider, а у кузова Box Collider, я не буду рассказывать в этом уроке что такое коллизии и как на основе них рассчитать положение объекта в пространстве, объясню проще: если у объекта имеется какой – либо из простых collider’ов (Box, Capsule, Mesh,…) то этот объект может взаимодействовать с другими объектами содержащими Rigidbody и один или несколько collider’ов, в свою очередь объект содержащий Rigidbody будет вычислять свою позицию на основе столкновений всех подчиненных ему collider’ов и назначенных им физических материалов.
Если вы из вышесказанного не поняли ничего, не заморачивайтесь, понимание придет с практикой.
А пока что выберите каждое из наших колес и удалите у них Capsule Collider (Правой кнопкой по нему и Remove Component). Нажмите Play, теперь автомобиль больше не стоит на колесах, они прошли сквозь террейн и взаимодействует с поверхностью у нас теперь только кузов.
2. Добавляем Wheel Colliders
Итак что же такое этот загадочный Wheel Collider? Давайте разбираться. Создайте пустой GameObject назовите его WCollider Forward Left, теперь добавьте к нему компонент Wheel Collider (Component -> Physics -> Wheel Collider), он выглядит как окружность с отрезком внутри, он может быть слишком большим или слишком маленьким по сравнению с вашим колесом, все зависит от того насколько вы увлеклись использованием scale. К счастью у него есть параметр Radius, изменяя данный параметр подгоните его под радиус своего колеса, затем создайте новый префаб (Assets -> Create -> Prefab) назовите его WCollider затем перетяните WCollider Forward Left на префаб. Мы создали префаб для удобства, так как коллайдеров у нас будет столько же сколько колес, и чтобы не изменять параметры каждого, будем изменять параметры префаба, остальные коллайдеры унаследуют его свойства. Теперь делаем стандартную процедуру, перетаскиваем WCollider Forward Left на объект Car и помещаем в центр переднего левого колеса, дублируйте его 3 раза переименуйте и переместите в соответствии с другими колесами.
Нажмите Play, автомобиль упадет и опять встанет на колеса (кстати если колеса все равно проходят сквозь террейн, это значит что автомобиль падает на землю с очень большой скоростью, и вам просто напросто нужно уменьшить расстояние до земли, либо увеличите параметр Drag у Rigidbody). Как видите результат не сильно отличается от того где у нас на колёсах стояли Capsule Colliders, все правильно, потому, что мы не задали нашим Wheel Collider’ам никаких параметров.
Итак, выйдите из Game Mode, выберите наш префаб WCollider, поехали меняем свойства компонента Wheel Collider:
- Suspension Distanse – по сути это длина пружины нашей подвески, ставим 0.15
- Suspension Spring -> Spring – жесткость подвески, чем больше вес нашего Rigidbody, тем больше должна быть жесткость, ставим 9000
- Suspension Spring -> Damper – Смягчитель подвески, большее значение заставляет пружину двигаться медленнее, ставим 100
- Mass – масса колеса, оставляем 1
- Forward Friction – «передняя» сила трения колеса, оставляем как есть
- Sideways Friction – «боковая» сила трения колеса, полезно если хотим реализовать дрифт автомобиля, оставляем как есть
Итого у нас получилось:
(Не смотрите на радиус, у вас он может быть другой, главное чтобы окружность совпадала с вашим колесом)
Итак, нажмите Play сейчас, посмотрите, наш автомобиль ведет себя теперь совсем по другому, теперь он пружинит от поверхности террейна, а если вы поставите его на склон какой нибудь горки, он покатится с неё, вот так у нас работают Wheel Collider’ы.
Но это далеко не все на что они способны, давайте выясним как с помощью них управлять автомобилем и сделать так чтобы наши колеса крутились и реагировали на неровности ландшафта. Начинаем скриптовать!
3. Управляем Wheel Collider’ами
В этом уроке я буду писать скрипты на C#, простите меня JavaScript’еры, он мне привычнее, я думаю вы разберетесь.
Создаем новый С# скрипт (Assets -> Create -> C Sharp Script), назовем его CarController, открываем и напишем следующее:
using UnityEngine; using System.Collections; public class CarController : MonoBehaviour < //1 public WheelCollider[] WColForward; //2 public WheelCollider[] WColBack; //3 // Use this for initialization void Start () < //4 >void FixedUpdate () < //5 >>
- Главный класс скрипта должен называться точно так же как и файл скрипта, иначе компилятор выдаст ошибку.
- Массив передних коллайдеров.
- Массив задних коллайдеров.
- Будем использовать функцию Start() для инициализации.
- Как то в уроке Unity3d script basics я говорил что для физического взаимодействия с объектами лучше использовать функцию FixedUpdate(), нежели функцию Update().
Затем перетяните скрипт на объект Car, как вы можете видеть в инспекторе, у нас в скрипте есть два поля, собственно это массивы которые мы объявили, в них надо перетянуть наши коллайдеры следующим образом.
Далее работаем с нашими коллайдерами, дополняем скрипт:
using UnityEngine; using System.Collections; public class CarController : MonoBehaviour < public WheelCollider[] WColForward; public WheelCollider[] WColBack; public float maxSteer = 30; //1 public float maxAccel = 25; //2 public float maxBrake = 50; //3 // Use this for initialization void Start () < >void FixedUpdate () < float accel = 0; float steer = 0; accel = Input.GetAxis("Vertical"); //4 steer = Input.GetAxis("Horizontal"); //4 CarMove(accel,steer); //5 >private void CarMove(float accel,float steer) < //5 foreach(WheelCollider col in WColForward)< //6 col.steerAngle = steermaxSteer; //6 >if(accel == 0) < //7 foreach(WheelCollider col in WColBack)< //7 col.brakeTorque = maxBrake; //7 >>else < //8 foreach(WheelCollider col in WColBack)< //8 col.brakeTorque = 0; //8 col.motorTorque = accelmaxAccel; //8 >> > >
Теперь можете нажать Play, и используя кнопки W,S или стрелки вперед — назад, привести в движение нашу «формулу один», а кнопками A,D, или стрелками влево – вправо, поворачивать.
- Максимальный угол поворота колес.
- Максимальный крутящий момент передающийся на колесо.
- Максимальный тормозной момент.
- Функция GetAxis() класса Input ждет события которое возникает когда мы нажимаем на клавиши движения на контроллере, либо двигаем стики джойстика, функция принимает направление виртуальной оси, (например «Vertical» – это клавиши W,S, либо движение джойстика вперед – назад) и возвращает число от -1 до 1, где -1 – движение назад либо влево, (клавиша S в «Vertical» оси, D в «Horizontal») 0 – оси не активны и 1 – движение вперед, либо вправо (клавиша W в «Vertical» оси, A в «Horizontal», либо движение стика джойстика влево — вправо).
- Передаем результаты полученные от виртуальных осей в функцию CarMove().
- Для каждого Wheel Collider’а находящегося в массиве WColForward мы изменяем переменную steerAngle, (угол поворота колеса) умножая её на значение полученное с горизонтальной оси.
- Если вертикальная ось у нас в положении покоя (тоесть если мы не давим на клавиши W или S) то изменяем переменную коллайдера brakeTorque, которая отвечает за силу торможения, это нужно для того чтобы автомобиль не двигался по инерции если мы не нажимаем на клавиши движения.
- В других случаях, (если мы нажимаем на клавиши W или S) обнуляем силу торможения, а затем изменяем переменную motorTorque, (крутящий момент колеса) умножая её на значение полученное с вертикальной оси, вследствие чего наш автомобиль начинает ускоряться.
Давайте обо всем по порядку.
4. Центр тяжести
Задается он удивительно просто, во первых нам надо создать пустой GO, назовем его Center of mass, затем перетянуть его на объект Car, и разместить его приблизительно там где вы считаете он должен быть, например чтобы моё авто не переворачивалось при повороте на высокой скорости мне пришлось сделать его вот так:
- Объявите переменную: public Transform COM.
- Перетащите GO Center of mass на поле COM в скрипте.
- Внутри функции Start() напишите следующую строчку: rigidbody.centerOfMass = COM.localPosition
5. «Оживляем» колеса
Ну вот мы и добрались до финальной, на мой взгляд самой интересной в этом уроке и в то же время довольно сложной темы.
Для того чтобы оживить колесо, нам необходимо вычислять позицию и угол его вращения в каждом фиксированном кадре, в этом нам опять поможет чудесный WheelCollider, его метод GetGroundHit(), который способен вернуть структуру WheelHit в которой в свою очередь содержится точка соприкосновения коллайдера и террейна (переменная point). Благодаря методу GetGroundHit() мы можем вычислить позицию колеса на основе движения пружины подвески. Ну а что касается угла поворота – это совсем просто, в WheelCollider’е есть float переменная rpm, это аббревиатура от rotation per minute, на её основе мы и можем определить угол вращения колеса.
Переходим собственно к скрипту, он вырос, потолстел и теперь выглядит так:
using UnityEngine; using System.Collections; public class CarController : MonoBehaviour < public WheelCollider[] WColForward; public WheelCollider[] WColBack; public Transform[] wheelsF; //1 public Transform[] wheelsB; //1 public float wheelOffset = 0.1f; //2 public float wheelRadius = 0.13f; //2 public float maxSteer = 30; public float maxAccel = 25; public float maxBrake = 50; public Transform COM; public class WheelData< //3 public Transform wheelTransform; //4 public WheelCollider col; //5 public Vector3 wheelStartPos; //6 public float rotation = 0.0f; //7 >protected WheelData[] wheels; //8 // Use this for initialization void Start () < rigidbody.centerOfMass = COM.localPosition; wheels = new WheelData[WColForward.Length+WColBack.Length]; //8 for (int i = 0; ifor (int i = 0; i > private WheelData SetupWheels(Transform wheel, WheelCollider col) < //10 WheelData result = new WheelData(); result.wheelTransform = wheel; //10 result.col = col; //10 result.wheelStartPos = wheel.transform.localPosition; //10 return result; //10 >void FixedUpdate () < float accel = 0; float steer = 0; accel = Input.GetAxis("Vertical"); steer = Input.GetAxis("Horizontal"); CarMove(accel,steer); UpdateWheels(); //11 >private void UpdateWheels()< //11 float delta = Time.fixedDeltaTime; //12 foreach (WheelData w in wheels)< //13 WheelHit hit; //14 Vector3 lp = w.wheelTransform.localPosition; //15 if(w.col.GetGroundHit(out hit))< //16 lp.y -= Vector3.Dot(w.wheelTransform.position - hit.point, transform.up) - wheelRadius; //17 >else < //18 lp.y = w.wheelStartPos.y - wheelOffset; //18 >w.wheelTransform.localPosition = lp; //19 w.rotation = Mathf.Repeat(w.rotation + delta w.col.rpm 360.0f / 60.0f, 360.0f); //20 w.wheelTransform.localRotation = Quaternion.Euler(w.rotation, w.col.steerAngle, 90.0f); //21 > > private void CarMove(float accel,float steer) < foreach(WheelCollider col in WColForward)< col.steerAngle = steermaxSteer; >if(accel == 0) < foreach(WheelCollider col in WColBack)< col.brakeTorque = maxBrake; >>else < foreach(WheelCollider col in WColBack)< col.brakeTorque = 0; col.motorTorque = accelmaxAccel; >> > >
Скопируйте его, вставьте в свой скрипт, затем во вкладке hierarchy выберите наш объект Car, как вы можете видеть в инспекторе у нас появились новые переменные и массивы в скрипте, в массивы wheelsF и wheelsB необходимо передать передние и задние колеса соответственно, (колеса а не Wheel Collier’ы!) как сделаете это нажмите Play. Если колеса уходят под террейн то поменяйте значение переменной wheelRadius, оно должно примерно совпадать с радиусом ваших коллайдеров.
Итак должно произойти чудо, ваши колеса теперь будут вращаться и реагировать на неровности ландшафта прям как настоящие.
- Создаем массивы которые будут хранить Transform наших колес.
- Данные переменные нам пригодятся, ниже объясню для чего.
- Создадим класс который будет содержать нужную нам информацию о каждом нашем колесе, а именно:
- Transform колеса;
- Wheel Collider Колеса;
- Стартовую позицию колеса;
- Угол вращения колеса.
- Объявляем массив wheels c типом WheelData.
- Передаем в массив wheels необходимые нам данные, для этого я написал функцию SetupWheels().
- Функция SetupWheels() принимает Transform колеса и его WheelCollider, передает в переменные содержащиеся в классе WheelData необходимые нам данные и возвращает его.
- Напишем функцию UpdateWheels() в которой будет вычисляться позиция и угол поворота наших колес.
- Запоминаем переменную fixedDeltaTime класса Time она нужна нам для того чтобы вращение колеса было равномерно растянуто по времени.
- Для каждого элемента массива wheels выполняем следующие операции:
- Создаем переменную класса WheelHit (о ней я рассказывал выше);
- Запоминаем локальную позицию колеса (локальная позиция – это позиция относительно родительских координат, глобальная – относительно мировых);
- Если WheelCollider колеса сталкивается с поверхностью террейна (либо чего нибудь другого);
- То из координаты Y локальной позиции колеса вычитаем Dot() между вектором с началом в точке в которой коллайдер соприкасается с поверхностью террейна (hit.point) и концом в текущей позиции колеса (w.wheelTransform.position) и между вектором направленным вверх относительно объекта Car (transform.up) и из всего этого вычитаем еще и переменную wheelRadius чтобы колесо заняло правильное место. (Если из вышесказанного вы не поняли ни слова, не расстраивайтесь большинство людей не понимает этого с первого раза, для этого вам нужно хорошо знать что такое Dot product и как он связан с перечисленными выше векторами, либо просто знайте что позиция колеса должна вычисляться именно так);
- Если WheelCollider не касается поверхности террейна. То из координаты Y начальной локальной позиции колеса отнимаем wheelOffset, благодаря этому наши колеса не улетают в неизвестном направлении когда автомобиль падает с высоты или лежит на «спине»;
- Применяем измененную позицию колеса к его текущей позиции;
- Вычисляем угол «вращения» колеса, используя функцию Repeat() класса Mathf, данная функция помогает нам держать угол в интервале от 0 до 360 градусов, сам же угол мы вычисляем благодаря не хитрой формуле (w.rotation + delta w.col.rpm 360.0f / 60.0f) где мы собственно к текущему углу поворота прибавляем дельту времени умноженную на число поворотов в минуту хранящееся в коллайдере и умноженную на 360/60.
- Применяем к текущим локальным углам поворота получившийся результат, в этом нам помогает функция Euler() класса Quaternion которая возвращает кватернион в Эйлеровых углах (как бы не было страшно слово кватернион я советую вам абстрагироваться и воспринимать его как структуру хранящую локальные и глобальные углы поворота, хотя означает оно совсем другое, здесь я не буду рассказывать что именно, так просто будет понятнее). Первым аргументом (тоесть угол вокруг оси X) мы передаем наш вычисленный угол «вращения» колеса, вторым аргументом (угол вокруг оси Y) мы передаем угол поворота коллайдера, и третьим аргументом (угол по оси Z, теперь понимаете почему в пункте 1 было важно повернуть колеса именно вокруг оси Z) предаем константу 90.0f.
Заключение
Ну вот и все что я могу рассказать про то как построить механику обычного автомобиля на Unity3D, прочитав и осознав все вышесказанное вы можете прямо сейчас отправляться делать гоночный симулятор.
Ну а в следующей части урока я как и обещал расскажу о том как построить механику гусениц танка, также я расскажу о методах его моделирования в 3D редакторе и немного про экспорт модели из него и импорт в Unity ну и естественно я не буду вас заставлять моделировать то что вы видели в данном демо, а предоставлю уже готовую модель. Спасибо за внимание и удачи в изучении Unity3d.
https://habr.com/ru/articles/115557/