• Главная
  • О нас
  • Статьи
  • Вакансии
  • Контакты

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

27 Август 2013 by Juds in How-To, Unity3D tags: tower defense, unity3d, гайд, туториал, урок

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

Часть первая

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

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

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

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

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

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

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

PlasmaTurretAI.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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 за совет :)

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

585858

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

575757

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

BaseHP.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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, но на первый взгляд — ничего сложного.

Пишите в комментариях, какие ещё моменты осветить в третьей части.

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

Источник: habrahabr.ru

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

Leave a Comment! Отменить ответ

You must be logged in to post a comment.
Уроки
  • Cinema 4D
  • Unity3D
  • PHP
  • Delphi
  • JavaScript
  • Python
  • HTML5
  • Go
Статьи
  • Новости
  • Game Development
  • PHP
  • QA
  • IT Юмор
  • Разное
Теги
Android Composer Delphi excerption experience Game Design game development gameplay Git Go! AOP google Google Analytics HHVM it experience it юмор Laravel Linux manager Phalcon PHP Python QA RFC Selenium Silex Slim Symfony 2 unity3d warcraft Yii Yii 2 Zend Framework 2 Zephir Биографии Новости Обучение веб-разработка высоконагруженные проекты дайджест дизайн исследование подборка ссылки стартап тенденции
О Нас

Juds–компания по разработке программного обеспечения, разработке веб-проектов и мобильных приложений. Все предлагаемые нами решения индивидуальны и направлены на максимально точное удовлетворение потребностей наших партнеров. Мы находимся в постоянном поиске новых ярких решений. Главные критерии – актуальность применения и инновационность.

Статьи
  • Лучшее из мира PHP за 2013
  • Полезные функции Google Analytics
  • Что в SEO можно считать нормальным и работающим, а что – отжившим
  • 30 полезных для себя вещей
  • Дайджест интересных новостей и материалов из мира PHP (20 октября — 10 ноября 2013)
  • Cinema 4D: создаем плагин – объект
IT Юмор
Метки
Android Composer experience Game Design game development google HHVM it experience it юмор Laravel manager PHP unity3d Yii Zend Framework 2 Zephir Новости Обучение веб-разработка дайджест исследование подборка ссылки стартап тенденции
© 2014 Juds. Все права защищены.