Как создать простую Tower Defense игру на Unity3D, часть вторая

  • Tutorial
Здравствуйте! Весьма надолго у меня растянулась подготовка материала (жизнь давала изрядных пинков под зад), но вот я справился и готов поделиться продолжением первой статьи с вами.

Часть первая


Неудачный тест физики

В этой части мы:
— оптимизируем код из предыдущей статьи;
— создадим объект «база» и научим её чиниться время от времени;
— добавим пушкам патроны и перезарядку;
— избавимся от «неудобной» переменной gv;

А в конце статьи вас ожидает маленький бонус :)

Всем заинтересовавшимся — добро пожаловать под долгожданный кат!


Оптимизация, багфиксы, перестановка на сцене и всё такое



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

Начнём же мы с AI скрипта пушки, изменения в котором коснулись способа расчёта расстояния, появилась обойма с патронами, и перезарядка, длящаяся указанное время:

PlasmaTurretAI.cs
using UnityEngine;

public class PlasmaTurretAI : MonoBehaviour
{
	public GameObject curTarget;
	public float towerPrice = 100.0f;
	public float attackMaximumDistance = 50.0f; //дистанция атаки
	public float attackMinimumDistance = 5.0f;
	public float attackDamage = 10.0f; //урон
	public float reloadTimer = 2.5f; //задержка между выстрелами, изменяемое значение
	public float reloadCooldown = 2.5f; //задержка между выстрелами, константа
	public float rotationSpeed = 1.5f; //множитель скорости вращения башни
	public int FiringOrder = 1; //очередность стрельбы для стволов (у нас же их 2)
	public int upgradeLevel = 0;
	public int ammoAmount = 64;
	public int ammoAmountConst = 64;
	public float ammoReloadTimer = 5.0f;
	public float ammoReloadConst = 5.0f;
	public LayerMask turretLayerMask; //в самой Unity3D создайте новый слой для мобов по аналогии с тегами и выберите его тут. Я назвал его Monster. Не забудьте выбрать его на префабе моба.

	public Transform turretHead;

	//используем этот метод для инициализации
	private void Start()
	{
		turretHead = transform.Find("pushka"); //находим башню в иерархии частей модели
	}

	//а этот метод вызывается каждый фрейм
	private void Update()
	{
		if (curTarget != null) //если переменная текущей цели не пустая
		{
			float squaredDistance = (turretHead.position - curTarget.transform.position).sqrMagnitude; //меряем дистанцию до нее
			if (Mathf.Pow(attackMinimumDistance, 2) < squaredDistance && squaredDistance < Mathf.Pow(attackMaximumDistance, 2)) //если дистанция больше мертвой зоны и меньше дистанции поражения пушки
			{
				turretHead.rotation = Quaternion.Lerp(turretHead.rotation, Quaternion.LookRotation(curTarget.transform.position - turretHead.position), rotationSpeed * Time.deltaTime); //вращаем башню в сторону цели
				if (reloadTimer > 0) reloadTimer -= Time.deltaTime; //если таймер перезарядки больше нуля - отнимаем его
				if (reloadTimer <= 0)
				{
					if (ammoAmount > 0) //пока есть порох в пороховницах
					{
						MobHP mhp = curTarget.GetComponent<MobHP>();
						switch (FiringOrder) //смотрим, из какого ствола стрелять
						{
							case 1:
								if (mhp != null) mhp.ChangeHP(-attackDamage); //наносим урон цели
								FiringOrder++; //переключаем ствол
								ammoAmount--; //минус патрон
								break;
							case 2:
								if (mhp != null) mhp.ChangeHP(-attackDamage);
								FiringOrder = 1;
								ammoAmount--;
								break;
						}
						reloadTimer = reloadCooldown; //возвращаем переменной таймера перезарядки её первоначальное значение из "константы"
					}
					else
					{
						if (ammoReloadTimer > 0) ammoReloadTimer -= Time.deltaTime;
						if (ammoReloadTimer <= 0)
						{
							ammoAmount = ammoAmountConst;
							ammoReloadTimer = ammoReloadConst;
						}
					}
				}
				if (squaredDistance < Mathf.Pow(attackMinimumDistance, 2)) curTarget = null;//сбрасываем с прицела текущую цель, если она вне радиуса атаки
			}
		}
		else
		{
			curTarget = SortTargets(); //сортируем цели и получаем новую
		}
	}

