Страницы

среда, 4 декабря 2019 г.

Работа с корутинами в Unity. Часть 3. Примеры

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

  • движение к цели;
  • простой вариант искусственного интеллекта;
  • инициализация процедуры;
  • реализация игрового цикла.

Пример 1. Движение к цели

Мы хотим, чтобы что-то начинало двигаться к цели .как только объект (скажем, игрок в нашем случае) входит в триггерную область. Эта триггерная зона может быть переключателем на полу и чем-нибудь подобным.
public class MoveToTarget : MonoBehaviour
{
   [SerializeField] private Transform _target;
   [SerializeField] private Transform _mover;
   [SerilaizeField] private float _speed = 2f;
   private bool _moved;
   private void OnTriggerEnter(Collider other)
   {
      If(other.CompareTag(“Player) && !_moved)
      StartCoroutine(Move());
   }
   private IEnumerator Move()
   {
      _moved = true;
      while(Vector3.Distance(_target.position, _mover.posiiton) > float.Epsilon)
     {
        _mover.position = Vector3.MoveTowards(_mover.position,      _target.position, _speed * Time.deltaTime);
        yield return null;
     }
   }
}
Нужно пояснить тут несколько вещей.

  1. мы используем логическую переменную _moved. Когда запускается корутина, мы устанавливаем ей значение true. Это значение каждый раз проверяется при срабатывании триггера. В противном случае, корутина будет запускаться при каждом входе персонажа в триггерную зону.
  2. мы не использовали WaitForSeconds в этом примере, потому что наш объект  _mover должен перемещаться без каких-либо пауз, пока не достигнет своей цели. Вместо этого мы используем yield return null. Если в вашей корутине есть раздел, который вам не нужен, применяйте yield return null.
  3. мы используем цикл while с условием. Условие срабатывает, пока _mover находится достаточно далеко от цели (_target), чтобы продолжать двигаться. Как только _mover окажется близко, сопрограмма завершится. Мы реализуем перемещение объекта с помощью метода Vector3.MoveTowards.

Пример 2. Простой вариант ИИ

В этом примере создадим очень простой искусственный интеллект. Заставим его выбрать случайные значения из семи путевых точек, переместиться в нужное место, подождать случайный интервал времени, а затем повторять это всегда.
public class SimpleAI : MonoBehaviour
{
 [SerializeField] private Transform[] _waypoints;
 [SerializeField] private float _minWaitTime = 1f;
 [SerializeField] private float _maxWaitTime = 5f;
 [SerializeField] private float _speed = 2f;
 private IEnumerator Start()
 {
  while(true)
  {
    yield return StartCoroutine(Move());
    yield return StartCoroutine(Wait());
  }
 }
 private IEnumerator Move()
 {
  Vector3 destination = _waypoints[Random.Range(0,      _waypoints.Length)].position;
  while(Vector3.Distance(transform.position, destination) > float.Epsilon)
  {
  Transform.postion = Vector3.MoveTowards(transform.position, destination, _speed * Time.deltaTime);
  yield return null;
  }
 }
 private IEnumerator Wait()
 {
  yield return new WaitForSeconds(Random.Range(_minWaitTime, _maxWaitTime);
 }
}
Это почти то же самое, что и в первом примере. Разница здесь в том, что у нас есть сеть путевых точек, чтобы следовать, и движение не основано на игроке, входящем в триггер.

В методе start() мы реализуем бесконечный цикл. Также обратите внимание, что в данном случае мы ждем завершения сопрограмм. Я не буду обсуждать, почему бесконечный цикл работает с IEnumerators, но, возможно, стоит подумать о методе update. Он тоже будет постоянно повторять то, что прописано в его коде.

Теперь по пунктам объясню, что здесь происходит.

