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

в 16:56, , рубрики: game development, tower defense, unity3d, детский сад, урок, метки: , , ,

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

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

Первые шаги

Базовая сцена

Пришло время создать поверхность, на которой мы, собственно, будем располагать наши пушки, мобов и прочее. Жмём Terrain -> Create Terrain. А давайте заодно его и раскрасим? Выделяем террейн в объектах, далее нажимаем в свойствах кнопку с изображением кисти, чуть ниже жмём Edit textures -> Add texture.
Появится окно добавления текстуры, с правой части первой строки есть маленький кружочек, это прямой аналог кнопки Browse, нажимаем на него и выбираем текстуру для нашей поверхности).
Как создать простую Tower Defense игру на Unity3D, часть первая
Как создать простую Tower Defense игру на Unity3D, часть первая
Закрываем окно выбора текстур, возвращаемся к окну выбора текстур, нажимаем в нём кнопку Apply. Вуаля, наш террейн окрасился в выбранную нами текстуру. Можно таким же образом добавить ещё текстур и раскрасить его более детально. Поэкспериментируйте с кнопками, создайте немного горок, ям и так далее ;)

Добавляем пушку

Наигрались с террейном? Отлично, время серьёзных игрушек: качаем нашу единственную пушку (5Mb, ссылка)
Распаковываем архив в папку Assets проекта или в окно Project в редакторе, эффект будет одинаковым.
Также нам понадобится создать ещё одну папку: в том же Project в верхней части есть кнопка Create. Жмём её, выбираем Folder. Называем папку prefabs. Кликаем правой кнопкой по этой папке и выбираем Create -> Prefab. И имя ему будет gun_prefab.

Немного теории: Префаб (Prefab) — такой специальный вид объекта, который содержит в себе другой(ие) объект(-ы) со своими настройками, а также их можно будет быстро спаунить на сцене (и нам это пригодится в дальнейшем). Префабы в списке объектов сцены имеют синий цвет.

Нажмите на файл пушки (cannon2) — в инспекторе свойств загрузятся свойства плагина импорта пушки.
Установите значения следующим образом:
Scale Factor поставим равным 0.1
Отметим галочкой Generate Colliders
Остальное не трогаем, чуть ниже жмём кнопочку Apply.

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

В окне Project перетащим нашу пушку (cannon2) на свежесозданный префаб (gun_prefab).

Теперь можно перетаскивать наш префаб прямо на сцену — пушка будет клонироваться, но при этом каждый новый экземпляр будет являться отдельным объектом. Попробуйте! А когда наиграетесь — удалите все пушки со сцены (выделите их и нажмите Delete).

Создаём мобов

У нас простейшая ТД, потому и мобы у нас будут простыми шариками. Создать их, не прибегая к 3D-моделированию, проще простого.
Жмите GameObject -> Create Other -> Sphere. На сцене и в инспекторе объектов появится объект с именем Sphere. Уже известным способом создайте для него префаб с именем monster01 и перетащите нашего монстра на префаб. После этого можно удалить монстра со сцены, он там более не понадобится, т.к. мы будем спаунить его прямо из кода.

Карты, деньги, два ствола

Перетащим префаб пушки прямо на сцену из Project, и поставим в любом удобном месте (да, потом спаун будет реализован по-другому, но для начала и так пойдет). Время написать AI пушки!
Создайте в Проекте папку scripts, а в ней папку ai. Затем ПКМ по папке ai и выбираем Create -> C# Script. Скрипт назовём PlasmaTurretAI.
Открываем его даблкликом, загрузится ваша IDE с данным скриптом, который будет представлять из себя вот такой каркас для скриптования:
PlasmaTurretAI.cs

using UnityEngine;
using System.Collections;

public class PlasmaTurretAI : MonoBehaviour //Имя класса ОБЯЗАТЕЛЬНО должно совпадать с именем файла, а наследование от MonoBehaviour необходимо для возможности "натянуть" скрипт на любой GameObject.
{
   //используем этот метод для инициализации
   void Start ()
   {
   
   }
   
