Всем привет! Это вторая часть урока для новичков о том, как создать маленькую игру жанра Tower Defence на движке Unity. Мы остановились на создании скрипта для спауна крипов. Если интересно, прошу под кат.



Часть 1 ? Часть 2

Скрипт «Spawner»


Вот так выглядит шаблон кода при создании нового скрипта:

using UnityEngine;

public class Spawner : MonoBehaviour
{
   void Start()
   {
   }
   
   void Update()
   {
   }
}

Объявим публичную переменную хранящую образец объекта для клонирования.

public GameObject spawnObject;

Перейдём в редактор Unity, и перетащим скрипт на объект Spawner.


Этими действиями мы привязали скрипт к объекту. Так как переменная spawnObject с публичным модификатором, её мы сможем изменять прямо в редакторе Unity через окно инспектора.


Вернёмся к коду. Объявим ещё одну публичную переменную. Она будет отвечать за время в секундах, через которое произойдёт создание нового крипа. Также объявим приватную переменную timer.

public float spawnTime = 1f;
private float timer = 0;

С каждым кадром переменная timer будет уменьшать своё значение на дельту времени, пока не достигнет нуля или отрицательного значения. После чего, она примет значение переменной spawnTime и создаст нового крипа на сцене. И всё начнётся по новой.

Реализуем вышесказанное в методе Update:

timer -= Time.deltaTime;

if (timer <= 0)
{
   Instantiate(spawnObject, transform.position, transform.rotation);
   timer = spawnTime;
}

Функция Instantiate создаёт копию объекта и возвращает на него ссылку. В данной перегрузке принимает аргументы: ссылка на оригинальный объект, позиция и вращение для вставки нового объекта.

Весь код скрипта Spawner будет выглядеть так:

using UnityEngine;

public class Spawner : MonoBehaviour
{
   public GameObject spawnObject;
   public float spawnTime = 1f;

   private float timer = 0;

   void Start()
   {
   }
   
   void Update()
   {
      timer -= Time.deltaTime;

      if (timer <= 0)
      {
         Instantiate(spawnObject, transform.position, transform.rotation);
         timer = spawnTime;
      }
   }
}

Крипы (противники)


Сохраним проект и перейдём в редактор Unity. Создадим новый объект сферу. Именно так будут выглядеть крипы в моём уроке. Занулим позицию. Немного уменьшим сферу по всем 3-м осям.


В папке материалов создадим новый материал. Назовём его Enemy — «Противник».



Я задам ему красный цвет. Переместим материал на сферу. Теперь создадим новый скрипт для того чтобы вдохнуть в крипа жизнь.


Переименуем сферу в Enemy и перетащим на неё одноимённый скрипт.


Любой уважающий себя крип должен двигаться к цели


Переходим к коду. Так как структура кода проекта была изменена в редакторе Unity, нам предлагают перезагрузить проект или выбрать другое действие.


Выберем перезагрузку и откроем скрипт Enemy. Объявим приватные переменные. Первая для хранения объекта содержащий вэйпоинты. Вторая для хранения трансформации текущего вэйпоинта (точки пути).

private Transform waypoints;
private Transform waypoint;

Именно к текущему вэйпоинту крип будет идти, пока собственно не дойдёт. После чего он выберет следующую точку и направится уже к ней. Объявим ещё одну переменную.

private int waypointIndex = -1;

Она будет хранить порядковый номер текущей точки пути. Зададим начальное значение -1. Почему так, узнаете дальше.

В методе Start напишем код для поиска объекта на сцене с именем WayPoints и установки первого вэйпоинта в качестве текущего:

waypoints = GameObject.Find("WayPoints").transform;
NextWaypoint();

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

void NextWaypoint()
{
   // Тут был код…
}

В теле метода инкрементируем порядковый номер точки:

waypointIndex++;

Далее, проверяем, больше или равно ли значение индекса от кол-ва точек пути. Если условие верно, то уничтожаем объект на сцене и прерываем выполнение метода.

if (waypointIndex >= waypoints.childCount)
{
   Destroy(gameObject);
   return;
}

После блока if, переменной waypoint нужно присвоить точку пути по указанному порядковому номеру:

waypoint = waypoints.GetChild(waypointIndex);

Добавим публичную переменную скорости движения крипа:

public float speed = 2f;

В теле метода Update, напишем код движения крипа к текущей точке пути:

Vector3 dir = waypoint.transform.position - transform.position;
dir.y = 0;

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

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

float _speed = Time.deltaTime * speed;

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

transform.Translate(dir.normalized * _speed);

Теперь нужно поставить условие. Если длина вектора меньше или равна длине шага, то это сигнал к смене точки пути.

if (dir.magnitude <= _speed)
   NextWaypoint();

В итоге весь код скрипта Enemy должен выглядеть так:

using UnityEngine;

public class Enemy : MonoBehaviour
{
   public float speed = 2f;

   private Transform waypoints;
   private Transform waypoint;
   private int waypointIndex = -1;

   void Start()
   {
      waypoints = GameObject.Find("WayPoints").transform;
      NextWaypoint();
   }
   
   void Update()
   {
      Vector3 dir = waypoint.transform.position - transform.position;
      dir.y = 0;

      float _speed = Time.deltaTime * speed;
      transform.Translate(dir.normalized * _speed);

      if (dir.magnitude <= _speed)
         NextWaypoint();
   }