  • Unity запускает сопрограмму со стартом сцены.
  • создается бесконечный цикл while(true). Это позволяет повторяться нашим сопрограммам вечно.
  • вызывается функция yield return StartCoroutine (Move ()). Сопрограмма будет выполнять этот метод до тех пор, пока объект не прибудет в путевую точку, случайно выбранную из массива _waypoints.
  • yield return StartCoroutine (Wait ()) вызывается сразу после завершения перемещения. ИИ будет просто оставаться в точке маршрута в течение случайного количества времени.
  • после того, как ожидание закончилось, мы повторяем нашу сопрограмму, начиная с Move снова. Это будет продолжаться вечно. Еще раз, причина этого повторения цикл while(true).

Вот видите, насколько проще использовать сопрограммы для создания поведения, основанного на условиях. Если вы знакомы с navmesh Unity, вы можете, с несколькими незначительными изменениями, использовать navmeshagent, чтобы ваш ИИ избегал столкновения со стенами. Можно даже расширить этот пример с проверкой линии визирования для игрока, а затем заставить ИИ преследовать его или добавить что-то еще.

Пример 3. Инициализация процедуры

Одна вещь, которая хороша в циклических сопрограммах, заключается в том, что вы можете инициализировать их перед входом в цикл. Это здорово, потому что вы можете сохранить весь код, содержащийся внутри одного IEnumerator и легко работать с ним.
Давайте возьмем наш предыдущий пример т будем воспроизводить разную анимацию в зависимости от того, что делает ИИ.
public class SimpleAI : MonoBehaviour
{
 [SerializeField] private Transform[] _waypoints;
 [SerializeField] private Animator _animator;
 [SerializeField] private float _minWaitTime = 1f;
 [SerializeField] private float _maxWaitTime = 5f;
 [SerializeField] private float _speed = 2f;
 private void Awake()
 {
  _animator = GetComponent<Animator>();
 }

 private IEnumerator Start()
 {
  while(true)
  {
   yield return StartCoroutine(Move());
   yield return StartCoroutine(Wait());
  }
 }
 private IEnumerator Move()
 {
  Vector3 destination = _waypoints[Random.Range(0, _waypoints.Length)].position;
  _animator.SetBool(“Moving”, true);
  while(Vector3.Distance(transform.position, destination) > float.Epsilon)
  {
   Transform.postion = Vector3.MoveTowards(transform.position, destination, _speed * Time.deltaTime);
   yield return null;
  }
 }
 private IEnumerator Wait()
 {
  _animator.SetBool(“Moving”, false);
  yield return new WaitForSeconds(Random.Range(_minWaitTime,  _maxWaitTime);
 }
}
Здесь вы можете видеть, что мы добавили ссылку на аниматор. Когда наш ИИ изменяет состояние, он теперь также меняет анимацию в зависимости от того, движется ли он или ждет.
Обратите внимание, что в методе Move мы меняем анимацию перед циклом while, чтобы она выполнялась только один раз за изменение состояния. Затем мы меняем анимацию на ожидание, бездействие, когда ИИ изменяется на Wait IEnumerator.

Пример 4 Игровой Цикл

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

  • Инициализация – настройка игровой сцены. Это может быть спаунинг  всех объектов в нужных местах и все остальное, что требуется для запуска.
  • Игровой процесс - это будет просто игра, работающая нормально. Игра здесь обычно приостанавливается и проверяет условие на окончание, чтобы очистить сцену.
  • Конец игры – что происходит, когда игра заканчивается. Это может быть отключение управления персонажами или объектами и показ меню или еще что-то.

Это лишь пример, общая идея реализации игрового цикла Вы можете использовать сценарий цикла игры, чтобы контролировать, в какой фазе находится ваша игра.
public class GameLoop : MonoBehaviour
{
 [SerializeField] private Player[] _players;
 [SerializeField] private GameObject _gameOverMenu;
 private IEnumerator Start()
 {
  Initialize();
  yield return StartCoroutine(GameOver());
 }
 private void Initialize()
 {
  Foreach(Player player in _players)
  {
   player spawn = Instantiate(player);
   float x = Random.Range(0, 1000f);
   float y = Random.Range(0, 1000f);
   float z = Random.Range(0, 1000f);
   spawn.transform.position = new Vector3(x, y, z);
  }
 }
 private IEnumerator GameOver()
 {
 while(true)
 {
  Foreach(Player player in _players)
  {
    If(player.IsDead)
    {
     _gameOverMenu.setActive(true);
     yield break;
    }
   } yield return null;
  }
 }
}
В этом примере мы создаем игроков, а затем просто перебираем их всех, пока один из них не умрет. Как только это произойдет, мы покажем меню game over.
Мы выходим из сопрограммы через break, если один из игроков умер. Пока этого не происходит, наша корутина  продолжает цикл.
Этот пример является базовым, но он показывает, что вы можете легко управлять потоком игры с помощью нескольких сопрограмм.

По книге Lucas Faustino “Unity5 Coroutines”.
Другие части статьи
1 часть. Что такое корутины.
2 часть. Coroutine vs Update.
3 часть. Примеры использования сопрограмм.
4 часть. Финал.