   //а этот метод вызывается каждый фрейм
   void Update ()
   {
   
   }
}

А теперь, собственно, сам код AI в комментариях:
PlasmaTurretAI.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//Имя класса ОБЯЗАТЕЛЬНО должно совпадать с именем файла, а наследование от MonoBehaviour необходимо для возможности "натянуть" скрипт на любой GameObject (ну и не только для этого).
public class PlasmaTurretAI : MonoBehaviour
{
   public GameObject[] targets; //массив всех целей
   public GameObject curTarget;
   public float attackMaximumDistance = 50.0f; //дистанция атаки
   public float attackMinimumDistance = 5.0f;
   public float attackDamage = 10.0f; //урон
   public float reloadTimer = 2.5f; //задержка между выстрелами, изменяемое значение
   public const float reloadCooldown = 2.5f; //задержка между выстрелами, константа
   public float rotationSpeed = 1.5f; //множитель скорости вращения башни
   public int FiringOrder = 1; //очередность стрельбы для стволов (у нас же их 2)

   public Transform turretHead;

   public RaycastHit Hit;

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

   //а этот метод вызывается каждый фрейм
   private void Update()
   {
      if (curTarget != null) //если переменная текущей цели не пустая
      {
         float distance = Vector3.Distance(turretHead.position, curTarget.transform.position); //меряем дистанцию до нее
         if (attackMinimumDistance < distance && distance < attackMaximumDistance) //если дистанция больше мертвой зоны и меньше дистанции поражения пушки
         {
            turretHead.rotation = Quaternion.Slerp(turretHead.rotation, Quaternion.LookRotation(curTarget.transform.position - turretHead.position), rotationSpeed * Time.deltaTime); //вращаем башню в сторону цели
            if (reloadTimer > 0) reloadTimer -= Time.deltaTime; //если таймер перезарядки больше нуля - отнимаем его
            if (reloadTimer < 0) reloadTimer = 0; //если он стал меньше нуля - устанавливаем его в ноль
            if (reloadTimer == 0) //став нулем
            {
               switch (FiringOrder) //смотрим, из какого ствола стрелять
               {
                  case 1:
                     Debug.Log("Стреляет первый ствол"); //пишем в консоль
                     FiringOrder++; //увеличиваем FiringOrder на 1
                     break;
                  case 2:
                     Debug.Log("Стреляет второй ствол"); //пишем в консоль
                     FiringOrder = 1; //устанавливаем FiringOrder в изначальную позицию
                     break;
               }
               reloadTimer = reloadCooldown; //возвращаем переменной задержки её первоначальное значение из константы
            }
         }
      }
      else //иначе
      {
         curTarget = SortTargets(); //сортируем цели и получаем новую
      }
   }

   //Очень примитивный метод сортировки целей, море возможностей для модификации!
   public GameObject SortTargets()
   {
      float closestMobDistance = 0; //инициализация переменной для проверки дистанции до моба
      GameObject nearestmob = null; //инициализация переменной ближайшего моба
      List<GameObject> sortingMobs = GameObject.FindGameObjectsWithTag("Monster").ToList(); //находим всех мобов с тегом Monster и создаём массив для сортировки

      foreach (var everyTarget in sortingMobs) //для каждого моба в массиве
      {
         //если дистанция до моба меньше, чем closestMobDistance или равна нулю
         if ((Vector3.Distance(everyTarget.transform.position, turretHead.position) < closestMobDistance) || closestMobDistance == 0)
         {
            closestMobDistance = Vector3.Distance(everyTarget.transform.position, turretHead.position); //Меряем дистанцию от моба до пушки, записываем её в переменную
            nearestmob = everyTarget;//устанавливаем его как ближайшего
         }
      }
      return nearestmob; //возвращаем ближайшего моба
   }
}

Комментарии, думаю, довольно ясно описывают код. Единственным непонятным может показаться такой монстр, как кватернион. Не стесняйтесь, погуглите, почитайте, эта тема не всем легко даётся. А здесь можно почитать про кватернионы в Unity3D на их же сайте.