   void NextWaypoint()
   {
      waypointIndex++;

      if (waypointIndex >= waypoints.childCount)
      {
         Destroy(gameObject);
         return;
      }

      waypoint = waypoints.GetChild(waypointIndex);
   }
}

Последние приготовления


Сохраняем код и возвращаемся к редактору Unity. Скрипту Spawner, который привязан к одноимённому объекту, нужно указать образец для клонирования. Образцом будет префаб Enemy, которого нет… Создадим его, перетащив объект Enemy из окна Иерархии в папку префабов. Оригинал удалим со сцены.


Теперь можно установить образец, присвоив переменной spawnObject префаб крипа.


Отключим коллизию у объекта Spawner.


Сохраняемся и смотрим что получилось.


Свеженькое мясо пошло по конвееру…

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


P. S.: Если у вас есть вопросы или пожелания, оставляйте свои комментарии на YouTube под видео.
На хабре я комментарии не читаю.

Многие заметили, что статья и видео похожи на уроки с этого канала. От части это так. Но я позаимствовал только лишь расстановку вэйпоинтов и построение уровня.

Спасибо за внимание! Конец второй части.
Поделиться с друзьями
-->

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


  1. Tutanhomon
    09.09.2016 11:12
    +2

    Годный туториал, если говорить о «игре, сделанной на коленке просто потренироваться и выкинуть».
    Но если говорить о серьезных играх (даже мелочи типа TD) то это уроки о том как делать не нужно. И упомянутые в статье видео, к слову, тоже качеством не блещут.
    Проблема подобных туториалов в том, что новички, насмотревшись на них, идут и делают так же, принимая все вышеказаное за «Best Practices».
    Потому думают, почему у них долго инициализирется сцена (пустые методы, типа Start, Awake), почемуу них низки fps (обращение к компонентам без кеширования), и вообще, почему в годе происходит непноятная «магия» (потому что оставили поле public, только для того, чтобы оно было видно в инспекторе, всесто использования private + SerializeField, и кто-то из лени записал в это поле свое значение). Я уже молчу об общепринятых Style и Naming Conventions.


    1. PoliTeX
      09.09.2016 15:08

      А разве компилятор не выбросит пустые методы?


      1. Tutanhomon
        09.09.2016 15:51

        Компилятор не сможет его выбросить, потому что Юнити на него ссылается. Когда скрипт добавляется в проект Юнити сохраняет список «этих самых» методов для каждого монобеха и потом их вызывает.
        https://blogs.unity3d.com/2015/12/23/1k-update-calls/


        1. PoliTeX
          09.09.2016 16:36

          Полезная статья, спасибо.


  1. Leopotam
    09.09.2016 11:46
    +2

    Многие заметили, что статья и видео похожи на уроки с этого канала. От части это так. Но я позаимствовал только лишь расстановку вэйпоинтов и построение уровня.

    Это не так, повзаимствованы все косяки и нежелание их исправлять, т.е. статья просто перевод — так ее и надо оформлять. В комментах к прошлому переводу были ссылки на best practices. Что в итоге? На крипах опять колайдер без Rigidbody, двигаются они в Update, а спаунятся и умирают бесконтрольно (привет, фризы на GC).


    1. Kassil
      14.09.2016 11:56

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


      1. Leopotam
        14.09.2016 12:00
        +1

        Все, что связано с физикой — может двигаться только в FixedUpdate (частота вызовов FixedUpdate соответствует частоте симуляции физики). Цепочка проста: используем двигающиеся колайдеры -> используем rigidbody на них -> двигаем только в FixedUpdate. Если двигать в Update / LateUpdate — мы будем двигать чаще чем физика будет симулироваться и это приведет к дрожанию и протыканию колайдеров со всякими неопределенными поведениями.


    1. Keeree
      14.09.2016 11:57

      Я извинюсь, а где они должны двигаться? В FixedUpdate? И что значит умирают безконтрольно? Как это лучше сделать?
      Напишите если не сложно, хотелось бы узнать


      1. Leopotam
        14.09.2016 12:07

        Бесконтрольно — значит инстанцируются и уничтожаются без дополнительной обработки. По сути — это самые тяжелые операции (инстанцирование само по себе, уничтожение — в результате копится мусор, который однажды будет собран GarbageCollector-ом с фризом основного потока). Чтобы такого не было, применяют разные техники, например, пулинг: когда требуется много одинаковых объектов создавать / уничтожать, мы просто не уничтожаем их, а прячем и кладем в очередь на будущее использование; когда в следующий раз потребуется инстанцировать такой объект — сначала проверяем эту очередь и берем инстанс оттуда (настраиваем позицию, свойства и тп — все как для нового инстанса). В результате память через какое-то время перестает выделяться и течь в GC — инстансы будут переиспользоваться по кругу.
        Пример реализации: PoolContainer
        Пример использования (тут лучше смотреть в тестовую сцену — на префабе висит RecycleAfterTime): PoolingTest


  1. DyadichenkoGA
    14.09.2016 12:01

    А для чего на Spawner вообще висит компонента бокс коллайдер? Решение так себе, согласен с Tutanhomon И зачем вейпоинты искать файндом? Вообще юнитёвые файнды лучше юзать в крайних случаях, когда по-другому не обойтись, а тут и без него обойтись довольно просто.


    1. Tutanhomon
      14.09.2016 14:13

      Просто потому что он там висит по умолчанию, непонятно почему его отключают а не удаляют.