	//Модифицированный алгоритм поиска ближайшей цели
	private GameObject SortTargets()
	{
		float closestMobSquaredDistance = 0; //переменная для хранения квадрата расстояния ближайшего моба
		GameObject nearestmob = null; //инициализация переменной ближайшего моба
		Collider[] mobColliders = Physics.OverlapSphere(transform.position, attackMaximumDistance, turretLayerMask.value); //находим коллайдеры всех мобов в радиусе максимальной дальности атаки и создаём массив для сортировки

		foreach (var mobCollider in mobColliders) //для каждого коллайдера в массиве
		{
			float distance = (mobCollider.transform.position - turretHead.position).sqrMagnitude;
			//если дистанция до моба меньше, чем closestMobDistance или равна нулю
			if (distance < closestMobSquaredDistance && (distance > Mathf.Pow(attackMinimumDistance, 2)) || closestMobSquaredDistance == 0)
			{
				closestMobSquaredDistance = distance; //записываем её в переменную
				nearestmob = mobCollider.gameObject;//устанавливаем моба как ближайшего
			}
		}
		return nearestmob; // и возвращаем его
	}

	private void OnGUI()
	{
		Vector3 screenPosition = Camera.main.WorldToScreenPoint(gameObject.transform.position); //Находим позицию объекта на экране относительно мира
		Vector3 cameraRelative = Camera.main.transform.InverseTransformPoint(transform.position); //Получаем дальность объекта от камеры
		if (cameraRelative.z > 0) //если объект находится впереди камеры
		{
			string ammoString;
			if (ammoAmount > 0)
			{
				ammoString = ammoAmount + "/" + ammoAmountConst;
			}
			else
			{
				ammoString = "Reloading: " + (int)ammoReloadTimer + " s left";
			}
			GUI.Label(new Rect(screenPosition.x, Screen.height - screenPosition.y, 250f, 20f), ammoString);
		}
	}
}


Как видно, тут используется расчёт через квадрат расстояния и сравнение его с квадратом максимальной дистанции для пушки. Это работает быстрее, т.к. не используется Sqrt. Спасибо Leopotam за совет :)

Следующим шагом приведём сцену примерно в следующий вид:



Красными точками я обозначил места спаунпойнтов, по центру у меня находится «база» в виде стандартного максовского чайника :)



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

BaseHP.cs
using UnityEngine;
 
public class BaseHP : MonoBehaviour
{
   public float maxHP = 1000;
   public float curHP = 1000;
   public float regenerationDelayConstant = 2.5f; //константа задержки между регенерацией хп базы
   public float regenarationDelayVariable = 2.5f; //переменная той же задержки
   public float regenerationAmount = 10.0f; //количество восстанавливаемого хп при регенерации за раз
 
   private GlobalVars gv;
 
   private void Awake()
   {
      gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>();
      if (maxHP < 1) maxHP = 1;
   }
 
   public void ChangeHP(float adjust)
   {
      if ((curHP + adjust) > maxHP) curHP = maxHP;
      else curHP += adjust;
      if (curHP > maxHP) curHP = maxHP; //just in case
   }
 
   private void Update()
   {
      if (curHP <= 0)
      {
         Destroy(gameObject);
      }
      else
      {
         if (regenarationDelayVariable > 0) regenarationDelayVariable -= Time.deltaTime; //если переменная задержки более нуля - отнимаем от неё единицу в секунду
         if (regenarationDelayVariable <= 0) //если она стала меньше или равна нулю
         {
            ChangeHP(regenerationAmount); //восстанавливаем ранее указанное количество ХП
            regenarationDelayVariable = regenerationDelayConstant; //и возвращаем нашу переменную в её первоначальное значение
         }
      }
   }
}


Вешаем скрипт на наш объект с базой. Она готова, можно приступить к переобучению мобов!

В скрипте AI мобов изменению подлежит только метод Update, потому остальной код приводить не буду:

