Создание игры Tower Defense в Unity — Часть 1

https://www.raywenderlich.com/168079/create-tower-defense-game-unity-part-1-2
  • Перевод
image

Игры жанра tower defense приобретают всё большую популярность, и это неудивительно — немногое может сравниться с удовольствием от наблюдения за собственными линиями защиты, уничтожающими злых врагов! В этом туториале из двух частей мы создадим игру tower defense на движке Unity!

Вы узнаете, как сделать следующее:

  • Создавать волны врагов
  • Заставить их следовать по точкам маршрута
  • Строить и апгрейдить башни, а также научите их, как разбивать врагов на мелкие пиксели

В конце мы получим каркас игры, который можно развивать дальше!

Примечание: вам необходимы начальные знания Unity (например, вы должны знать, как добавляются ассеты и компоненты, что такое префабы) и основы языка C#. Для изучения всего этого я рекомендую вам пройти туториалы по Unity Шона Даффи или серию Beginning C# with Unity Брайана Мокли.

Я буду работать в версии Unity для OS X, но этот туториал подойдёт и для Windows.

Сквозь окна башни из слоновой кости


В этом туториале мы создадим игру tower defense, в которой враги (маленькие жучки) ползут к печеньке, принадлежащей вам и вашим миньонам (разумеется, это монстры!). Игрок может размещать монстров в стратегических точках и улучшать их за золото.

Игрок должен убить всех жуков, пока они не доберутся до печенья. Каждую новую волну врагов всё сложнее победить. Игра заканчивается, когда вы переживёте все волны (победа!) или когда до печенья доползут пять врагов (проигрыш!).

Вот скриншот готовой игры:


Монстры, объединяйтесь! Защищайте печеньку!

Приступаем к работе


Скачайте эту заготовку проекта, распакуйте её и откройте проект TowerDefense-Part1-Starter в Unity.

В заготовке проекта есть ассеты графики и звуков, готовые анимации и несколько полезных скриптов. Скрипты не связаны напрямую с играми жанра tower defense, поэтому я не буду здесь о них рассказывать. Однако если вы хотите больше узнать о создании 2D-анимаций в Unity, то изучите этот туториал по Unity 2D.

В проекте также содержатся префабы, которые мы позже дополним, чтобы создать персонажей. Наконец, в проекте есть сцена с фоном и настроенным интерфейсом пользователя.

Откройте GameScene, находящуюся в папке Scenes и задайте для режима Game соотношение сторон 4:3, чтобы все метки правильно совпадали с фоном. В режиме Game вы увидите следующее:


Авторство:

  • Графика для проекта взята из бесплатного пака Вики Вендерлих! Другие графические работы можно найти на её сайте gameartguppy.
  • Отличная музыка взята с сайта BenSound, на котором есть другие потрясающие саундтреки!
  • Также благодарю Майкла Джеспера за очень полезную функцию тряски камеры
.

Место помечено крестом: расположение монстров


Монстров можно ставить только на точки, помеченные знаком x.

Чтобы добавить их в сцену, перетащите Images\Objects\Openspot из Project Browser в окно Scene. Пока позиция для нас не важна.

Выбрав в иерархии Openspot, нажмите на Add Component в Inspector и выберите Box Collider 2D. В окне Scene Unity покажет прямоугольный коллайдер с зелёной линией. Мы будем использовать этот коллайдер для распознавания нажатий мыши на этом месте.


Аналогичным образом добавьте к Openspot компонент Audio\Audio Source. Выберите для параметра AudioClip компонента Audio Source файл tower_place, который находится в папке Audio, и отключите Play On Awake.

Нам нужно создать ещё 11 точек. Хотя существует искушение повторить все эти действия, в Unity есть решение получше: Prefab!

Перетащите Openspot из Hierarchy в папку Prefabs внутри Project Browser. Его название станет в Hierarchy синим, это означает, что он присоединён к префабу. Примерно так:


Теперь, когда у нас есть заготовка-префаб, мы можем создать сколько угодно копий. Просто перетащите Openspot из папки Prefabs внутри Project Browser в окно Scene. Повторите это 11 раз, и у нас появится в сцене 12 объектов Openspot.

