Как стать автором
Обновить

Unity 3d Tank Tutorial: Ходовая часть (Урок 2. Гусеничное шасси)

Время на прочтение19 мин
Количество просмотров81K
Урок 1 <<

image

Вступление


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

А именно: я расскажу о том как лучше смоделировать гусеничное шасси в 3d редакторе для того чтобы оно могло адекватно двигаться и реагировать на неровности ландшафта, также дам вам готовую модель которую вы можете видеть в демо, затем вы узнаете как все это дело оживить и привести в движение с помощью Wheel Collider’ов.

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

1. Методы моделирования гусеницы


Как не странно но эти методы неразрывно связаны с методами движения гусеницы но естественно все зависит еще и от того какие возможности нам может предложить движок на котором мы хотим реализовать её движение. В случае с Unity 3d я вижу два варианта развития событий:
  1. Моделируем гусеницу из множества отдельных объектов как например в этом случае:
    (3ds max)
    image

    Приводим в движение каждый трак с помощью получения позиции следующего за ним и постепенно интерполируем позицию текущего трака к позиции следующего, вычисляем деформацию гусеницы на основе неровностей ландшафта с помощью метода Raycast() класса Collider, данный метод проецирует луч определенной длины, в определенном направлении и возвращает true если он пересекается с каким либо коллайдером.
    Достоинства данного метода: высокая детализация гусеницы, высокая реалистичность движения гусеницы.
    Недостатки: сложность реализации, нагрузка на вычислительную мощность зависящая от количества траков т.к. придется вычислять Raycast() для каждого, если их будет слишком много, падение производительности неизбежно.
    Честно сказать данный метод я и не пытался реализовать, т.к. полазив по gamedev'овским форумам нашел второй.
  2. Моделируем гусеницу непрерывной лентой:
    (3ds max)


    Затем накладываем повторяющуюся текстуру одного или нескольких траков.

    Действительно смотрите, тут все проще, иллюзию движения траков гусеницы можно создать передвижением текстуры с помощью изменения её текстурных координат. А деформирование в соответствии с неровностями ландшафта можно сделать привязав кости (bones) в тех местах ленты где находятся опорные катки, так как из первого урока мы уже знаем как вычислить позицию колес на основе метода GetGroundHit() привязанного к колесу WheelCollider’а, то вычислить позицию костей к которым привязана лента не составит труда. Либо можно использовать тот же Raycast (кому как угодно, лично меня вполне устраивает привязка к подвеске WheelCollider’а).
    Достоинства данного метода: простота реализации, небольшая нагрузка на вычисления.
    Недостатки: Не такая высокая детализация гусеницы по сравнению с первым методом (хотя тут вопрос относительный, посмотрите как то же самое сделано в World of Tanks, Crysis, Battlefield: Bad Company, да да там везде используется лента с наложенной на неё текстурой поддерживающей прозрачность, и выглядит она вполне реалистично).

Итак выбираем второй метод, и привязываем кости к гусенице так, чтобы она могла деформироваться следующим образом:
(3ds max)




Как вы можете видеть на скриншоте, колеса тоже привязаны к тем же костям что и гусеница. Давайте поэкспериментируем, создайте какой нибудь примитив, привяжите его к кости, или нескольким, затем экспортируйте его в Unity (про экспорт из различных пакетов 3d моделирования вы можете почитать здесь, лично я советую экспортировать все в формат FBX, а затем копировать получившийся файл в папку Assets вашего проекта, после этого вы можете увидеть название данного файла во вкладке Project вашего проекта, и его достаточно перетянуть на сцену для дальнейших манипуляций с ним). После этого выберите саму модель и попробуйте переместить её, повернуть, растянуть. Получается? Правильно, не получается! Дело в том что в Unity, любой Skinned Mesh привязанный к кости (костям) становится рабом её трансформаций, попробуйте проделать те же операции с костью к которой он привязан и у вас все получится.

Соответственно нам не нужно чтобы колеса были привязаны к кости так как они должны вращаться. Отвязываем их, теперь это должно выглядеть вот так:
(3ds max)


Ну вот в принципе и все, подготовка модели закончена, и как я обещал даю ссылку на свою модель, и текстуру гусениц. Модель в формате FBX, так что вы можете импортировать её в свой 3d редактор поддерживающий данный формат (насколько я знаю поддерживают его все популярные редакторы), и рассмотреть подробнее как она сделана и как привязаны кости, либо вы можете сразу импортировать в Unity и посмотреть уже на практике, чем мы собственно займемся далее.