MobAI.cs
private void Update()
   {
      if (Target != null)
      {
         mob.rotation = Quaternion.Lerp(mob.rotation, Quaternion.LookRotation(new Vector3(Target.transform.position.x, 0.0f, Target.transform.position.z) - new Vector3(mob.position.x, 0.0f, mob.position.z)), mobRotationSpeed);
         mob.position += mob.forward * MobCurrentSpeed * Time.deltaTime;
         float squaredDistance = (Target.transform.position - mob.position).sqrMagnitude; //меряем дистанцию до цели
         Vector3 structDirection = (Target.transform.position - mob.position).normalized;
         float attackDirection = Vector3.Dot(structDirection, mob.forward);
         if (squaredDistance < attackDistance * attackDistance && attackDirection > 0)
         {
            if (attackTimer > 0) attackTimer -= Time.deltaTime;
            if (attackTimer <= 0)
            {
               BaseHP bhp = Target.GetComponent<BaseHP>(); //подключаемся к компоненту HP цели
               if (bhp != null) bhp.ChangeHP(-damage); // отнимаем от её HP наш урон
               attackTimer = coolDown;
            }
         }
      }
      else
      {
         GameObject baseGO = GameObject.FindGameObjectWithTag("Base"); //находим наш объект с базой, он всего один
         if (baseGO != null) Target = baseGO; //если она существует - делаем её целью для моба.
      }
   }


Всё хорошо, мобы ползут кусать базу, пушки методично отстреливают нахалов. Но камера-то статичная! Непорядок, исправляем:

CameraControl.cs
using UnityEngine;
 
public class CameraControl : MonoBehaviour
{
   public float CameraSpeed = 100.0f; //Скорость движения камеры
   public float CameraSpeedBoostMultiplier = 2.0f; //Множитель ускорения движения камеры при зажатом Shift
 
   //Задаём позицию по умолчанию для камеры, здесь выставлена моя - меняйте под себя
   public float DefaultCameraPosX = 888.0f;
   public float DefaultCameraPosY = 50.0f;
   public float DefaultCameraPosZ = 1414.0f;
 
   private void Awake()
   {
      //Задаём позицию по умолчанию для камеры, используя ранее указанные координаты
      transform.position = new Vector3(DefaultCameraPosX, DefaultCameraPosY, DefaultCameraPosZ);
   }
 