Теперь воспользуемся Inspector, чтобы задать этим 12 объектам Openspot следующие координаты:

  • (X:-5.2, Y:3.5, Z:0)
  • (X:-2.2, Y:3.5, Z:0)
  • (X:0.8, Y:3.5, Z:0)
  • (X:3.8, Y:3.5, Z:0)
  • (X:-3.8, Y:0.4, Z:0)
  • (X:-0.8, Y:0.4, Z:0)
  • (X:2.2, Y:0.4, Z:0)
  • (X:5.2, Y:0.4, Z:0)
  • (X:-5.2, Y:-3.0, Z:0)
  • (X:-2.2, Y:-3.0, Z:0)
  • (X:0.8, Y:-3.0, Z:0)
  • (X:3.8, Y:-3.0, Z:0)

Когда вы это сделаете, сцена будет выглядеть так:


Размещаем монстров


Чтобы упростить размещение, в папке Prefab проекта есть префаб Monster.


Префаб Monster готов к использованию

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

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

Теперь мы создадим скрипт, который будет располагать Monster на Openspot.

В Project Browser выберите в папке Prefabs объект Openspot. В Inspector нажмите на Add Component, а затем выберите New Script и назовите скрипт PlaceMonster. Выберите в качестве языка C Sharp и нажмите на Create and Add. Так как мы добавили скрипт к префабу Openspot, то у всех объектов Openspot в сцене теперь будет этот скрипт. Отлично!

Дважды нажмите на скрипт, чтобы открыть его в IDE. Затем добавьте две переменные:

public GameObject monsterPrefab;
private GameObject monster;

Мы создадим экземпляр объекта, хранящегося в monsterPrefab, чтобы создать монстра, и сохраним его в monster, чтобы можно было манипулировать им во время игры.

По одному монстру на точку


Чтобы на одну точку можно было поставить только одного монстра, добавьте следующие метод:

private bool CanPlaceMonster()
{
  return monster == null;
}

В CanPlaceMonster() мы можем проверять, по-прежнему ли переменная monster равна null. Если это так, то в точке нет монстра, и мы можем разместить его.

Теперь добавим следующий код, чтобы размещать монстра, когда игрок нажимает на этот GameObject:

//1
void OnMouseUp()
{
  //2
  if (CanPlaceMonster())
  {
    //3
    monster = (GameObject) 
      Instantiate(monsterPrefab, transform.position, Quaternion.identity);
    //4
    AudioSource audioSource = gameObject.GetComponent<AudioSource>();
    audioSource.PlayOneShot(audioSource.clip);

    // TODO: вычитать золото
  }
}

Этот код располагает монстра при нажатии мыши или касании экрана. Как он работает?

  1. Unity автоматически вызывает OnMouseUp, когда игрок касается физического коллайдера GameObject.
  2. При вызове этот метод ставит монстра, если CanPlaceMonster() возвращает true.
  3. Мы создаём монстра с помощью метода Instantiate, который создаёт экземпляр заданного префаба с указанной позицией и поворотом. В данном случае мы копируем monsterPrefab, задаём ему текущую позицию GameObject и отсутствие поворота, передаём результат в GameObject и сохраняем его в monster
  4. В конце мы вызываем PlayOneShot для воспроизведения звукового эффекта, прикреплённого к компоненту AudioSource объекта.

Теперь наш скрипт PlaceMonster может располагать нового монстра, но нам всё ещё нужно указать префаб.

Использование подходящего префаба


Сохраните файл и вернитесь в Unity.

Чтобы задать переменную monsterPrefab, сначала выберите в браузере проекта объект Openspot из папки Prefabs.

В Inspector нажмите на кружок справа от поля Monster Prefab компонента PlaceMonster (Script) и выберите в появившемся диалоговом окне Monster.


Вот и всё. Запустите сцену и создавайте монстров на разных местах нажатием мыши или касанием экрана.


Отлично! Теперь мы можем создавать монстров. Однако они выглядят как странная каша, потому что отрисовываются все дочерние спрайты монстра. Сейчас мы это исправим.

Повышаем уровень монстров


На рисунке ниже показано, что с повышением уровня монстры выглядят всё более устрашающе.


Какой милаха! Но если вы попробуете украсть его печенье, этот монстр превратится в убийцу.