2. Импорт модели и подготовка скрипта


Итак, как я писал выше, скопируйте модель и текстуру в папку Assets вашего проекта, либо можете просто перетащить их прямо из своего explorer’a, во вкладку Project, главное чтобы модель и текстура находились в одной папке для того чтобы текстура наложилась автоматически, если же по каким – либо причинам текстуры на гусеницах вы не видите (перед тем как увидеть надо естественно вытянуть модель на сцену), или вы хотите вопреки всему закинуть её в другую папку, не беда, просто выберите одну из гусениц (они называются Track_line_left и Track_line_right), затем в инспекторе найдите настройку текстуры, нажмите кнопку Select и выберите текстуру гусеницы.
image
Также установите Tiling по y равным 2, как на изображении, для того чтобы увеличить количество траков.

Теперь создайте новый C# скрипт (Assets -> Create -> C Sharp Script), назовем его TankTrackController, откроем его и объявим необходимые нам переменные с которыми в дальнейшем будем работать:

using UnityEngine;
using System.Collections;
using System.Collections.Generic; //1

public class TankTrackController : MonoBehaviour {
	
	public GameObject wheelCollider; //2
	
	public float wheelRadius = 0.15f; //3
	public float suspensionOffset = 0.05f; //4
		
	public float trackTextureSpeed = 2.5f; //5
		
	public GameObject leftTrack;  //6
	public Transform[] leftTrackUpperWheels; //7
	public Transform[] leftTrackWheels; //8
	public Transform[] leftTrackBones; //9
	
	public GameObject rightTrack; //6
	public Transform[] rightTrackUpperWheels; //7
	public Transform[] rightTrackWheels; //8
	public Transform[] rightTrackBones; //9
	
	public class WheelData { //10
		public Transform wheelTransform; //11
		public Transform boneTransform; //12
		public WheelCollider col; //13
		public Vector3 wheelStartPos; //14
		public Vector3 boneStartPos; //15
		public float rotation = 0.0f; //16
		public Quaternion startWheelAngle;	//17
	}
	
	protected WheelData[] leftTrackWheelData; //18
	protected WheelData[] rightTrackWheelData; //18
	
	protected float leftTrackTextureOffset = 0.0f; //19
	protected float rightTrackTextureOffset = 0.0f; //19

}

  1. Разрешаем пространство имен необходимое для использования динамических списков, они пригодятся нам позже.
  2. Объявляем переменную в которой будет храниться префаб нашего Wheel Collider’а (вспоминаем п.2 предыдущего урока).
  3. Радиус наших колес.
  4. Смещение колеса относительно начальной позиции, когда оно не касается поверхности.
  5. Скорость движения гусеницы (по сути скорость смещения текстурных координат).
  6. Левая и правая гусеницы.
  7. Левые и правые верхние колеса к которым не будут добавляться Wheel Collider’ы.
  8. Колеса к которым будут добавляться Wheel Collider’ы (как видите здесь я в отличие от предыдущего урока, не объявлял массивов для Wheel Collider’ов, они будут добавляться к нашим колесам прямо из скрипта используя позицию колес, так что не перемещайте колеса, перед нажатием на кнопку Play они должны быть выровнены относительно друг друга).
  9. Кости привязанные к левой и правой гусенице.
  10. Объявим класс в котором будем хранить необходимую нам информацию о каждом колесе (кроме UpperWheels), а именно:
  11. Transform колеса;
  12. Transform кости привязанной к гусенице;
  13. WheelCollider колеса;
  14. Начальную позицию колеса;
  15. Начальную позицию кости;
  16. Угол вращения колеса;
  17. Начальные углы поворота колеса.
  18. Объявляем массивы хранящие данные о левых и правых колесах, как видите тут используется модификатор доступа protected, для того чтобы мы не могли изменить эти данные вне класса.
  19. И наконец объявляем переменные которые будут хранить текущее смещение текстурных координат на гусеницах.

Итак основные переменные объявлены, осталось связать их с нашей моделью, сохраняем скрипт, переходим в редактор, перетаскиваем наш скрипт на объект tank (если вы его не переименовывали конечно).

Перейдите на сам объект tank, как видите помимо прикрепленного только что скрипта там есть объект Animation, смело можете его удалить он там ни к чему.
image

Далее создайте префаб объекта с WheelCollider’ом (вспоминаем п.2 предыдущего урока), я назвал его tank_collider, параметры у него будут следующие.
image