Сохраните изменения и переключитесь назад на Unity3D.

Чтобы «натянуть» наш свеженаписанный скрипт на пушку, нужно перетащить файл скрипта прямо на её префаб. После этого, если нажать на префаб пушки — в инспекторе свойств появится раздел с нашим скриптом, где можно настраивать все public поля в коде!
Как создать простую Tower Defense игру на Unity3D, часть первая

Далее, для теста нашего кода, нам нужно приделать тег Monster к нашему монстру. Нажмите на него в Project, затем посмотрите на Инспектор объекта: в верхней его части есть выпадающее поле Tag, сейчас там стоит значение Untagged. Нажимаем на этот список и в нижней его части жмём Add tag.
Как создать простую Tower Defense игру на Unity3D, часть первая

Разворачиваем список Tags, и в поле Element 0 пишем «Monster» (без кавычек, как на скрине).
Как создать простую Tower Defense игру на Unity3D, часть первая

Опять нажимаем на нашего монстра, опять разворачиваем список возможных тегов — среди них будет и Monster. Выбираем его. Теперь можно перетащить нашего монстра на сцену, где-нибудь недалеко от пушки, запустить сцену (значок Play вверху посередине) и в окне Scene потягать монстра вокруг пушки. А также порадоваться, что она следит за ним великолепно, соблюдая заданные нами мертвую зону и максимальную дистанцию.

Школа начинающего моба

До сих пор наши мобы были просто объектами, но теперь мы научим их ползти к пушке и наносить ей радость, счастье и, прежде всего, урон.
Уже известным способом создаём новые C# скрипты: MobAI, GlobalVars, MobHP, TurretHP, SpawnerAI. Начнём по порядку:

MobAI.cs

using UnityEngine;

public class MobAI : MonoBehaviour
{
   public GameObject Target; //текущая цель

   public float mobPrice = 5.0f; //цена за убийство моба
   public float mobMinSpeed = 0.5f; //минимальная скорость моба
   public float mobMaxSpeed = 2.0f; //максимальная скорость моба
   public float mobRotationSpeed = 2.5f; //скорость поворота моба
   public float attackDistance = 5.0f; //дистанция атаки
   public float damage = 5; //урон, наносимый мобом
   public float attackTimer = 0.0f; //переменная расчета задержки между ударами
   public const float coolDown = 2.0f; //константа, используется для сброса таймера атаки в начальное значение

   private float MobCurrentSpeed; //скорость моба, инициализируем позже
   private Transform mob; //переменная для трансформа моба
   private GlobalVars gv; //поле для объекта глобальных переменных

   private void Awake()
   {
      gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //инициализируем поле
      mob = transform; //присваиваем трансформ моба в переменную (повышает производительность)
      MobCurrentSpeed = Random.Range(mobMinSpeed, mobMaxSpeed); //посредством рандома выбираем скорость между минимально и максимально указанной
   }

   private void Update()
   {
      if (Target == null) //если цели ещё нет
      {
         Target = SortTargets(); //пытаемся достать её из общего списка
      }
      else //если у нас есть цель
      {
         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 distance = Vector3.Distance(Target.transform.position, mob.position); //меряем дистанцию до цели
         Vector3 structDirection = (Target.transform.position - mob.position).normalized; //получаем вектор направления
         float attackDirection = Vector3.Dot(structDirection, mob.forward); //получаем вектор атаки
         if (distance < attackDistance && attackDirection > 0) //если мы на дистанции атаки и цель перед нами
         {
            if (attackTimer > 0) attackTimer -= Time.deltaTime; //если таймер атаки больше 0 - отнимаем его
            if (attackTimer <= 0) //если же он стал меньше нуля или равен ему
            {
               TurretHP thp = Target.GetComponent<TurretHP>(); //подключаемся к компоненту ХП цели
               if (thp != null) thp.ChangeHP(-damage); //если цель ещё живая, наносим дамаг (мы можем не одни бить по цели, потому проверка необходима)
               attackTimer = coolDown; //возвращаем таймер в исходное положение
            }
         }
      }
   }
   //Очень примитивный метод сортировки целей, море возможностей для модификации!
   private GameObject SortTargets()
   {
      float closestTurretDistance = 0; //инициализация переменной для проверки дистанции до пушки
      GameObject nearestTurret = null; //инициализация переменной ближайшей пушки
      List<GameObject> sortingTurrets = gv.TurretList; //оздаём массив для сортировки

      foreach (var turret in sortingTurrets) //для каждой пушки в массиве
      {
         //если дистанция до пушки меньше, чем closestTurretDistance или равна нулю
         if ((Vector3.Distance(mob.position, turret.transform.position) < closestTurretDistance) || closestTurretDistance == 0)
         {
            closestTurretDistance = Vector3.Distance(mob.position, turret.transform.position); //Меряем дистанцию от моба до пушки, записываем её в переменную
            nearestTurret = turret;//устанавливаем её как ближайшего
         }
      }
      return nearestTurret; //возвращаем ближайший ствол
   }
}

GlobalVars.cs — класс глобальных переменных

using System.Collections.Generic;
using UnityEngine;

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

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

   public float PlayerMoney = 200.0f; //деньги игрока
}

MobHP.cs

using UnityEngine;

public class MobHP : MonoBehaviour
{
   public float maxHP = 100; //Максимум ХП
   public float curHP = 100; //Текущее ХП
   public Color MaxDamageColor = Color.red; //цвета полностью побитого
   public Color MinDamageColor = Color.blue; //и целого моба
   private GlobalVars gv; //поле для объекта глобальных переменных

   private void Awake()
   {
      gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //инициализируем поле
      if (gv != null)
      {
         gv.MobList.Add(gameObject); //добавляем себя в общий лист мобов
         gv.MobCount++; //увеличиваем счетчик мобов
      }
      if (maxHP < 1) maxHP = 1; //если максимальное хп задано менее единицы - ставим единицу
   }

   public void ChangeHP(float adjust) //метод корректировки ХП моба
   {
      if ((curHP + adjust) > maxHP) curHP = maxHP;//если сумма текущего ХП и adjust в результате более, чем максимальное хп - текущее ХП становится равным максимальному
      else curHP += adjust; //иначе просто добавляем adjust
   }

   private void Update()
   {
      gameObject.renderer.material.color = Color.Lerp(MaxDamageColor, MinDamageColor, curHP / maxHP); //Лерпим цвет моба по заданным в начале цветам. В примере: красный - моб почти полностью убит, синий - целый.
      if (curHP <= 0) //если ХП упало в ноль или ниже
      {
         MobAI mai = gameObject.GetComponent<MobAI>(); //подключаемся к компоненту AI моба
         if (mai != null && gv != null) gv.PlayerMoney += mai.mobPrice; //если он существует - добавляем денег игроку в размере цены за голову моба
         Destroy(gameObject); //удаляем себя
      }
   }

   private void OnDestroy() //при удалении
   {
      if (gv != null)
      {
         gv.MobList.Remove(gameObject); //удаляем себя из глобального списка мобов
         gv.MobCount--; //уменьшаем глобальный счетчик мобов на 1
      }
   }
}

А следующий класс я не комментировал, он почти полная копия MobHP, за некоторыми различиями (например, ему не надо лерпить свой цвет).

TurretHP.cs

using UnityEngine;

public class TurretHP : MonoBehaviour
{
   public float maxHP = 100; //Максимум ХП
   public float curHP = 100; //Текущее ХП
   private GlobalVars gv; //поле для объекта глобальных переменных

   private void Awake()
   {
      gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //инициализируем поле
      if (gv != null)
      {
         gv.TurretList.Add(gameObject);
         gv.TurretCount++;
      }
      if (maxHP < 1) maxHP = 1;
   }

   public void ChangeHP(float adjust)
   {
      if ((curHP + adjust) > maxHP) curHP = maxHP;
      else curHP += adjust;
      if (curHP > maxHP) curHP = maxHP;
   }

   private void Update()
   {
      if (curHP <= 0)
      {
         Destroy(gameObject);
      }
   }

   private void OnDestroy()
   {
      if (gv != null)
      {
         gv.TurretList.Remove(gameObject);
         gv.TurretCount--;
      }
   }
}

SpawnerAI.cs

using UnityEngine;

public class SpawnerAI : MonoBehaviour
{
   public int waveAmount = 5; //Количество мобов за 1 волну на каждой точке спауна
   public int waveNumber = 0; //переменная текущей волны
   public float waveDelayTimer = 30.0F; //переменная таймера спауна волны
   public float waveCooldown = 20.0F; //переменная (не константа уже!) для сброса таймера выше, мы её будем модифицировать
   public int maximumWaves = 500; //максимальное количество мобов в игре
   public Transform Mob; //переменная для загрузки префаба в Unity
   public GameObject[] SpawnPoints; //массив точек спауна
   private GlobalVars gv; //поле для объекта глобальных переменных

   private void Awake()
   {
      SpawnPoints = GameObject.FindGameObjectsWithTag("Spawnpoint"); //забираем все точки спауна в массив
      gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //инициализируем поле
   }

   private void Update()
   {
      if (waveDelayTimer > 0) //если таймеh спауна волны больше нуля
      {
         if (gv != null)
         {
            if (gv.MobCount == 0) waveDelayTimer = 0; //если мобов на сцене нет - устанавливаем его в ноль
            else waveDelayTimer -= Time.deltaTime; //иначе отнимаем таймер
         }
      }
      if (waveDelayTimer <= 0) //если таймер менее или равен нулю
      {
         if (SpawnPoints != null && waveNumber < maximumWaves) //если имеются точки спауна и ещё не достигнут предел количества волн
         {
            foreach (GameObject spawnPoint in SpawnPoints) //на каждой точке спауна
            {
               for (int i = 0; i < waveAmount; i++) //используем i как модификатор для спауна, чтобы мобы не были в упор друг к другу
               {
                  Instantiate(Mob, new Vector3(spawnPoint.transform.position.x, spawnPoint.transform.position.y, spawnPoint.transform.position.z + i * 10), Quaternion.identity); //спауним моба
               }

               if (waveCooldown > 5.0f) //если задержка длится более 5 секунд
               {
                  waveCooldown -= 0.1f; //сокращаем на 0.1 секунды
                  waveDelayTimer = waveCooldown; //задаём новый таймер
               }
               else //иначе
               {
                  waveCooldown = 5.0f; //задержка никогда не будет менее 5 секунд
                  waveDelayTimer = waveCooldown;
               }

               if (waveNumber >= 50) //после 50 волны
               {
                  waveAmount = 10; //будем спаунить по 10 мобов на каждой точке
               }
            }
            waveNumber++; //увеличиваем номер волны
         }
      }
   }
}

А теперь необходимо поправить код AI пушки. Найдите там switch (FiringOrder) и замените весь блок полностью на такой:

               switch (FiringOrder) //смотрим, из какого ствола стрелять
               {
                  case 1:
                     if (mhp != null) mhp.ChangeHP(-attackDamage); //наносим дамаг цели
                     FiringOrder++; //увеличиваем FiringOrder на 1
                     break;
                  case 2:
                     if (mhp != null) mhp.ChangeHP(-attackDamage); //наносим дамаг цели
                     FiringOrder = 1; //устанавливаем FiringOrder в изначальную позицию
                     break;
               }

Также необходимо заменить в самом конце этого же класса строчку

return nearestmob;

на такую

return closestMobDistance > attackMaximumDistance ? null : nearestmob;

Это называется «тернарный оператор». Если условие до знака "?" верно — то оно вернёт null, иначе вернётся nearestmob. Смысл выражения в том, что пушка не схватит цель, до которой не может достать.

В целом, код готов. Теперь надо подготовить игровые объекты. Создайте объект MobSpawner, его местоположение не играет роли, лишь бы не мешался в дальнейшем. Повесьте на него скрипт SpawnerAI и выставьте желаемые значения переменных. На значение переменной Mob перетягиваем наш префаб моба.
Как создать простую Tower Defense игру на Unity3D, часть первая
Спаунер более не трогаем.

Создайте объект с именем GlobalVars и перетащите на него одноименный скрипт, укажите стартовое количество денег у игрока.
Далее, создайте нужное количество объектов (для удобства именуйте в духе «имя_порядковыйНомер») для точек спауна и разместите их в желаемых местах спауна мобов. Присвойте им тег Spawnpoint, а заодно создайте тег Turret и присвойте его префабу пушки.

Повесьте на мобов наши 2 скрипта MobAI и MobHP, а на пушку — TurretHP. Не забывайте побаловаться со значениями переменных.
На значение target в MobAI префаб пушки перетягивать не надо, AI сам ищет цели. Очень примитивно, медленно, но ищет.

Добавьте компонент Rigidbody на префаб монстра (Component -> Physics -> Rigidbody).

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

ПоторGUIем?

Для создания GUI нам понадобится новый C# скрипт с названием Graphic:

Graphic.cs

using UnityEngine;

public class Graphic : MonoBehaviour
{
   private GlobalVars gv; //поле для объекта глобальных переменных

   public Rect buyMenu; //квадрат меню покупки
   public Rect firstTower; //квадрат кнопки покупки первой башни
   public Rect secondTower; //квадрат кнопки покупки второй башни
   public Rect thirdTower; //квадрат кнопки покупки третьей башни
   public Rect fourthTower; //квадрат кнопки покупки четвёртой башни
   public Rect fifthTower; //квадрат кнопки покупки пятой башни

   public Rect towerMenu; //квадрат сервисного меню башни (продать/обновить)
   public Rect towerMenuSellTower; //квадрат кнопки продажи башни
   public Rect towerMenuUpgradeTower; //квадрат кнопки апгрейда башни

   public Rect playerStats; //квадрат статистики игрока
   public Rect playerStatsPlayerMoney; //квадрат зоны отображения денег игрока

   public GameObject plasmaTower; //префаб первой пушки, необходимо назначить в инспекторе
   public GameObject plasmaTowerGhost; //призрак первой пушки, необходимо назначить в инспекторе
   private RaycastHit hit; //переменная для рейкаста
   public LayerMask raycastLayers = 1; //а это вам маленькое Д/З - узнать, что это делает

   private GameObject ghost; //переменная для призрака устанавливаемой пушки

   private void Awake()
   {
      gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //инициализируем поле
      if (gv == null) Debug.LogWarning("gv variable is not initialized correctly in " + this); //сообщим об ошибке, если gv пуста

      buyMenu = new Rect(Screen.width - 185.0f, 10.0f, 175.0f, Screen.height - 100.0f); //задаём размеры квадратов, последовательно позиция X, Y, Ширина, Высота. X и Y указывают на левый верхний угол объекта
      firstTower = new Rect(buyMenu.x + 12.5f, buyMenu.y + 30.0f, 150.0f, 50.0f);
      secondTower = new Rect(firstTower.x, buyMenu.y + 90.0f, 150.0f, 50.0f);
      thirdTower = new Rect(firstTower.x, buyMenu.y + 150.0f, 150.0f, 50.0f);
      fourthTower = new Rect(firstTower.x, buyMenu.y + 210.0f, 150.0f, 50.0f);
      fifthTower = new Rect(firstTower.x, buyMenu.y + 270.0f, 150.0f, 50.0f);

      playerStats = new Rect(10.0f, 10.0f, 150.0f, 100.0f);
      playerStatsPlayerMoney = new Rect(playerStats.x + 12.5f, playerStats.y + 30.0f, 125.0f, 25.0f);

      towerMenu = new Rect(10.0f, Screen.height - 60.0f, 400.0f, 50.0f);
      towerMenuSellTower = new Rect(towerMenu.x + 12.5f, towerMenu.y + 20.0f, 75.0f, 25.0f);
      towerMenuUpgradeTower = new Rect(towerMenuSellTower.x + 5.0f + towerMenuSellTower.width, towerMenuSellTower.y, 75.0f, 25.0f);
   }