Скрипт используется в качестве основы для реализации системы уровней монстров. Он отслеживает силу монстра на каждом уровне и, разумеется, текущий уровень монстра.

Добавим этот скрипт.

Выберите в Project Browser префаб Prefabs/Monster. Добавьте новый скрипт C# с названием MonsterData. Откройте скрипт в IDE и добавьте следующий код выше класса MonsterData.

[System.Serializable]
public class MonsterLevel
{
  public int cost;
  public GameObject visualization;
}

Так мы создаём MonsterLevel. В нём группируются цена (в золоте, которое мы будем поддерживать ниже) и визуальное представление уровня монстра.

Мы добавляем сверху [System.Serializable], чтобы экземпляры класса можно было изменять в инспекторе. Это позволяет нам быстро менять все значения класса Level, даже когда игра запущена. Это невероятно полезно для балансировки игры.

Задание уровней монстров


В нашем случае мы будем хранить заданный MonsterLevel в List<T>.

Почему бы просто не использовать MonsterLevel[]? Нам несколько раз понадобится индекс конкретного объекта MonsterLevel. Хотя несложно написать для этого код, нам всё равно придётся использовать IndexOf(), реализующий функционал Lists. Нет смысла изобретать велосипед.


Изобретать велосипед заново — обычно плохая идея.

В верхней части MonsterData.cs добавим следующую конструкцию using:

using System.Collections.Generic;

Она даёт нам доступ к обобщённым структурам данных, чтобы мы могли использовать в скрипте класс List<T>.

Примечание: обобщения — это мощная концепция C#. Они позволяют задавать типобезопасные структуры данных, не придерживаясь типа. Это удобно для таких классов-контейнеров, как списки и множества. Чтобы подробнее узнать про обобщённые структуры, прочитайте книгу Introduction to C# Generics.

Теперь добавим следующую переменную в MonsterData для хранения списка MonsterLevel:

public List<MonsterLevel> levels;

Благодаря обобщениям мы можем гарантировать, что List из level будет содержать только объекты MonsterLevel.

Сохраните файл и переключитесь в Unity, чтобы настроить каждый уровень.

Выберите Prefabs/Monster в Project Browser. В Inspector теперь отображается поле Levels компонента MonsterData (Script). Задайте для параметра size значение 3.


Далее зададим стоимость для каждого уровня:

  • Element 0: 200
  • Element 1: 110
  • Element 2: 120

Теперь назначим значения полей визуального отображения.

Развернём Prefabs/Monster в Project browser, чтобы видеть его дочерние элементы. Перетащите дочерний Monster0 в поле visualization Element 0.

Далее назначим Element 1 значение Monster1, а Element 2 значение Monster2. В GIF показан этот процесс:


Когда вы выбираете Prefabs/Monster, префаб должен выглядеть так:


Задание текущего уровня


Вернитесь к MonsterData.cs в IDE и добавьте к MonsterData ещё одну переменную.

private MonsterLevel currentLevel;

В частной переменной currentLevel мы будем хранить текущий уровень монстра.

Теперь зададим currentLevel и сделаем его видимым для других скриптов. Добавьте следующие строки в MonsterData вместе с объявлением переменных экземпляра:

//1
public MonsterLevel CurrentLevel
{
  //2
  get 
  {
    return currentLevel;
  }
  //3
  set
  {
    currentLevel = value;
    int currentLevelIndex = levels.IndexOf(currentLevel);

    GameObject levelVisualization = levels[currentLevelIndex].visualization;
    for (int i = 0; i < levels.Count; i++)
    {
      if (levelVisualization != null) 
      {
        if (i == currentLevelIndex) 
        {
          levels[i].visualization.SetActive(true);
        }
        else
        {
          levels[i].visualization.SetActive(false);
        }
      }
    }
  }
}

Довольно большой кусок кода на C#, правда? Давайте разберём его по порядку:

  1. Задаём свойство частной переменной currentLevel. Задав свойство, мы сможем вызывать её как любую другую переменную: или как CurrentLevel (внутри класса) или как monster.CurrentLevel (за его пределами). Мы можем определить в методе геттера или сеттера свойства любое поведение, а создавая только геттер, сеттер или их обоих, можно управлять характеристиками свойства: только для чтения, только для записи и для записи/чтения.
  2. В геттере мы возвращаем значение currentLevel.
  3. В сеттере мы присваиваем currentLevel новое значение. Затем мы получаем индекс текущего уровня. Наконец, мы проходим в цикле по всем уровням и включаем/отключаем визуальное отображение в зависимости от currentLevelIndex. Это отлично, потому что при изменении currentLevel спрайт обновляется автоматически. Свойства — это очень удобная штука!

Добавим следующую реализацию OnEnable:

void OnEnable()
{
  CurrentLevel = levels[0];
}

Здесь мы при размещении задаём CurrentLevel. Это гарантирует, что будет показан только нужный спрайт.

Примечание: важно инициализировать свойство в OnEnable, а не в OnStart, потому что мы вызываем порядковые методы при создании экземпляров префабов.

OnEnable будет вызван сразу же при создании префаба (если префаб был сохранён в состоянии enabled), но OnStart не вызывается, пока объект не начинает выполняться как часть сцены.

Нам необходимо проверять эти данные до размещения монстра, поэтому мы инициализируем их в OnEnable.

Сохраните файл и вернитесь в Unity. Запустите проект и расположите монстров; теперь у них отображаются правильные спрайты самого нижнего уровня.


Апгрейд монстров


Вернитесь в IDE и добавьте в MonsterData следующий метод:

public MonsterLevel GetNextLevel()
{
  int currentLevelIndex = levels.IndexOf (currentLevel);
  int maxLevelIndex = levels.Count - 1;
  if (currentLevelIndex < maxLevelIndex)
  {
    return levels[currentLevelIndex+1];
  } 
  else
  {
    return null;
  }
}

В GetNextLevel мы получаем индекс currentLevel и индекс наивысшего уровня; если монстр не достиг максимального уровня, то возвращается следующий уровень. В противном случае возвращается null.

Можно использовать этот метод, чтобы узнать, возможен ли апгрейд монстра.

Для повышения уровня монстра добавьте следующий метод:

public void IncreaseLevel()
{
  int currentLevelIndex = levels.IndexOf(currentLevel);
  if (currentLevelIndex < levels.Count - 1)
  {
    CurrentLevel = levels[currentLevelIndex + 1];
  }
}

Здесь мы получаем индекс текущего уровня, а затем убеждаемся, что это не максимальный уровень, проверяя, что он меньше levels.Count - 1. Если это так, то присваиваем CurrentLevel значение следующего уровня.

Проверяем функционал апгрейдов


Сохраните файл и вернитесь к PlaceMonster.cs в IDE. Добавьте новый метод:

private bool CanUpgradeMonster()
{
  if (monster != null)
  {
    MonsterData monsterData = monster.GetComponent<MonsterData>();
    MonsterLevel nextLevel = monsterData.GetNextLevel();
    if (nextLevel != null)
    {
      return true;
    }
  }
  return false;
}

Сначала проверяем, есть ли монстр, которого можно улучшить, сравнивая переменную monster с null. Если это верно, то мы получаем текущий уровень монстра из его MonsterData.

Затем мы проверяем, доступен ли следующий уровень, то есть не возвращает ли GetNextLevel() значение null. Если повышение уровня возможно, то мы возвращаем true; в противном случае возвращаем false.

Реализуем улучшения за золото


Чтобы включить опцию апгрейда, добавим к OnMouseUp ветвь else if:

if (CanPlaceMonster())
{
  // Здесь код остаётся таким же
}
else if (CanUpgradeMonster())
{
  monster.GetComponent<MonsterData>().IncreaseLevel();
  AudioSource audioSource = gameObject.GetComponent<AudioSource>();
  audioSource.PlayOneShot(audioSource.clip);
  // TODO: вычитать золото
}

Проверяем возможность апгрейда с помощью CanUpgradeMonster(). Если возможен, то получаем доступ к компоненту MonsterData с помощью GetComponent() и вызываем IncreaseLevel(), который увеличивает уровень монстра. Наконец, мы запускаем AudioSource монстра.

Сохраните файл и вернитесь в Unity. Запустите игру разместите и улучшите любое количество монстров (но это пока).


Платим золотом — Game Manager


Пока мы можем сразу же построить и улучшить любых монстров, но разве это будет интересно в игре?

Давайте рассмотрим вопрос золота. Проблема с его отслеживанием заключается в том, что нам придётся передавать информацию между разными игровыми объектами.

На рисунке ниже показаны все объекты, которые должны принимать в этом участие.


Все выделенные игровые объекты должны знать, сколько золота есть у игрока.

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

Нажмите правой клавишей на Hierarchy и выберите Create Empty. Назовите новый объект GameManager.

Добавьте к GameManager новый скрипт C# с названием GameManagerBehavior, а затем откройте его в IDE. Мы будем отображать общее количество золота игрока в метке, поэтому в верхней части файла добавьте следующую строку:

using UnityEngine.UI;

Это позволит нам получить доступ к классам UI наподобие Text, который используется для меток. Теперь добавим в класс следующую переменную:

public Text goldLabel;

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

Теперь, когда GameManager знает о метке, как нам синхронизировать количество хранящегося в переменной золота и отображаемой в метке величины? Мы создадим свойство.

Добавьте в GameManagerBehavior следующий код:

private int gold;
public int Gold {
  get
  { 
    return gold;
  }
  set
  {
    gold = value;
    goldLabel.GetComponent<Text>().text = "GOLD: " + gold;
  }
}

Он кажется знакомым? Код похож на CurrentLevel, который мы задали в Monster. Сначала мы создаём частную переменную gold для хранения текущей суммы золота. Затем мы задаём свойство Gold (неожиданно, правда?) и реализуем геттер и сеттер.

Геттер просто возвращает значение gold. Сеттер более интересен. Кроме задания значения переменной он также задаёт поле text для goldLabel, чтобы отображать новую величину золота.

Насколько щедрыми мы будем? Добавьте в Start() следующую строку, чтобы дать игроку 1000 золота, или меньше, если вам жалко денег:

Gold = 1000;

Назначение объекта метки скрипту


Сохраните файл и вернитесь в Unity. В Hierarchy выберите GameManager. В Inspector нажмите на круг справа от Gold Label. В диалоговом окне Select Text выберите вкладку Scene и выберите GoldLabel.


Запустите сцену и в метке отобразится Gold: 1000.


Проверяем «кошелёк» игрока


Откройте в IDE скрипт PlaceMonster.cs и добавьте следующую переменную экземпляра:

private GameManagerBehavior gameManager;

Мы воспользуемся gameManager для получения доступа к компоненту GameManagerBehavior объекта GameManager сцены. Чтобы задать её, добавьте в Start() следующее:

gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();

Мы получаем GameObject с названием GameManager с помощью функции GameObject.Find(), которая возвращает первый найденный игровой объект с таким именем. Затем получаем его компонент GameManagerBehavior и сохраняем его на будущее.

Примечание: можно сделать это, задав поле в редакторе Unity или добавив статический метод в GameManager, возвращающий экземпляр синглтона, из которого мы можем получить GameManagerBehavior.

Однако в показанном выше блоке кода есть тёмная лошадка: метод Find, который медленнее работает в процессе выполнения приложения; но он удобен и его можно применять в умеренных количествах.

Возьми мои деньги!


Мы пока не вычитаем золото, поэтому дважды добавим внутрь OnMouseUp() эту строку, заменив каждый из комментариев // TODO: вычитать золото:

gameManager.Gold -= monster.GetComponent<MonsterData>().CurrentLevel.cost;

Сохраните файл и вернитесь в Unity, улучшите несколько монстров и посмотрите на обновление значения Gold. Теперь мы вычитаем золото, но игроки могут строить монстров, пока им хватает места; они просто берут деньги в долг.


Бесконечный кредит? Отлично! Но мы не можем этого позволить. Игрок должен иметь возможность ставить монстров, пока у него достаточно золота.

Проверка золота для монстров


Переключитесь в IDE на PlaceMonster.cs и замените содержимое CanPlaceMonster() следующим:

int cost = monsterPrefab.GetComponent<MonsterData>().levels[0].cost;
return monster == null && gameManager.Gold >= cost;

Получаем цену размещения монстра из levels в его MonsterData. Затем проверяем, что monster не равно null, и что gameManager.Gold больше этой цены.

Задание для вас: самостоятельно добавьте в CanUpgradeMonster() проверку, достаточно ли у игрока золота.

Решение внутри
Замените строку:

return true;

на такую:

return gameManager.Gold >= nextLevel.cost;

Она будет проверять, больше ли у игрока Gold, чем цена апгрейда.

Сохраните и запустите сцену в Unity. Теперь попробуйте-те как неограниченно добавлять монстров!


Теперь мы можем строить только ограниченное количество монстров.

Башенная политика: враги, волны и точки маршрута


Настало время «проложить дорогу» нашим врагам. Враги появляются на первой точке маршрута, движутся к следующей и повторяют процесс, пока не дойдут до печенья.

Заставить врагов двигаться можно так:

  1. Задать дорогу, по которой будут идти враги
  2. Перемещать врага по дороге
  3. Поворачивать врага, чтобы он смотрел вперёд

Создание дороги из точек маршрута


Нажмите правой клавишей на Hierarchy и выберите Create Empty, чтобы создать новый пустой игровой объект. Назовите его Road и расположите в точке (0, 0, 0).

Теперь нажмите правой клавишей на Road в Hierarchy и создайте ещё один пустой игровой объект как дочерний элемент Road. Назовите его Waypoint0 и разместите в точке (-12, 2, 0) — отсюда враги будут начинать своё движение.


Аналогичным образом создайте ещё пять точек маршрута со следующими названиями и позициями:

  • Waypoint1: (X:7, Y:2, Z:0)
  • Waypoint2: (X:7, Y:-1, Z:0)
  • Waypoint3: (X:-7.3, Y:-1, Z:0)
  • Waypoint4: (X:-7.3, Y:-4.5, Z:0)
  • Waypoint5: (X:7, Y:-4.5, Z:0)

На скриншоте ниже показаны точки маршрута и получившийся путь.


Создание врагов


Теперь создадим несколько врагов, чтобы они могли двигаться по дороге. В папке Prefabs есть префаб Enemy. Его позиция равна (-20, 0, 0), поэтому новые экземпляры будут создаваться за пределами экрана.

Во всём остальном он настраивается почти так же, как префаб Monster, имеет AudioSource и дочерний Sprite, и этот спрайт мы сможем поворачивать в дальнейшем, не поворачивая полосу здоровья.


Двигаем врагов по дороге


Добавьте новый скрипт C# с названием MoveEnemy к префабу Prefabs\Enemy. Откройте скрипт в IDE и добавьте следующие переменные:

[HideInInspector]
public GameObject[] waypoints;
private int currentWaypoint = 0;
private float lastWaypointSwitchTime;
public float speed = 1.0f;

В waypoints в массиве хранится копия точек маршрута, а строка [HideIninspector] над waypoints гарантирует, что мы не сможем случайно изменить это поле в Inspector, но по-прежнему будем иметь доступ к нему из других скриптов.

currentWaypoint отслеживает, из какой точки маршрута идёт враг в текущий момент времени, а в lastWaypointSwitchTime хранится время, когда враг прошёл по ней. Кроме того, мы храним скорость speed врага.

Добавим эту строку в Start():

lastWaypointSwitchTime = Time.time;

Так мы инициализируем lastWaypointSwitchTime со значением текущего времени.

Чтобы враг двигался вдоль маршрута, добавим в Update() следующий код:

// 1 
Vector3 startPosition = waypoints [currentWaypoint].transform.position;
Vector3 endPosition = waypoints [currentWaypoint + 1].transform.position;
// 2 
float pathLength = Vector3.Distance (startPosition, endPosition);
float totalTimeForPath = pathLength / speed;
float currentTimeOnPath = Time.time - lastWaypointSwitchTime;
gameObject.transform.position = Vector2.Lerp (startPosition, endPosition, currentTimeOnPath / totalTimeForPath);
// 3 
if (gameObject.transform.position.Equals(endPosition)) 
{
  if (currentWaypoint < waypoints.Length - 2)
  {
    // 3.a 
    currentWaypoint++;
    lastWaypointSwitchTime = Time.time;
    // TODO: поворачиваться в направлении движения
  }
  else
  {
    // 3.b 
    Destroy(gameObject);

    AudioSource audioSource = gameObject.GetComponent<AudioSource>();
    AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
    // TODO: вычитать здоровье
  }
}

Разберём код пошагово:

  1. Из массива точек маршрута мы получаем начальную и конечную позиции текущего сегмента маршрута.
  2. Вычисляем время, необходимое для прохождения всего расстояния с помощью формулы время = расстояние / скорость, а затем определяем текущее время на маршруте. С помощью Vector2.Lerp, мы интерполируем текущую позицию врага между начальной и конечной точной сегмента.
  3. Проверяем, достиг ли враг endPosition. Если да, то обрабатываем два возможных сценария:
    1. Враг пока не дошёл до последней точки маршрута, поэтому увеличиваем значение currentWaypoint и обновляем lastWaypointSwitchTime. Позже мы добавим код для поворота врага, чтобы он смотрел в направлении своего движения.
    2. Враг достиг последней точки маршрута, тогда мы уничтожаем его и запускаем звуковой эффект. Позже мы добавим код уменьшающий health игрока.

Сохраните файл и вернитесь в Unity.

Сообщаем врагам направление движения


В своём текущем состоянии враги не знают порядка точек маршрута.

Выберите Road в Hierarchy и добавьте новый скрипт C# под названием SpawnEnemy. Откройте его в IDE и добавьте следующую переменную:

public GameObject[] waypoints;

Мы будем использовать waypoints для хранения ссылок на точку маршрута в сцене в нужном порядке.

Сохраните файл и вернитесь в Unity. Выберите Road в Hierarchy и задайте Size массива Waypoints равным 6.

Перетащите каждый из дочерних элементов Road в поля, вставив Waypoint0 в Element 0, Waypoint1 в Element 1 и так далее.


Теперь у нас есть массив, содержащий точки маршрута в правильном порядке — заметьте, враги никогда не отступают, они упорно стремятся к сладкой награде.

Проверяем, как всё это работает


Откройте в IDE SpawnEnemy и добавьте следующую переменную:

public GameObject testEnemyPrefab;

В ней будет храниться ссылка на префаб Enemy в testEnemyPrefab.

Чтобы создать врага при запуске скрипта, добавим в Start() следующий код:

Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>().waypoints = waypoints;

Так мы создадим новую копию префаба, хранящуюся в testEnemy, и назначим ей маршрут следования.

Сохраните файл и вернитесь в Unity. Выберите в Hierarchy объект Road и выберите для параметра Test Enemy префаб Enemy.

Запустите проект и посмотрите, как враг движется по дороге (в GIF для большей наглядности скорость увеличена в 20 раз).


Заметили, что он не всегда смотрит туда, куда идёт? Это забавно, но мы ведь пытаемся сделать профессиональную игру. Поэтому во второй части туториала мы научим врагов смотреть вперёд.

Куда двигаться дальше?


Мы уже сделали многое и быстро движемся к созданию собственной игры в жанре tower defense.

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

Скачайте готовый результат отсюда.

Во второй части мы рассмотрим создание огромных волн врагов и их уничтожение. До встречи!
  • +29
  • 6,8k
  • 3
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 3
  • 0
    В двух из трех статей или книг по Юнити вся логика взаимодействия обязательно завязывается на GameManager, который при сколько-нибудь заметном наращивании объемов превращается антипаттерн GodObject. Если нанимаешь нового юнитиста, обязательно приходится переучивать не делать эти GameManager.
    • 0
      Если нанимаешь нового юнитиста, обязательно приходится переучивать не делать эти GameManager.

      Интересно почитать, что же вы делаете вместо них) (не сарказм)
      • 0
        Если вкратце, то мы используем MVP — бизнес-логика и логика отображений уходит в unity-независимые слои, на компонентах остается только относительно тонкий view-слой, который практически не принимает решений, только визуализирует изменения в слое презентеров. Отдельно, правда, приходится оговаривать физику и навигацию — у них довольно сложное положение в этой иерархии.

        Также очень хорошие результаты показывает ECS-подход, но мы пока недостаточно осмелели для полного перехода на него.

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

    Самое читаемое