Начинаем перетаскивать объекты в наш скрипт, первым делом можете перетащить только что созданный префаб на поле Wheel Collider внутри нашего скрипта. Далее перетащите гусеницы (они называются Track_line_left и Track_line_right) на поля Left Track и Right Track. Затем верхние колеса (Upper_wheel[номер]_left, Upper_wheel[номер]_right), в массивы Left Track Upper Wheels и Right Track Upper Wheels. Ну с остальными колесами и костями я думаю сами разберетесь (колеса называются rowheel_[номер]_right и rowheel_[номер]_left, а кости Suspension_bone[номер]_left и Suspension_bone[номер]_right), главное чтобы все колеса и кости были переданы в одинаковом порядке, для того чтобы это было легче сделать я их специально пронумеровал, и еще не трогайте кости которые называются Chain_bone[номер]_left и Chain_bone[номер]_right, к ним привязана статичная часть гусеницы.

Вот как все это в конце концов должно выглядеть:
image

Далее, выберите объект tank и добавьте к нему Rigidbody со следующими параметрами:
image

Затем найдите дочерний объект hull (корпус танка), добавьте к нему Mesh Collider (Component -> Physics -> Mesh Collider), и поставьте галочку Convex (эта галочка значит что данный Mesh Collider будет вычислять столкновения не всех треугольников содержащихся в корпусе танка, а создаст свой Mesh который будет содержать максимум 255 треугольников).

3. Устанавливаем Wheel Collider’ы и параметры колес


Итак теперь наши переменные объявлены и проинициализированы, двигаемся дальше, и начнем с сохранения параметров колес в массивах WheelData[] leftTrackWheelData и WheelData[] rightTrackWheelData. Будем это делать внутри функции Awake() и объявим вспомогательную функцию WheelData SetupWheels(), которая как вы можете видеть должна вернуть значение типа WheelData, перед этим выполнив все операции с ним. Все это выглядит следующим образом:
void Awake() { 
				 
	leftTrackWheelData = new WheelData[leftTrackWheels.Length]; //1 
	rightTrackWheelData = new WheelData[rightTrackWheels.Length]; //1 
			 
	for(int i=0;i<leftTrackWheels.Length;i++){ 
		leftTrackWheelData[i] = SetupWheels(leftTrackWheels[i],leftTrackBones[i]);  //2 
	} 
		 
	for(int i=0;i<rightTrackWheels.Length;i++){ 
		rightTrackWheelData[i] = SetupWheels(rightTrackWheels[i],rightTrackBones[i]); //2  
	} 

	Vector3 offset = transform.position; //3 
	offset.z +=0.01f;  //3 
	transform.position = offset; //3		 
				 
} 
	 
	 
WheelData SetupWheels(Transform wheel, Transform bone){  //2 
	WheelData result = new WheelData(); 
		 
	GameObject go = new GameObject("Collider_"+wheel.name); //4
	go.transform.parent = transform; //5 	
	go.transform.position = wheel.position; //6
	go.transform.localRotation = Quaternion.Euler(0,wheel.localRotation.y,0); //7 
		
	WheelCollider col = (WheelCollider) go.AddComponent(typeof(WheelCollider));//8 
	WheelCollider colPref = wheelCollider.GetComponent<WheelCollider>();//9 

	col.mass = colPref.mass;//10
	col.center = colPref.center;//10
	col.radius = colPref.radius;//10
	col.suspensionDistance = colPref.suspensionDistance;//10
	col.suspensionSpring = colPref.suspensionSpring;//10
	col.forwardFriction = colPref.forwardFriction;//10
	col.sidewaysFriction = colPref.sidewaysFriction;//10 

	result.wheelTransform = wheel; //11
	result.boneTransform = bone; //11
	result.col = col; //11
	result.wheelStartPos = wheel.transform.localPosition; //11
	result.boneStartPos = bone.transform.localPosition; //11
	result.startWheelAngle = wheel.transform.localRotation; //11
		 
	return result; //12
}


  1. Объявляем размерность массивов которые будут содержать информацию о наших колесах.
  2. Заполняем эти массивы, для этого мы, как я уже писал выше, создали функцию SetupWheels() первым аргументом в которую передаем Transform колеса, а вторым Transform кости.
  3. Странно, но у меня при входе в Play mode коллайдеры постоянно проваливались под землю, к счастью лечится это простым перемещением объекта tank немного вперед, что и делают данные операции.
  4. Создаем новый пустой GameObject имя которого будет состоять из имени колеса с префиксом Collider_.
  5. Делаем только что созданный объект дочерним к объекту к которому прикреплен скрипт (в нашем случае дочерним к объекту tank).
  6. Перемещаем только что созданный объект на то место где у нас находится колесо.
  7. Поворачиваем объект локально вокруг оси Y на тот же угол на который у нас повернуто текущее колесо.
  8. Добавляем компонент Wheel Collider к нашему пустому GO.
  9. Получаем настройки Wheel Collider’a из нашего префаба.
  10. Присваиваем настройки полученные из префаба к вновь созданному Wheel Collider’у (как видите пришлось присваивать каждую настройку отдельно, в данном случае нельзя написать просто col = colPref компилятор хоть и пропускает такую конструкцию, но настройки коллайдера остаются по умолчанию).
  11. Присваиваем остальную необходимую нам информацию.
  12. Возвращаем результат.

