0,0
рейтинг
24 июля 2012 в 20:56

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

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

Часть вторая

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


Первые шаги


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

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


Закрываем окно выбора текстур, возвращаемся к окну настройки текстур, нажимаем в нём кнопку 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 towerPrice = 100.0f;
   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) //став нулем
            {
               MobHP mhp = curTarget.GetComponent<MobHP>();
               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 поля в коде!


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


Разворачиваем список Tags, и в поле Element 0 пишем «Monster» (без кавычек, как на скрине).


Опять нажимаем на нашего монстра, опять разворачиваем список возможных тегов — среди них будет и Monster. Выбираем его.

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


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

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

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 перетягиваем наш префаб моба.

Спаунер более не трогаем.

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

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

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



Потор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 Tower\n100$")) //если идёт нажатие на первую кнопку
      {
         gv.mau5tate = GlobalVars.ClickState.Placing; //меняем глобальное состояние мыши
      }
      if (GUI.Button(secondTower, "Pulse Tower\n155$")) //с остальными аналогично
      {
         //action here
      }
      if (GUI.Button(thirdTower, "Beam Tower\n250$"))
      {
         //action here
      }
      if (GUI.Button(fourthTower, "Tesla Tower\n375$"))
      {
         //action here
      }
      if (GUI.Button(fifthTower, "Artillery Tower\n500$"))
      {
         //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. Кстати, чтобы увидеть абсолютно всю структуру пушки — её необходимо поместить на сцену и раскрывать иерархию уже там. Если возникнут проблемы с созданием призрака — выложу уже готовый. У некоторых всё же возникли — ссылка на скачку пакета здесь (3,7 мб).

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

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

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



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

26.07.12: Исправил ошибки в коде
27.08.13: Починил битые ссылки
23.12.13: Починил битые ссылки
Андрей Данилов @Andy_Ion
карма
28,7
рейтинг 0,0
C# developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (45)

  • 0
    Планируете рассказать о применении NavMesh?
    Зачем дублировать пушки вместо обычной смены материала?
    • 0
      Да, поиск пути в планах.
      Пушки задублировал для простоты объяснения новичкам. В следующих частях я буду проводить оптимизацию с пояснениями.
      • 0
        По оптимизации, лучше создавать компонент Enimy и искать врагов по наличию этого компонента. По скорости получается чуть медленнее, но не нужно возиться с тегами… добавил компонент, настроил поведение и сделал префаб.

        Насчет создания префабов, то проще сначала сделать 1 игровой объект и перетащить его с дерева сцены на Assets (так проще для понимания геймдизайнерам).
        • 0
          Да там много где странностей:
          1. При знании того, что в сцене может быть много объектов — никогда не использовать поиск по имени / тегу, ибо оно идет по всему дереву, проверяя все объекты. См п.3.
          2. Никогда не использовать VectorX.Distance(), тем более в Update. Всегда стараться использовать (Vector1 — Vector0).sqrDistance и сравнивать с квадратом расстояния (предрассчитанным или просто умноженным на себя). Понять, почему так, поможет изучение формулы рассчета расстояния между точками.
          3. Проверять все объекты по списку при большом количестве может быть нерационально. Т.к. на всех объектах висит коллайдер, то рациональнее всегда получать список объектов через Physics.OverlapSphere() — физика в unity3d работает быстро + использует внутренние оптимизации по разбиению и раннему отсечению пространства + можно явно указывать слои для поиска. Потом просто найти минимум дистанции в полученном массиве и получить цель.
          4. Много дублирующегося кода.
          Если хочется глобального хранилища переменных, стоит подумать над статическим классом, либо реализацией синглтона (что предпочтительнее, т.к. позволит визуально настраивать паблик-параметры — unity3d не показывает в инспекторе статик-свойства).
          Надо подумать над унификацией скрипта выбора цели и атаки — пушка является таким же мобом, только неподвижным. Разницу в выборе целей можно реализовать слоями.
          • 0
            Я полностью с Вами согласен, именно так и планировал модифицировать код в дальнейшем. Только про пункт 2 впервые слышу, почитаю обязательно на эту тему.
            • 0
              В формуле есть sqrt — он гораздо медленнее, чем просто умножение.
  • +5
    Теги смущают :)
  • 0
    Отлично, спасибо.
    А на JS сильно код будет отличаться?
    • +1
      Сам на JS не писал, но регулярно на него смотрю в Scripting Reference. Там справа вверху от примеров кода есть переключатель языка, посмотрите на разницу сами ;)
  • 0
    а что это за IDE?
    • 0
      Вместе с Unity3D идёт MonoDevelop, а я использую MSVS 2010.
    • 0
    • 0
      Это собственно и есть Unity 3D
  • 0
    Выглядит здорово!
    Только так и не понял, зачем мобу добавлять компонент Rigidbody? Потом планируется прикручивать физику?
    • +1
      Присмотрелся и увидел :) При спауне мобов они появляются чуть выше поверхности, и потом падают
      • 0
        Собираюсь далее сделать движение не через Transform.position, а через Rigidbody.AddForce.
        • 0
          Это плохая мысль — могут начать происходить всякие забавные штуки при ошибках расчета столкновений типа мгновенного увеличения скорости до такой степени, что боты будут выстреливаться прочь с игровой области. Лучше воспользоваться CharacterCollider — у него есть готовые методы по перемещению.
          • 0
            Хм, никогда не видел таких ошибок от перемещения через AddForce: тестировал сцену с 16000 «шариками», они бились друг о друга, но не улетали за пределы моей сцены.
            • 0
              Зависит от формы коллайдера — есть есть острые-прямые углы — начинаются «запинания» и дрожания. У «шарика» по умолчанию SphereCollider, его нормально «выталкивает» и он скользит по поверхности других тел.
  • 0
    Спасибо, отличная статья! С нетерпением жду всей серии!
  • –2
    >Закрываем окно выбора текстур, возвращаемся к окну выбора текстур

    Лолчто? (:
  • +3
    Узнал новое слово: дамаг.

    Уж извините, но либо damage, либо повреждения.

    «она подала на него в суд за сексуальный дамаг»?
    • 0
      Нормальное в среде ММО-шников заимствование. Его можно часто услышать в игровых тусовках, на форумах и в конференциях.
    • +4
      Узнал новое слово: дамаг.

      Подбери шип подходящий, определись с типом танка — пассив/актив, скилы учи на капу, шилд/армор резы, реген и объём шилда/работу с репками и вперёд. Танкуй хоть до посинения. Я вон написал про две T2 репы и харднера в домике. 5 капречей T2, CCC, и капа не кончается вообще. Аукс нано памп даёт прибавку в 10 % к репкам. На миске агришь группу, выпускаешь T2 огров и сидишь форумы читаешь. …незнаю сидел както на станции фитил ББху, а тут за воротами тарань с клавой на дрык напали, пока я ББхой подварпал к гейту и пропрыгнул от тарани остался врек, а клава летала в 300 км и ругалась в локале. так что хз чего вы там танкуете. …о/, харош кемпить на гейте, точку в алли я заинвайчу в ганг и варп на гейт ЕЦ, проваливаемся и на меня в ноль, я на мальке, держу апок, меня дроны пилят. Варп в ноль (на инсту), дреды сразу в сидж, лочат палку, кары лочат дредов и друг друга, дикторы бублят все что движется, фальконы и прочие реконы висят в клоке до спецприказа. Интера, осторожней с файтерами, чтоб смартами не посносило, на белтах потом отрабатывать придется. Хаки и прочие кто помельче выдавливаем саппорт противника. Коврик — скинь пробки.
      • 0
        О, а я 2-3 месяца как бросил играть в неё :)
    • –1
      Если уж начинаете играть в граммар наци, то хоть сами ошибки не ляпайте. Что за «сексуальные повреждения» такие?
      Sexual assault, либо harassment.
      • +1
        Нет, это я использовал как «дамаг-ательства».
        • 0
          В таком случае пардон. У меня не очень хорошо с играми в слова в середине рабочей недели.
    • 0
      Я исправлюсь ;)
  • 0
    Вот это поинтереснее будет:
    раз и два
  • +1
    Для меня самым сложным в этом всём кажется это баланс, редко встретишь нормально сбалансированную Tower Defence
  • 0
    Дошли руки, попробовать повторить все описанное
    Застрял на «Создайте объект MobSpawner, его местоположение не играет роли» — какой объект? GameObject, а дальше? Сфера, куб или плейн?
    • 0
      GameObject -> Create empty (Ctrl+Shift+N)

      Самый первый пункт в выпадающем меню, если верно помню :)
  • 0
    Спасибо, выполнил урок, жду прододжения )
  • 0
    А будет продолжение? Хотелось бы почитать.
    • 0
      Сейчас пишу.
  • –1
    Ссылка на пушку похерилась. Есть где в другом месте скачать и желательно в zip?
  • 0
    Спасибо интересная статья, но при попытке повторить, не смог сделать пушку призрак.
  • 0
    Спасибо. Продолжение планируется.?

    • 0
      Есть уже черновик с продолжением, но всё не могу выделить время сесть и дописать — многое в жизни меняется.
  • +1
    Когда же долгожданное продолжение?
  • 0
    Спасибо за статью!
    Переправьте, пожалуйста, ссылки, они ссылаются в пустоту…

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