Главная » Хабрахабр » [Перевод] Создание игры Tower Defense в Unity — Часть 1

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

image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Авторство:

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

.

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

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

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

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

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

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

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

Теперь, когда у нас есть заготовка-префаб, мы можем создать сколько угодно копий. Просто перетащите 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.

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

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

public GameObject monsterPrefab;
private GameObject monster;

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

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

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

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

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

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

//1
void OnMouseUp()

}

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

  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.

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

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

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

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

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

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

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

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

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

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

Serializable], чтобы экземпляры класса можно было изменять в инспекторе. Мы добавляем сверху [System. Это невероятно полезно для балансировки игры. Это позволяет нам быстро менять все значения класса 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, чтобы настроить каждый уровень.

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

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

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

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

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

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

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

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

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

private MonsterLevel currentLevel;

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

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

//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.

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

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

Чтобы включить опцию апгрейда, добавим к 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

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

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

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

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

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

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

Мы будем отображать общее количество золота игрока в метке, поэтому в верхней части файла добавьте следующую строку: Добавьте к 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).

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

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

  • 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, но по-прежнему будем иметь доступ к нему из других скриптов.

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

Добавим эту строку в 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.

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

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

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

public GameObject[] waypoints;

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

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

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

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

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

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

public GameObject testEnemyPrefab;

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

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

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

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

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

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

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

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

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

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

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

До встречи! Во второй части мы рассмотрим создание огромных волн врагов и их уничтожение.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Всемирная организация здравоохранения официально признала существование игровой зависимости

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

[Перевод] Конструкция async/await в JavaScript: сильные стороны, подводные камни и особенности использования

Конструкция async/await появилась в стандарте ES7. Её можно считать замечательным улучшением в сфере асинхронного программирования на JavaScript. Она позволяет писать код, который выглядит как синхронный, но используется для решения асинхронных задач и не блокирует главный поток. Несмотря на то, что ...