Можете смело нажать Play и убедиться что Wheel Collider’ы добавятся к нижним колесам автоматически, нежели как в первом уроке где мы добавляли каждый коллайдер к каждому колесу вручную, я думаю не стоит рассказывать о преимуществах такого подхода.

4. «Оживляем» колеса и гусеницу


Из первого урока вы уже должны знать как научить наши колеса вращаться и реагировать на неровности ландшафта, здесь будем использовать тот же самый принцип, за исключением того что у легкового автомобиля передние и задние колеса могут вращаться с разной скоростью, у танка же опорные катки на отдельной гусенице вращаются с одинаковой скоростью, следовательно нам надо найти некую среднюю скорость вращения колес. И естественно к этой скорости нам еще необходимо привязать скорость движения текстуры гусеницы. Давайте обо всем подробнее:
void FixedUpdate(){ 
		 
	UpdateWheels(); //1  
	 
} 
 
 
public void UpdateWheels(){ //1  
	float delta = Time.fixedDeltaTime;  //2 
	 
	float trackRpm = CalculateSmoothRpm(leftTrackWheelData); //3		 
		 
	foreach (WheelData w in leftTrackWheelData){  //4 
		w.wheelTransform.localPosition =  CalculateWheelPosition(w.wheelTransform,w.col,w.wheelStartPos); //5 
		w.boneTransform.localPosition = CalculateWheelPosition(w.boneTransform,w.col,w.boneStartPos); //6     
		 
		w.rotation = Mathf.Repeat(w.rotation + delta * trackRpm * 360.0f / 60.0f, 360.0f);  //7 
		w.wheelTransform.localRotation = Quaternion.Euler(w.rotation, w.startWheelAngle.y, w.startWheelAngle.z); //8 			 
		
	} 
	 
	 
	leftTrackTextureOffset = Mathf.Repeat(leftTrackTextureOffset + delta*trackRpm*trackTextureSpeed/60.0f,1.0f); //9 
	leftTrack.renderer.material.SetTextureOffset("_MainTex",new Vector2(0,-leftTrackTextureOffset)); //10 
	 
	trackRpm = CalculateSmoothRpm(rightTrackWheelData);  //3 
		 
	foreach (WheelData w in rightTrackWheelData){  //4   
		w.wheelTransform.localPosition = CalculateWheelPosition(w.wheelTransform,w.col,w.wheelStartPos); //5 
		w.boneTransform.localPosition = CalculateWheelPosition(w.boneTransform,w.col,w.boneStartPos); //6 
					 
		w.rotation = Mathf.Repeat(w.rotation + delta * trackRpm * 360.0f / 60.0f, 360.0f);  //7 
		w.wheelTransform.localRotation = Quaternion.Euler(w.rotation, w.startWheelAngle.y, w.startWheelAngle.z);  //8 
		 
		
	} 
			 
	rightTrackTextureOffset = Mathf.Repeat(rightTrackTextureOffset + delta*trackRpm*trackTextureSpeed/60.0f,1.0f);  ///9 
	rightTrack.renderer.material.SetTextureOffset("_MainTex",new Vector2(0,-rightTrackTextureOffset));  //10 
	 
	for(int i=0;i<leftTrackUpperWheels.Length;i++){  //11 
	 	leftTrackUpperWheels[i].localRotation = Quaternion.Euler(leftTrackWheelData[0].rotation, leftTrackWheelData[0].startWheelAngle.y, leftTrackWheelData[0].startWheelAngle.z);  //11 
	} 
	 
	for(int i=0;i<rightTrackUpperWheels.Length;i++){  //11 
	 	rightTrackUpperWheels[i].localRotation = Quaternion.Euler(rightTrackWheelData[0].rotation, rightTrackWheelData[0].startWheelAngle.y, rightTrackWheelData[0].startWheelAngle.z);  //11 
	} 
} 
 
 
private float CalculateSmoothRpm(WheelData[] w){ //12 
	float rpm = 0.0f;  
	 
	List<int> grWheelsInd = new List<int>(); //13 
			 
	for(int i = 0;i<w.Length;i++){ //14 
		if(w[i].col.isGrounded){  //14 
			grWheelsInd.Add(i); //14 
		} 
	} 
	 
	if(grWheelsInd.Count == 0){  //15   
		foreach(WheelData wd in w){  //15 
			rpm +=wd.col.rpm;  //15				 
		} 
		 
		rpm /= w.Length; //15 
					 
	}else{  //16 
								 
		for(int i = 0;i<grWheelsInd.Count;i++){  //16 
			rpm +=w[grWheelsInd[i]].col.rpm; //16	 
		} 
		 
		rpm /= grWheelsInd.Count; //16 
	} 
	 
	return rpm; //17 
} 
 
 
private Vector3 CalculateWheelPosition(Transform w,WheelCollider col,Vector3 startPos){  //18 
	WheelHit hit; 
			 
	Vector3 lp = w.localPosition; 
	if (col.GetGroundHit(out hit)) { 
		lp.y -= Vector3.Dot(w.position - hit.point, transform.up) - wheelRadius; 
		 
	}else { 
		lp.y = startPos.y - suspensionOffset; 
					 
	} 
	 
	return lp;		 
}

  1. Будем вызывать внутри функции FixedUpdate() нашу функцию UpdateWheels() которая будет вычислять позицию и угол вращения колес.
  2. См. урок 1.
  3. Функция CalculateSmoothRpm() будет вычислять среднюю скорость вращения колес (подробнее расскажу дальше) для того чтобы они вращались с одинаковой скоростью в неё мы передаем весь массив leftTrackWheelData[], а потом массив rightTrackWheelData[].
  4. Для всех элементов массивов содержащих данные о левых и правых колесах выполняем следующие операции:
  5. Вычисляем локальную позицию колеса по оси Y, в чем нам поможет функция CalculateWheelPosition() (подробности ниже), в которую мы передаем Transform колеса, его WheelCollider, и его начальную локальную позицию;
  6. Те же самые операции, только на этот раз чтобы вычислить локальную позицию кости к которой привязана данная часть гусеницы;
  7. Вычисляем угол вращения колеса (см. урок 1), только на этот раз используем не rpm коллайдера, а среднюю скорость вращения которую вычислили ранее;
  8. Применяем вычисленный угол вращения к локальному углу поворота колеса (см. урок 1).
  9. Вычисляем смещение текстуры гусеницы. К сожалению я не нашел универсальной формулы которая позволила бы с одинаковой скоростью вращать колесо и перемещать гусеницу, поэтому пришлось ввести дополнительную переменную trackTextureSpeed (см. выше), которую позже придется подгонять вручную чтобы колесо и гусеница двигались с равномерной скоростью.
  10. Применяем смещение гусеницы к координате Y (new Vector2(0,-leftTrackTextureOffset)), главной текстуры ("_MainTex") материала который используют GO leftTrack и rightTrack.
  11. Вычисляем вращение верхних колес к которым у нас не привязаны WheelCollider’ы, можем позаимствовать угол вращения у какого нибудь другого колеса, все равно все они теперь двигаются с одинаковой скоростью.
  12. Ну наконец мы добрались до функции CalculateSmoothRpm() которая принимает первым аргументом массив типа WheelData, который в свою очередь носит оригинальное имя w.
  13. Создаем новый динамический список который будет содержать индексы тех элементов массива w, внутри которых WheelCollider col, в данный момент касается поверхности (террейна).
  14. Пробегаемся по массиву и находим индексы элементов в которых коллайдеры касаются поверхности.
  15. Если количество элементов списка равно нулю, то есть если ни один из коллайдеров не касается земли, то складываем все rpm коллайдеров внутри массива w, затем делим получившееся значение на количество элементов в массиве w, таким образом находим среднее значение.
  16. Если у нас один или более элементов списка, тоесть если один или более коллайдеров касается земли, то складываем скорости только этих коллайдеров и делим на количество элементов списка.
  17. Возвращаем получившееся значение. (Должно быть у вас возник вопрос: для чего все эти операции по вычислению коллайдеров которые касаются земли, и почему нельзя просто постоянно находить среднюю скорость вращения всех колес? Ответ – домашнее задание: подумайте (а лучше поэкспериментируйте) как тогда будут вращаться колеса в данной ситуации:
    image
  18. Функция CalculateWheelPosition() которая первым аргументом принимает Transform колеса (либо кости), вторым WheelCollider, и третьим стартовую локальную позицию колеса (либо кости). Как я уже говорил выше, данная функция вычисляет локальную позицию колеса (либо кости), и не несет в себе абсолютно ничего нового, так как данный алгоритм мы разбирали в первом уроке.

Итак теперь вы можете смело нажимать на Play, и убедиться что колеса пришли в движение, но так как наш танк еще не умеет ездить, просто переместите его на неровную поверхность. Также вы скорее всего заметили что колеса ушли под землю, не беда, вспомните, у нас же есть переменная wheelRadius, подгоните её значение не выходя из Play mode так чтобы колеса с гусеницей лежали на земле, а также значение переменной trackTextureSpeed, чтобы синхронизировать движение гусеницы с движением колес. У меня данные значения получились следующими:
image

5. Учимся ездить


Какая наверное самая главная особенность движения гусеничного транспорта которая отличает его от обычного автомобиля? Я считаю что это поворот на месте. Не верите? Смотрите. Будь у обычного автомобиля возможность разворачиваться на месте глядишь блондинки выезжающие со стоянки не так часто врезались бы в автомобиль стоящий перед ними.

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

Я предлагаю начать с самого простого и для начала научить наш танк поворачивать на месте. Тут казалось бы все понятно. И мы можем сделать так:
public float rotateOnStandTorque = 1500.0f; //1
public float rotateOnStandBrakeTorque = 500.0f; //2
public float maxBrakeTorque = 1000.0f; //3

void FixedUpdate(){ 
	float accelerate = 0;  
	float steer = 0;  
			 
	accelerate = Input.GetAxis("Vertical");  //4
	steer = Input.GetAxis("Horizontal"); //4
	 
	UpdateWheels(accelerate,steer); //5 
} 
 
 
public void UpdateWheels(float accel,float steer){ //5
	float delta = Time.fixedDeltaTime; 
	 
	float trackRpm = CalculateSmoothRpm(leftTrackWheelData);		 
		 
	foreach (WheelData w in leftTrackWheelData){ 
		w.wheelTransform.localPosition = CalculateWheelPosition(w.wheelTransform,w.col,w.wheelStartPos); 
		w.boneTransform.localPosition = CalculateWheelPosition(w.boneTransform,w.col,w.boneStartPos); 
		 
		w.rotation = Mathf.Repeat(w.rotation + delta * trackRpm * 360.0f / 60.0f, 360.0f); 
		w.wheelTransform.localRotation = Quaternion.Euler(w.rotation, w.startWheelAngle.y, w.startWheelAngle.z); 
		 
		CalculateMotorForce(w.col,accel,steer);  //6 
	} 
	 
	 
	leftTrackTextureOffset = Mathf.Repeat(leftTrackTextureOffset + delta*trackRpm*trackTextureSpeed/60.0f,1.0f); 
	leftTrack.renderer.material.SetTextureOffset("_MainTex",new Vector2(0,-leftTrackTextureOffset)); 
	 
	trackRpm = CalculateSmoothRpm(rightTrackWheelData); 
		 
	foreach (WheelData w in rightTrackWheelData){ 
		w.wheelTransform.localPosition = CalculateWheelPosition(w.wheelTransform,w.col,w.wheelStartPos); 
		w.boneTransform.localPosition = CalculateWheelPosition(w.boneTransform,w.col,w.boneStartPos); 
					 
		w.rotation = Mathf.Repeat(w.rotation + delta * trackRpm * 360.0f / 60.0f, 360.0f); 
		w.wheelTransform.localRotation = Quaternion.Euler(w.rotation, w.startWheelAngle.y, w.startWheelAngle.z); 
		 
		CalculateMotorForce(w.col,accel,-steer); //6 
	} 
			 
	rightTrackTextureOffset = Mathf.Repeat(rightTrackTextureOffset + delta*trackRpm*trackTextureSpeed/60.0f,1.0f); 
	rightTrack.renderer.material.SetTextureOffset("_MainTex",new Vector2(0,-rightTrackTextureOffset)); 
	 
	for(int i=0;i<leftTrackUpperWheels.Length;i++){ 
	 	leftTrackUpperWheels[i].localRotation = Quaternion.Euler(leftTrackWheelData[0].rotation, leftTrackWheelData[0].startWheelAngle.y, leftTrackWheelData[0].startWheelAngle.z); 
	} 
	 
	for(int i=0;i<rightTrackUpperWheels.Length;i++){ 
	 	rightTrackUpperWheels[i].localRotation = Quaternion.Euler(rightTrackWheelData[0].rotation, rightTrackWheelData[0].startWheelAngle.y, rightTrackWheelData[0].startWheelAngle.z); 
	} 
}


public void CalculateMotorForce(WheelCollider col, float accel, float steer){  //6
	 if(accel == 0 && steer == 0){ //7
		col.brakeTorque = maxBrakeTorque; //7
	}else if(accel == 0.0f){  //8
		col.brakeTorque = rotateOnStandBrakeTorque; //9
		col.motorTorque = steer*rotateOnStandTorque; //10
	} 
			 
}


  1. Крутящий момент который будем передавать на коллайдеры, когда танк стоит на месте.
  2. Тормозной момент который будем передавать на коллайдеры, когда танк стоит на месте.
  3. Максимальный тормозной момент.
  4. См. урок 1.
  5. Модифицируем функцию UpdateWheels() сделаем так чтобы она могла принимать значения с виртуальных осей.
  6. Будем использовать функцию CalculateMotorForce() для контроля крутящего и тормозного момента на коллайдерах, будем передавать в эту функцию коллайдер и виртуальные оси (обратите внимание для leftTrackWheelData мы передаем положительное значение с горизонтальной оси, а для rightTrackWheelData отрицательное, что позволит нам двигать гусеницы в разных направлениях).
  7. Если не нажата ни одна клавиша движения, то передаем коллайдеру максимальный тормозной момент (для того чтобы он не скатывался с неровных мест).
  8. Если не нажата клавиша движения вперед, но нажата клавиша движения в сторону то:
  9. Передаем коллайдеру тормозной момент.
  10. Передаем крутящий момент умноженный на значение полученное с горизонтальной оси (данный крутящий момент должен быть больше тормозного момента rotateOnStandBrakeTorque, для того чтобы танк смог двигаться).

Итак, нажимаем Play, а затем на клавишу движения в сторону (A или D). И что же мы видим, только жалкие попытки нашего танка развернуться на месте, такое ощущение что ему не хватает мощности. Можно конечно увеличить значение переменной rotateOnStandTorque но результат будет довольно забавным.

На самом деле здесь дело не в силе крутящего момента, её более чем достаточно для поворота танка. Давайте вернемся к истокам, тоесть к нашему префабу tank_collider настройки которого у нас наследуются для всех WheelCollider’ов танка. Обратите внимание на поле Sideways Friction, в предыдущем уроке я упомянул об этой настройке, я говорил что это «боковая» сила трения колеса, и она полезна если мы хотим реализовать дрифт. Сейчас эта самая сила трения которая действует на наши коллайдеры с боковых сторон неимоверно большая, крутящий момент колес не в силах её преодолеть, поэтому наш танк не поворачивается. Теперь обратите внимание на переменную Stiffness Factor внутри Sideways Friction, она та нам и нужна, по сути это число на которое умножается боковая сила трения, ставьте его в ноль, нажимайте Play.
image

О чудо наш танк сошел с ума и теперь может крутиться со скоростью волчка, да еще и ездить боком, все правильно, теперь бокового трения нету совсем. Теперь выйдите из Play mode, поставьте значение в 0.06 и снова нажмите Play. Ну наконец то, теперь наш танк поворачивается вокруг своей оси и вполне адекватно. Выходите из Play mode и установите значение Sideways Friction обратно в 1. Конечно все это хорошо менять значение прямо из префаба, но лучше это делать из скрипта, потомучто когда боковое трение не управляемо есть шанс что наш танк будет входить в самый настоящий дрифт. Заодно научим танк ехать вперед и поворачивать во время движения. Модифицируем функцию CalculateMotorForce() и объявим еще маленько глобальных переменных:
public float forwardTorque = 500.0f; //1
public float rotateOnMoveBrakeTorque = 400.0f; //2 
public float minBrakeTorque = 0.0f; //3 
public float minOnStayStiffness = 0.06f; //4 
public float minOnMoveStiffness = 0.05f;  //5 
public float rotateOnMoveMultiply = 2.0f; //6

public void CalculateMotorForce(WheelCollider col, float accel, float steer){ 
	WheelFrictionCurve fc = col.sidewaysFriction;  //7 
			 
	if(accel == 0 && steer == 0){ 
		col.brakeTorque = maxBrakeTorque; 
	}else if(accel == 0.0f){ 
		col.brakeTorque = rotateOnStandBrakeTorque; 
		col.motorTorque = steer*rotateOnStandTorque;	 
		fc.stiffness = 1.0f + minOnStayStiffness - Mathf.Abs(steer); 
		 
	}else{ //8 
		 
		col.brakeTorque = minBrakeTorque;  //9 
		col.motorTorque = accel*forwardTorque;  //10 
					 
		if(steer < 0){ //11 
			col.brakeTorque = rotateOnMoveBrakeTorque; //12 
			col.motorTorque = steer*forwardTorque*rotateOnMoveMultiply;//13 
			fc.stiffness = 1.0f + minOnMoveStiffness - Mathf.Abs(steer);  //14 
		} 
		 
		if(steer > 0){ //15 
			 
			col.motorTorque = steer*forwardTorque*rotateOnMoveMultiply;//16 
			fc.stiffness = 1.0f + minOnMoveStiffness - Mathf.Abs(steer); //17
		} 
		 
					 
	} 
	 
	if(fc.stiffness > 1.0f)fc.stiffness = 1.0f; //18		 
	col.sidewaysFriction = fc; //19
	 
	if(col.rpm > 0 && accel < 0){ //20 
		col.brakeTorque = maxBrakeTorque;  //21
	}else if(col.rpm < 0 && accel > 0){ //22 
		col.brakeTorque = maxBrakeTorque; //23
	} 
}

  1. Крутящий момент при движении (вперед, назад).
  2. Тормозной момент при повороте во время движения.
  3. Минимальный тормозной момент.
  4. Минимальное боковое трение при повороте на месте.
  5. Минимальное боковое трение при повороте во время движения.
  6. Множитель крутящего момента при повороте во время движения.
  7. Объявляем переменную fc типа WheelFrictionCurve, сохраняем в ней Sideways Friction нашего Wheel Collider’а.
  8. Если нажата клавиша движения, либо одновременно клавиша движения и клавиша поворота то:
  9. Передаем коллайдеру минимальный тормозной момент;
  10. Умножаем значение полученное с вертикально оси на максимальный крутящий момент;
  11. Если значение полученное с горизонтальной оси меньше нуля (тоесть при повороте налево операции заключенные в данное условие будут выполняться только для коллайдеров расположенных на левых опорных катках, при повороте направо, соответственно на правых) то:
  12. Передаем коллайдеру тормозной момент (тоесть при повороте налево, левые коллайдеры будут притормаживать);
  13. Передаем крутящий момент умноженный на значение полученное с вертикальной оси и умноженный на множитель крутящего момента (мало того что данные коллайдеры будут притормаживать они еще и будут стремиться вращаться в противоположную сторону, благодаря этому поворот станет более резким);
  14. Понижаем множитель боковой силы трения который сохранен в переменной fc.
  15. Если значение полученное с горизонтальной оси больше нуля (тоесть при повороте налево операции заключенные в данное условие будут выполняться только для коллайдеров расположенных на правых опорных катках, при повороте направо, соответственно на левых) то:
  16. Коллайдеры расположенные на противоположной гусенице не должны притормаживать, они будут наоборот разгоняться.
  17. Понижаем множитель боковой силы трения который сохранен в переменной fc.
  18. Проверяем не получилось ли значение множителя боковой силы трения больше единицы, если получилось, присваиваем ему единицу.
  19. Присваиваем переменной sidewaysFriction нашего коллайдера переменную fs (к сожалению нельзя сразу написать col.sidewaysFriction.stiffnes = (float)значение, компилятор будет ругаться).
  20. Если колеса вращаются вперед, а мы жмем клавишу назад то:
  21. Передаем на коллайдеры тормозной момент.
  22. Если колеса вращаются назад а мы жмем клавишу вперед то:
  23. Без комментариев.

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

В заключение скажу что это далеко не универсальный алгоритм движения танка, над ним еще работать и работать, например необходимо прописать ограничение максимальной скорости, чтобы наш танк не мог разгоняться до 300 км. в час. Я думаю если вы поймете все то что написано выше, вы без труда сможете это модифицировать. Спасибо за внимание, следующие уроки coming soon.
Теги:
Хабы:
Всего голосов 78: ↑76 и ↓2+74
Комментарии9

Публикации

Истории

Работа

Unity разработчик
10 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань