Продолжаем создавать 2D игру на Unity в стиле Tower Defense, начатую в 2D игра на Unity в стиле Tower Defense. Часть 1.
Золото – еще одна фишка в игре
Сейчас это можно построить и модернизировать всех монстров сразу, но где тогда интерес? Разберемся с золотом. Сложность с ним заключается в том, что нужно устроить обмен информацией между различными игровыми объектами. На следующем рисунке показаны все объекты, которые должны в этом задействоваться.
Для хранения этой информации вы будете использовать общий объект, у которого есть доступ к остальным. Кликните правой кнопкой мыши на Иерархии и выберите Create Empty. Назовите новый игровой объект GameManager. Добаввте C#-скрипт по имени GameManagerBehavior в GameManager, затем откройте новый сценарий в MonoDevelop. Вы будете показывать сколько золота у игрока в ярлыке, поэтому добавьте следующую строку в верхней части файла:
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. В Иерархии выберите GameManager. В Инспекторе, нажмите на круг справа от Gold Label. В диалоге Select Text перейдите на вкладку Scene (Сцена) и выберите GoldLabel.
Запутите игру и вы увидите Gold: 1000.
Проверяем “кошелек” игрока
Откройте файл PlaceMonster.cs в MonoDevelop и добавьте следующую переменную экземпляра:
private GameManagerBehavior gameManager;
Вы будете использовать gameManager
, чтобы получить доступ к GameManagerBehavior - компоненту сцены GameManager. Для того, чтобы назначить его, добавьте следующие строки в Start()
:
gameManager =
GameObject.Find("GameManager")
.GetComponent<GameManagerBehavior>();
Вы получаете игровой объект по имени GameManager, используя GameObject.Find(), который возвращает первый найденный игровой объект с данным именем. Затем извлечь его коипонент GameManagerBehavior и сохраняем его для последующего использования.
Вы могли бы сделать это, установив поле в редакторе Unity, либо путем добавления статического метода в GameManager, который возвращает одноэлементный экземпляр, из которого вы могли бы получить GameManagerBehavior. Однако, есть метод Find, который работает чуть медленнее, но очень удобен и может использоваться довольно экономно.
Нужны деньги!
Золото пока не уменьшается, так что добавьте эту строку дважды внутри OnMouseUp()
, замените каждый из комментариев, где написано // СДЕЛАТЬ: уменьшить золото
:
gameManager.Gold -= monster.GetComponent<MonsterData>()
.CurrentLevel.cost;
Сохраните файл и переключитесь в Unity, улучшайте монстров и смотрите за изменением золота. Оно уменьшается, но игроки могут расставлять монстров до тех пор, пока существует свободное место, при этом они просто влезают в долги.
Бесконечный кредит? Потрясающе, но вы не можете этого позволить! Монстры должны быть размещены только тогда, когда игрок имеет достаточное количество золота.
Требуется золото для монстров
Переключесь на PlaceMonster.cs в MonoDevelop и замените содержимое 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;
Мы проверяем по средствам ли игроку модернизация монстра. Сохраните файл, запустите сцену в Unity и попробуйте разместить неограниченное количество монстров!
Управление башней: Враги, Волны и Вейпоинты
Пора "проложить дорогу" для ваших врагов. Враги появляются в первой точке маршрута, переходят к следующей и так до тех пор, пока они не достигнут вашего печенья. Враги будут двигаться так:
- По определенной для них дороге
- Поворачиваясь лицом по направлению движения
Создаем дорогу с вейпоинтами в Unity
Нажните правой кнопкой мыши по Иерархии и выберите Create Empty, чтобы создать новый игровой объект. Назовите его Road и убедитесь, что он спозиционирован в (0, 0, 0)
. Снова щелкните правой кнопкой мыши, но на этот раз по Road в Иерархии и создайте еще один пустой игровой объект как потомок Road Назовите его Waypoint0 и установите его позицию (-12, 2, 0)
— это место, откуда враги начинают движение.
Создайте еще пять путевых точек таким же образом со следующими именами и позициями: Waypoint1: (7, 2, 0)
, Waypoint2: (7, -1, 0)
, Waypoint3: 7.3, -1, 0)
, Waypoint4: (-7.3, -4.5, 0)
, Waypoint5: (7, -4.5, 0)
. На скриншоте ниже вы можете увидеть положения путевых точек и результирующий путь.
Появление врагов
Теперь сделаем, чтобы враги следовали по дороге. Папка Prefabs содержит префаб Enemy. Его позиция (-20, 0, 0), поэтому новые объекты будут появляться за пределами экрана. В остальном он настроен схожим образом на префаб монстра AudioSource
и Sprite
и это спрайт, поэтому позже вы сможете повернуть его не поворачивая индикатор здоровья.
Перемещение монстров вниз по дороге
Добавьте новый C#-скрипт по имени MoveEnemy в префаб Prefabs\Enemy. Откройте скрипт в MonoDevelop и добавьте следующие переменные:
[HideInInspector]
public GameObject[] waypoints;
private int currentWaypoint = 0;
private float lastWaypointSwitchTime;
public float speed = 1.0f;
waypoints хранит копию путевых точек в массиве, в то время как [HideInInspector] (тот, что выше waypoints) гарантирует, что вы не можете случайно изменить поле в Инспекторе, но вы все еще можете получить доступ к нему из других скриптов. 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 = Vector3.Lerp (startPosition,
endPosition,
currentTimeOnPath / totalTimeForPath);
// 3
if (gameObject.transform.position.Equals(endPosition)) {
if (currentWaypoint < waypoints.Length - 2) {
// 3.a
currentWaypoint++;
lastWaypointSwitchTime = Time.time;
// Предстоит сделать: поворот при изменении направления движения
} else {
// 3.b
Destroy(gameObject);
AudioSource audioSource = gameObject.GetComponent<AudioSource>();
AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
// Предстоит сделать: уменьшать величину здоровья
}
}
Поясним, что тут происходит шаг за шагом:
- Из массива путевых точек вы извлекаете начальную и конечную позицию для текущего сегмента пути.
- Вычисляем время, необходимое, чтобы пройти весь путь по формуле
time = distance / speed
, затем определяем текущее время в пути. С помощью Vector3.Lerp
вы интерполируете текущую позицию противника между начальными и конечными точками сегмента.
- Проверяем, дошел ли враг до конечной точки
endPosition
. Если да, то рассматриваем следующие два возможных сценария:
- Враг еще не у последнего вейпоинта, тогда повышаем значение
currentWaypoint
и обновляем lastWaypointSwitchTime
. Позже мы добавим код, чтобы повернуть врага в ту сторону, в которую он движется.
- Противник достиг последней точки, поэтоу удаляем его и проигрываем звук. Позже мы добавим код для уменьшения
health
(здоровьч) игрока.
Сохраните файл и переключитесь в Unity.
Учим врагов ориентироваться
В своем нынешнем состоянии враги не знают про порядок следования путевых точек. Выберите Road в Иерархии и добавьте новый C#-скрипт по имени SpawnEnemy. Откройте скрипт в MonoDevelop и добавьте следующую переменную:
public GameObject[] waypoints;
Вы будете использовать waypoints
для хранения ссылок на путевые точки в сцене в правильном порядке. Сохраните файл и переключитесь на Unity. Выберите Road в Иерархии и установите Size массива Waypoints равным 6
. Перетащите каждого потомка Road в соответствующее поле: Waypoint0 в Element 0, Waypoint1 в Element 1 и т.д.
Теперь у вас есть массив, который содержит аккуратно упорядоченные путевые точки, поэтому есть путь, но важно знать, что враги не отступят, они умрут, но пойдут до конца.
Проверим, что у нас все работает. Вверху SpawnEnemy в MonoDevelop добавьте следующую переменную:
public GameObject testEnemyPrefab;
Она сохраняет ссылку на префаб Enemy в testEnemyPrefab
. Для того, чтобы создать противника при запуске сценария, добавьте следующий код в Start()
:
Instantiate(testEnemyPrefab).GetComponent<MoveEnemy>()
.waypoints = waypoints;
Это создает новый экземпляр префаба, хранящегося в testEnemy
и присваивает вейпоинты. Сохраните файл и переключитесь в Unity. Выбрите Road в Иерархии и установите Test Enemy в префаб Enemy. Запустите проект, чтобы посмотреть, как двигаются враги.
Подведение итогов
Нами была проделана большая работа по созданию 2D игры в Unity: игрок может в ограниченном количестве создавать монстро-башни, уничтожающие бегущих к вашему печенью врагов. У игрока есть золото, кроме того, он может модернизировать монстров. Загрузить итоговый результат вы можете отсюда.
В третьей части мы поговорим о том, как создавать сразу множество врагов, а также о борьбе с ними.