   private void Update()
   {
      switch (gv.mau5tate) //свитчим состояние курсора мыши
      {
         case GlobalVars.ClickState.Placing: //если он в режиме установки башен
            {
               if (ghost == null) ghost = Instantiate(plasmaTowerGhost) as GameObject; //если переменная призрака пустая - создаём в ней объект призрака башни
               else //иначе
               {
                  Ray scrRay = Camera.main.ScreenPointToRay(Input.mousePosition); //создаём луч, бьющий от координат мыши по координатам в игре
                  if (Physics.Raycast(scrRay, out hit, Mathf.Infinity, raycastLayers)) // бьём этим лучем в заданном выше направлении (т.е. в землю)
                  {
                     Quaternion normana = Quaternion.FromToRotation(Vector3.up, hit.normal); //получаем нормаль от столкновения
                     ghost.transform.position = hit.point; //задаём позицию призрака равной позиции точки удара луча по земле
                     ghost.transform.rotation = normana; //тоже самое и с вращением, только не от точки, а от нормали
                     if (Input.GetMouseButtonDown(0)) //при нажатии ЛКМ
                     {
                        GameObject tower = Instantiate(plasmaTower, ghost.transform.position, ghost.transform.rotation) as GameObject; //Спауним башенку на позиции призрака
                        if (tower != null) gv.PlayerMoney -= tower.GetComponent<PlasmaTurretAI>().towerPrice; //отнимаем лаве за башню
                        Destroy(ghost); //уничтожаем призрак башни
                        gv.mau5tate = GlobalVars.ClickState.Default; //меняем глобальное состояние мыши на обычное
                     }
                  }
               }
               break;
            }
      }
   }

   private void OnGUI()
   {
      GUI.Box(buyMenu, "Buying menu"); //Делаем гуевский бокс на квадрате buyMenu с заголовком, указанным между ""
      if (GUI.Button(firstTower, "Plasma Towern100$")) //если идёт нажатие на первую кнопку
      {
         gv.mau5tate = GlobalVars.ClickState.Placing; //меняем глобальное состояние мыши
      }
      if (GUI.Button(secondTower, "Pulse Towern155$")) //с остальными аналогично
      {
         //action here
      }
      if (GUI.Button(thirdTower, "Beam Towern250$"))
      {
         //action here
      }
      if (GUI.Button(fourthTower, "Tesla Towern375$"))
      {
         //action here
      }
      if (GUI.Button(fifthTower, "Artillery Towern500$"))
      {
         //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"))
      {
         //action here
      }
   }
}

Ах да, теперь нам ещё нужно неплохо изменить скрипт GlobalVars:

GlobalVars.cs

using System.Collections.Generic;
using UnityEngine;

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

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

   public float PlayerMoney; //деньги игрока

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

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

   public void Awake()
   {
      PlayerMoney = PlayerPrefs.GetFloat("Player Money", 200.0f); //при старте игры, если нету сохранённых данных про деньги игрока - их становится 200$, иначе загружается из реестра
   }

   public void OnApplicationQuit()
   {
      PlayerPrefs.SetFloat("Player Money", PlayerMoney); //сохраняет деньги игрока при выходе
      PlayerPrefs.Save();
   }
}

Далее, нам надо создать призрак пушки, а делается это довольно легко: дублируем пушку, закидываем в неё какой-либо пустой ГО, удаляем его и пушка отвязывается от своего префаба, главное в него же не сохранить! Далее проходимся по всей иерархии объектов внутри пушки и меняем попавшиеся шейдеры на Transparent Diffuse. Кстати, чтобы увидеть абсолютно всю структуру пушки — её необходимо поместить на сцену и раскрывать иерархию уже там. Если возникнут проблемы с созданием призрака — выложу уже готовый.

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

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

И если у вас всё получилось правильно, то выглядеть это будет примерно так:

Продолжение следует!

Автор: Andy_Ion

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js