   private void Update()
   {
      float smoothCamSpeed = CameraSpeed * Time.smoothDeltaTime; //множим скорость перемещения камеры на сглаженную версию Time.deltaTime
 
      //При нажатии какой-либо из кнопки из WASD происходит перемещение в соответствующую сторону, нажания сразу двух кнопок также обрабатываются (WA будет двигать камеру вверх и влево), зажатие Shift при этом ускоряет передвижение.
      if (Input.GetKey(KeyCode.W)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(0.0f, 0.0f, smoothCamSpeed * CameraSpeedBoostMultiplier) : new Vector3(0.0f, 0.0f, smoothCamSpeed); //вверх
      if (Input.GetKey(KeyCode.A)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(-smoothCamSpeed * CameraSpeedBoostMultiplier, 0.0f, 0.0f) : new Vector3(-smoothCamSpeed, 0.0f, 0.0f); //налево
      if (Input.GetKey(KeyCode.S)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(0.0f, 0.0f, -smoothCamSpeed * CameraSpeedBoostMultiplier) : new Vector3(0.0f, 0.0f, -smoothCamSpeed); //вниз
      if (Input.GetKey(KeyCode.D)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(smoothCamSpeed * CameraSpeedBoostMultiplier, 0.0f, 0.0f) : new Vector3(smoothCamSpeed, 0.0f, 0.0f); //направо
   }
}


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

Следующий багфикс состоит в том, что мы можем покупать пушки «в кредит». Да, нужна простая проверка денег игрока и стоимости пушки. Правим это дело:
Graphic.cs
private void OnGUI()
   {
      GUI.Box(buyMenu, "Buying menu"); //Делаем гуевский бокс на квадрате buyMenu с заголовком, указанным между ""
      if (GUI.Button(firstTower, "Plasma Tower\n" + (int)TowerPrices.Plasma + "$")) //если идёт нажатие на первую кнопку
      {
         if (gv.PlayerMoney >= (int)TowerPrices.Plasma) //если у игрока достаточно денег
            gv.mau5tate = GlobalVars.ClickState.Placing; //меняем глобальное состояние мыши на "Установка пушки"
      }
      if (GUI.Button(secondTower, "Pulse Tower\n" + (int)TowerPrices.Pulse + "$")) //с остальными аналогично
      {
         //same action here
      }
      if (GUI.Button(thirdTower, "Beam Tower\n" + (int)TowerPrices.Beam + "$"))
      {
         //same action here
      }
      if (GUI.Button(fourthTower, "Tesla Tower\n" + (int)TowerPrices.Tesla + "$"))
      {
         //same action here
      }
      if (GUI.Button(fifthTower, "Artillery Tower\n" + (int)TowerPrices.Artillery + "$"))
      {
         //same action here
      }
 
      GUI.Box(playerStats, "Player Stats");
      GUI.Label(playerStatsPlayerMoney, "Money: " + gv.PlayerMoney + "$");
 
      GUI.Box(towerMenu, "Tower menu");
      if (GUI.Button(towerMenuSellTower, "Sell"))
      {
         //action here
      }
      if (GUI.Button(towerMenuUpgradeTower, "Upgrade"))
      {
         //same action here
      }
   }
 
   //цены на пушки
   private enum TowerPrices
   {
      Plasma = 100,
      Pulse = 150,
      Beam = 250,
      Tesla = 300,
      Artillery = 350
   }


Далее, уже после написания всего предыдущего кода, я избавился от объекта GlobalVars, сделав его и все его переменные static.

GlobalVars.cs
using System.Collections.Generic;
using UnityEngine;

public static class GlobalVars
{
	public static List<GameObject> MobList = new List<GameObject>(); //массив мобов в игре
	public static int MobCount = 0; //счетчик мобов в игре

	public static List<GameObject> TurretList = new List<GameObject>(); //массив пушек в игре
	public static int TurretCount = 0; //счетчик пушек в игре

	public static float PlayerMoney = 200.0f; //при старте игры, если нету сохранённых данных про деньги игрока - их становится 200$, иначе загружается из памяти

	public static ClickState mau5tate = ClickState.Default; //дефолтное состояние курсора

	public enum ClickState //перечисление всех состояний курсора
	{
		Default, //обычное
		Placing, //устанавливаем пушку
		Selling, //продаём пушку
		Upgrading //улучшаем пушку
	}
}


Во всех классах, где использовался GlobalVars, удаляем переменные gv, их инициализацию в Awake(). Заменяем все gv на GlobalVars. Удаляем бесполезные проверки GlobalVars на null. Удаляем компонент GlobalVars из одноимённого ГО (можно сам ГО переименовать во что-то информативное, например, cfg).
Я приведу полные листинги классов с изменениями, чтобы вам было, с чем сравнить результат этой операции.

Осторожно, спойлеры к следующей части! :)

bitbucket.org/andyion/habratd-tutorial/commits/db7c1bc0c10c89f45be187e59e0608a2fbb3083d

На этом замена завершена.
Следующим моментом добавлю маленький бонус, который значительно облегчит жизнь при регулировке дальности атаки как пушкам, так и мобам: bitbucket.org/andyion/habratd-tutorial/commits/18ec053f5f5697abbd3598890aa40306e038d472

Как использовать: надеваете скрипт на объект и в инспекторе регулируете дальность. Вокруг ГО при выделении появится желтый круг, это и есть указанная дальность.

Заключение

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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 41

    +4
    За материал спасибо, но размещать 10 листингов кода без каких-то пояснений — не самый хороший тон. Если нужно просто поделиться кодом, если github, bitbucket или что-нибудь еще. Если нужно пояснить какой-то участок, в этом случае его нужно вставить в статью. В конечном итоге, если кто-то что-то захочет проверить, придется копипастить все со статьи, собирать самому проект и т.д.
      +1
      Ваша правда, не подумал об этом. Залил на ведёрко.
      +3
      Это работает быстрее, т.к. не используется Sqrt.

      Mathf.Pow(attackMinimumDistance, 2)

      Матерь божья… Т.е. извлекать квадратный корень — это плохо, а возводить в степень — это нормально? :) Надо просто писать как
      attackMinimumDistance * attackMinimumDistance
        0
        Если честно, не заметил разницы под профайлером. И то, и то считается примерно за 0.01 мсек.
          +1
          Десктопный профайлер и дохлая мобильная железка, например, это разные вещи, об этом нужно думать наперед. :) Возможно это оптимизация компилятором и там генерируется код для умножения, но не факт, что под все платформы оно будет срабатывать именно так, а не использовать медленный FPU.
            0
            Я не разрабатываю ПО для мобильных устройств, потому и не задумываюсь об этих платформах. Предполагается, что разработчик мобильных игр весьма неплохо шарит в оптимизации :)
              0
              Да б-г с ним с этим перемножением, темболее, как написали ниже, Math.Pow (а в него идёт вызов из Mathf.pow, как и из многих других методов Mathf) шустрее, а там, вроде-как, методы extern (т.е. реализованы нативно). У него там всё кишит GetComponent'ами. Даже выборка из словаря по какому-нибудь ключу работает быстрее, чем GetComponent. Плюс, обращение к transform и т.д. это вроде-как тоже GetComponent. Автору — вместо этого всего, используйте ссылки на нужные обьекты класса ИИ (а не геймобьекта, вы все-равно ведь назначаете это поле!), а вместо FindGameObjectsWithTag используйте статическую коллекцию. Зачем, чёрт возьми, что-то искать, когда вы можете сохранить ссылки на них в коллекции? Подход «декстопы мощные, всё будет хорошо» работает ровно до тех пор, пока сцена является вот таким вот демо с парой обьектов.
                0
                там, вроде-как, методы extern (т.е. реализованы нативно)

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

                Ну однократный поиск объектов по тегу в Awake не так страшен, если инстансов таких компонентов не сотни. Но таки да, об этом нужно думать.
            0
            Math.Pow() в C#, емнип, работает быстрее, чем простое перемножение. Сам удивился, когда услышал.
              0
              И в моно тоже, на всех версиях?
                +1
                Это далеко не так. Давным-давно делал скрин участка кода на память, производительность вырасла просто в разы. (из какого то класса к графическому движку, который часто вызывался)
              +5
              Еще такая штука. Постоянно используется запрос к трансформу GameObject-а (ГО), типа такого:
              Target.transform.position

              Так делать не стоит, transform — это просто хелпер для поиска компонента Transform среди всех компонентов на ГО и его возврата каждый раз. Т.е. надо закешировать трансформ в локальную переменную и использовать ее во всем методе. То же самое можно сказать про transform.position, transform.forward / up / right — все эти хелперы вычисляются каждый раз при обращении к конкретному get-еру по всей иерархии ГО. Лечение аналогичное — кеширование хотя бы на уровне метода.
                0
                Именно так я и делал в одном из своих проектов.

                private Transform myTransform;
                
                private void Awake()
                {
                	myTransform = transform;
                }
                
                  0
                  Вы уверены насчет position, forward и прочих? Насколько я понял, transform — это одно из полей, которые были созданы для упрощение получения доступа к наиболее часто используемым компонентам. Однако position и подобные уже не являются компонентами, следовательно поиск по иерархии тут неуместен, на мой, не очень осведомленный, взгляд.
                    0
                    Во-первых, transform (с маленькой буквы) это свойство.
                    Во-вторых, называть Transform (с большой буквы, класс) компонентом для доступа к другим — дичайшее кощунство. Transform олицетворяет собой матрицу трансформации и удобный интерфейс к ней.
                    В третьих, эти «часто используемые свойства» на самом деле обьявлены в UnityEngine.Component, а UnityEngine.Transform только наследует их. В Transform обьявлены только следующие свойства:

                    public int childCount { get; }
                    public Vector3 eulerAngles { get; set; }
                    public Vector3 forward {get; set; }
                    public Vector3 localEulerAngles {get;set;}
                    public Vector3 localPosition { get;set; }
                    public Quaternion localRotation { get;set; }
                    public Vector3 localScale{get;set;}
                    public Matrix4x4 localToWorldMatrix{get; }
                    public Vector3 lossyScale{get;}
                    public Transform parent{ get;set;}
                    public Vector3 position{ get;set;}
                    public Vector3 right{get;set;}
                    public Transform root{get;}
                    public Quaternion rotation{get;set;}
                    public Vector3 up{get;set; }
                      0
                      Боюсь, вы меня неправильно поняли, и, к сожалению, ответили на все кроме моего вопроса.
                      Я не говорил что Transform это компонент для доступа к другим — я лишь сказал что это компонент. Но в то же время, это, само собой разумеется, и класс тоже.
                      Так же, по-прежнему, свойство transform (именно свойство, не сам класс) было создано для того, чтобы не использовать GetComponent(Transform), насколько я понимаю. Правда, я не знаю, быстрее ли доступ к компоненту через это свойство (кешируется ли этот компонент?), или же это было сделано просто для более короткой и удобной записи.

                      Мой вопрос: сопоставима ли разница во времени при получении доступа к компоненту через свойство по отношению к получению доступа через поиск (GetComponent) и получение доступа к свойству компонента напрямую по отношению к получению через кешированную переменную — например:

                      myObject.GetComponent(Transform);
                      // по отношению к 
                      myObject.transform;
                      
                      // и
                      
                      var t : Transform = myObject.transform;
                      ...
                      t.position.Set(4, 8, 15);
                      
                      // по отношению к
                      
                      var p : Vector3 = myObject.transform.position;
                      ...
                      p.Set(4, 8, 15);
                      
                        +1
                        Первый вариант — абсолютно идентично. Второй вариант — нерабочий, Vector3 — это MarshalByValue, т.е. вернется копия (структура) положения, а не указатель на инстанс.
                          0
                          Тогда что имел в виду 6opoDuJIo, написав выше
                          То же самое можно сказать про transform.position, transform.forward / up / right (...) Лечение аналогичное — кеширование хотя бы на уровне метода.
                          ?
                          Или это касается только unity-c#?
                            +1
                            Вроде как это написал я :) transform.forward и прочие глобальные вектора — это не просто переменные, это пары Getter / Setter, выполняющие кучу вычислений при каждом запросе. Пример:
                            public Vector3 forward {
                                get {
                                    return localToWorldMatrix.MultiplyPoint(Vector3.forward);
                                }
                                set {
                                // Расчет обратной операции с коррекцией всего трансформа
                                }
                            }
                            

                            Т.е. если в своем методе постоянно обращаться напрямую к transform.forward, то это будет двойной фейл:
                            1. Обращение к transform. Надо сделать локальную переменную, в которую сделается один запрос:
                            var cachedTransform = transform;
                            // bla-bla-bla with cachedTransform
                            

                            2. Обращение к forward. Надо сделать локальную переменную, в которую сделается один запрос:
                            var forward = cachedTransform.forward;
                            
                            // bla-bla-bla with forward
                            

                            Так понятнее?

                            Или это касается только unity-c#?

                            UnityScript транслируется в итоге все-равно в MSIL, причем местами ужасного качества (часть реализации скрывается удобными обертками, раздувающими код и снижающими скорость работы). Т.е. это не C#-фишка, это фишка .net-платформы.
                              +1
                              Пример обертки на UnityScript, скрывающей саму суть свойства:
                              transform.position.x = 10;
                              

                              Так можно писать, ибо внутри оно после трансляции соберется во что-то типа такого:
                              var pos = transform.position;
                              pos.x = 10;
                              transform.position = pos;
                              

                              Т.е. для программера на UnityScript невдомек, что есть какое-то различие между свойством и полем — он может менять что захочет, компилятор UnityScript нафигачит кучу оберток и все будут довольны.
                                +1
                                Понятно, спасибо за прояснение. И извините что спутал автора ^_^"
                                  0
                                  Умножение идёт на кватернион а не на матрицу — смотрите декомпил.
                                    0
                                    Абсолютно все-равно, что на что умножается. Суть в том, что это не бесплатно и внутреннего кеширования нет, т.к. ручной кеш дает хороший прирост производительности в любом случае.
                                    0
                                    >1. Обращение к transform. Надо сделать локальную переменную, в которую сделается один запрос:
                                    Всё верно. Тут всё достаточно понятно.
                                    В своём коде я добавил поле public Transform tr; в базовый класс для всех объектов, и инициализирую его в Awake(). Сначала сделал его приватным, но потом оказалось, что лучше его сделать публичным. И теперь у всех объектов есть уже инициализированное поле tr с кэшированной ссылкой на компонент. Удобно.

                                    >2. Обращение к forward. Надо сделать локальную переменную, в которую сделается один запрос:
                                    А это верно до тех пор, пока не поменяем rotation. Тогда transform.forward будет показывать на новое направление, а старое закэшированное значение
                                    >var forward = cachedTransform.forward;
                                    будет всё ещё показывать на старое направление. Но само обращение к cachedTransform.forward будет верным.
                                0
                                >>Я не говорил что Transform это компонент для доступа к другим
                                Постом выше:
                                >>Насколько я понял, transform — это одно из полей, которые были созданы для упрощение получения доступа к наиболее часто используемым компонентам.
                                И это:
                                >>Правда, я не знаю, быстрее ли доступ к компоненту через это свойство (кешируется ли этот компонент?)
                                Насколько мне известно, нет, компонент не кешируется. Мобильным разработчикам обычно советуют избегать обращения к свойствам и GetComponent именно поэтому.
                                >>Мой вопрос: сопоставима ли разница во времени при получении доступа к компоненту через свойство по отношению к получению доступа через поиск (GetComponent) и получение доступа к свойству компонента напрямую по отношению к получению через кешированную переменную
                                Обращение по ссылке — меньше одного тика, т.е. меньше 100 наносекунд. Не думаю что GetComponent быстрее.
                              0
                              Вот смотрите, если ГО, для которого надо узнать позицию в глобальных координатах, имеет 10 родителей, каждый из которых имеет свой трансформ, то для получения конечного трансформа нужно перемножить все матрицы по иерархии для получения конечной, по которой надо будет трансформировать локльную нулевую точку:
                              // Это псевдокод, реально не проверялся, но идея та же.
                              
                              Matrix4x4 GetLocalToWorldMatrix() {
                                  var lastParent = transform;
                                  var mat = Matrix4x4.SetTRS(lastParent.localPosition, lastParent.localRotation, lastParent.localScale);
                                  while (lastParent != transform.parent) {
                                      var parent = transform.parent;
                                      mat = Matrix4x4.SetTRS(parent.localPosition, parent.localRotation, parent.localScale) * mat;
                                      lastParent = parent;
                                  }
                              }
                              Vector3 GetPosition() {
                                  return GetLocalToWorldMatrix().MultiplyPoint(transform.localPosition);
                              }
                              
                              Vector3 GetForward() {
                                  return GetLocalToWorldMatrix().MultiplyPoint(Vector3.forward) - GetLocalToWorldMatrix().MultiplyPoint(transform.localPosition);
                              }
                              

                              Теоретически, матрица должна кешироваться и считаться один раз, но ведь иерархию можно менять по 10 раз за фрейм, как и любой трансформ в иерархии и если каждый раз пересчитывать для всех дочерних объектов на автомате, то никаких мощностей не хватит, так что вряд ли оно кешируется. Т.е. есть уклон в сторону гибкости общей системы ценой потенциального падения скорости при незнании математики расчетов — все отдано на откуп пользователю.
                                0
                                Ступил :)
                                Vector3 GetPosition() {
                                    return GetLocalToWorldMatrix().MultiplyPoint(Vector3.zero);
                                }
                                
                                Vector3 GetForward() {
                                    return GetLocalToWorldMatrix().MultiplyPoint(Vector3.forward) - GetPosition();
                                }
                                
                                  0
                                  К чему это?
                                  Внутренние значения rotation и position все-равно назначаются (и получаются) где-то в нативном коде. Но по идее, считаться должна только локальная матрица. По идее так: При рендере — локальная матрица умножается на матрицы всех обьектов выше по иерархии. Но можно и без этого, конечно, можно перемножить ещё при изменении иерархии. А изменение иерархии 10 раз за фрейм — это пшик. Плюс, не забывайте что эти значения (позиция и прочее) могут быть перерассчитаны только при обращении к ним.
                                  p.s. Transform — это компонент, и вы его не конструируете из матрицы, это самостоятельный класс.
                                    0
                                    Transform — это компонент, и вы его не конструируете из матрицы, это самостоятельный класс.

                                    Я просто показал математику, как оно может считаться.

                                    По идее так: При рендере — локальная матрица умножается на матрицы всех обьектов выше по иерархии.

                                    Нельзя — при апдейте оно уже должно быть актуальным для корректных расчетов юзвера, в этом и вся проблема с отсутствием встроенного кеша. Я могу перекраивать иерархию сотни ГО хоть каждый фрейм — это тяжело пересчитывать при изменении каждого Transform.parent.

                                    Причем во время рендера оно либо еще раз пересчитывается, либо использует последние использованные данные после LastUpdate — это нужно для быстрого отсечения по Renderer.Bounds до посылки на визуализацию. Если поменять трансформы во время рендера, то они применятся на следующий фрейм, что говорит о кешировании данных на время рендера.
                                      0
                                      LastUpdate -> LateUpdate, my bad.
                                        0
                                        Ну и вдогонку — при батчинге юнити собирает свой внутренний меш (видно по измененным координатам, приезжающим в шейдер), собираемый как раз при анализе потенциально видимых Renderer-ов.
                                          0
                                          >> при апдейте оно уже должно быть актуальным для корректных расчетов юзвера
                                          Значение позиции, вращения, значения localScale и lossyScale, Renderer.bounds инкапсулируются соответствующими им свойствами. Что происходит внутри — пёс его знает, ибо extern и [WrapperlessIcall].
                                          К комменту выше — Transform.forward является произведением вращения rotation на Vector3.forward.
                                            0
                                            Transform.forward является произведением вращения rotation на Vector3.forward.

                                            Ну так ведь rotation тоже надо посчитать и кешировать внутри нельзя — юзверь может двигать / поворачивать любой ГО из иерархии. Вполне возможно, что считается только LocalToWorld матрица, из которой уже декомпозицией извлекаются глобальные position, rotation (с переводом в кватернион), lossyScale.
                                  +2
                                  Когда будет готово целиком, опубликуйте ссылку на скачивание.
                                  К слову, старенькую TD Гений Обороны от Alawar считаю одной из лучших среди всех Tower Defence.
                                    +1
                                    //Задаём позицию по умолчанию для камеры, здесь выставлена моя — меняйте под себя
                                    public float DefaultCameraPosX = 888.0f;
                                    public float DefaultCameraPosY = 50.0f;
                                    public float DefaultCameraPosZ = 1414.0f;

                                    Лучше будет сделать одно поле
                                    public Transform initialPosition;

                                    куда в редакторе выставить какой-нибудь объект-пустышку, служащий отправной точкой для камеры. Никаких магических чисел, всё наглядно и меняется мышкой в случае чего.
                                      0
                                      private void Update()
                                      {
                                      float smoothCamSpeed = CameraSpeed * Time.smoothDeltaTime; //множим скорость перемещения камеры на сглаженную версию Time.deltaTime

                                      Насколько я понял, камера движется с постоянной скоростью. Обычно эффектнее выглядит плавное перемещение, для которого удобно использовать Vector3.SmoothDamp. Только с плавностью не надо перебарщивать, чтобы не выбесить игрока :)
                                        +2
                                        Вот так бывает, выложил человек урок с целью научить других, а в итоге его самого учат best practices :)
                                          0
                                          Просто возможностей и путей по оптимизации в Unity3D очень много, я всех не знаю :)
                                            0
                                            Да я не в пику вам, просто отметил. Когда я пару лет назад тут пытался написать серию туториалов, такого фидбэка не было, хоть там и было к чему придраться. Растёт аудитория девелоперов на Юнити, вырабатываются эти самые best practices, приятно это видеть.
                                          0
                                          Для тех кто заинтересовался оптимизацией очень советую прочитать и проверить используете ли вы следующие 50 советов 50 Tips for Working with Unity (Best Practices)/. Очень помогло мне когда пришло время оптимизировать проект на Unity3d для мобилок особенно для андроида.
                                            0
                                            Магический костыль для кеширования transform. Меняем MonoBehaviour на CachingMonoBehaviour, и обращение к GetComponent при доступе к transform будет происходить всего один раз лениво:
                                            public class CachingMonoBehaviour : MonoBehaviour {
                                            
                                                // Transform
                                                private Transform _cachedTransform;
                                                private bool _cachedTransformIsSet;
                                            
                                                public new Transform transform {
                                                    get {
                                                        if (!_cachedTransformIsSet) {
                                                            _cachedTransform = GetComponent<Transform>();
                                                            _cachedTransformIsSet = true;
                                                        }
                                                        return _cachedTransform;
                                                    }
                                                }
                                            
                                            }
                                            

                                            Only users with full accounts can post comments. Log in, please.