Отступление врага по достижении героя Unity2D

Ребят, привет. Не могу никак понять, что в коде прописать, чтобы враг по достижению героя, отступал на сколько то юнитов назад, а потом снова атаковал. Код писал по уроку с ютуба, потом уже изменения вносил. Пока учусь писать AI для врагов. Скрипт AI врагов ниже приложил. Спасибо.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
 
public class AIEnemy : MonoBehaviour
{
    public float speed;
    public float chaseDistance;
    public float stopDistance;
    public GameObject target;
    public PlayableDirector director;
 
 
    private float targetDistance;
    Animator animator;
 
    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>();
    }
 
    // Update is called once per frame
    void Update()
    {
        targetDistance = Vector2.Distance(transform.position, target.transform.position);
        if (targetDistance < chaseDistance && targetDistance > stopDistance && director.state != PlayState.Playing)
            ChasePlayer();
        else
            StopChasePlayer();
    }
 
    private void StopChasePlayer()
    {
        animator.SetBool("attack", false);
    }
 
    private void ChasePlayer()
    {
        if (transform.position.x < target.transform.position.x)
            GetComponent<SpriteRenderer>().flipX = true;
        else
            GetComponent<SpriteRenderer>().flipX = false;
 
        if (this.gameObject.CompareTag("Enemy"))
        {
            transform.position = Vector2.MoveTowards(transform.position, target.transform.position, speed * Time.deltaTime);
            animator.SetBool("attack", true);
        }
        else if (this.gameObject.CompareTag("Slime"))
        {
            animator.SetBool("attack", true);
        }
    }
}

Ответы (1 шт):

Автор решения: KOTlK

Плохое видео ты посмотрел. Если вот это там называют AI, то, по всей видимости, автор видео и не знает что такое аи. Для нормального аи применяется Behaviour Tree, либо вариант для бедных - машина состояний. Опишу вариант для бедных. Самая простая более-менее нормальная стейт машина - корутина, которая будет крутить в цикле итератор (стейт(состояние)). Далее огромный пласт кода, где я настолько все "упростил", что впхал все в один файл, так делать не надо.

using System;
using System.Collections;
using UnityEngine;
using Random = UnityEngine.Random;

namespace PoluRakPoluAi
{
    [RequireComponent(typeof(Collider2D))]
    public class Enemy : MonoBehaviour
    {
        [SerializeField] private float _followRange = 30f;
        [SerializeField] private float _attackRange = 1f;
        [SerializeField] private float _speed = 3f;
        [SerializeField] private float _damage = 20f;
        [SerializeField] private float _attackDelay = 2f;
        [SerializeField] private float _patrolRadius = 10f;
        
        private IEnumerable _currentState;
        
        private Player _target;

        private void Awake()
        {
            _currentState = Patrolling();
            
            StartCoroutine(StateMachine());
        }

        private void OnTriggerEnter2D(Collider2D other)
        {
            if (other.TryGetComponent(out Player player))
            {
                _target = player;
            }
        }

        private void OnTriggerExit2D(Collider2D other)
        {
            if (other.TryGetComponent(out Player player))
            {
                _target = null;
            }
        }

        private IEnumerator StateMachine()
        {
            while (_currentState != null)
            {
                foreach (var current in _currentState)
                {
                    yield return current;
                }
            }
            
        }

        private IEnumerable Following(Player target)
        {
            _target = target;
            var direction = _target.transform.position - transform.position;

            while (_target != null)
            {
                if (direction.magnitude < _attackRange)
                {
                    _currentState = Attacking();
                    yield break;
                }
                
                if (direction.magnitude > _followRange)
                {
                    break;
                }

                Move(direction.normalized);
                
                direction = _target.transform.position - transform.position;
                
                yield return new WaitForFixedUpdate();
            }

            _target = null;
            _currentState = Patrolling();
        }

        private IEnumerable Attacking()
        {
            var direction = _target.transform.position - transform.position;
            
            while (_target != null)
            {
                if (direction.magnitude > _attackRange)
                {
                    _currentState = Following(_target);
                    yield break;
                }

                _target.ApplyDamage(_damage);

                foreach (var current in WaitForSeconds(_attackDelay, () => direction.magnitude > _attackRange))
                {
                    direction = _target.transform.position - transform.position;
                    yield return current;
                }
            }

            _currentState = Patrolling();
        }

        private IEnumerable Patrolling()
        {
            var randomPoint = RandomPointAroundPlayer(_patrolRadius);
            
            while (_target == null)
            {
                foreach (var cur in MoveTo(randomPoint))
                {
                    yield return cur;
                }

                randomPoint = RandomPointAroundPlayer(_patrolRadius);
                yield return null;
            }

            _currentState = Following(_target);
        }

        private IEnumerable WaitForSeconds(float time, Func<bool> cancelCondition)
        {
            var estimatedTime = 0f;

            while (estimatedTime < time)
            {
                if (cancelCondition() == true) yield break;
                
                estimatedTime += Time.deltaTime;
                yield return null;
            }
        }

        private IEnumerable MoveTo(Vector3 position)
        {
            var direction = position - transform.position;
            
            while (direction.sqrMagnitude > 1)
            {
                if (_target != null) yield break;

                Move(direction.normalized);
                yield return new WaitForFixedUpdate();
                direction = position - transform.position;
            }
        }

        private void Move(Vector3 normalizedDirection)
        {
            transform.position += _speed * normalizedDirection * Time.fixedDeltaTime;
        }
        
        private Vector2 RandomPointAroundPlayer(float radius)
        {
            return (Vector2)transform.position + Random.insideUnitCircle * radius;
        }
    }
}

Кажется сложным, но нет, все очень просто. Начну сверху.

Метод Awake:

  1. Устанавливаю изначальное состояние, в котором будет находиться враг.
  2. Стартую машину состояний, которая в данном случае является корутиной.

Методы OnTriggerEnter и OnTriggerExit:

На геймобжекте, куда вешаешь компоент Enemy должен быть компонент коллайдер2д с включенным полем isTrigger этот триггер олицетворяет дистанцию, при подходе к которой игрок становится целью и враг начинает его преследовать. При выходе из этого триггера, цель обнуляется.

Далее сама машина состояний (StateMachine):

Все просто. Дословно: пока есть какой-то стейт, возвращать то, что он возвращает.

Далее сами состояния:

Начну с того, в котором он изначально - Patrolling(патрулирование). (Придется код прокрутить пониже, потому что мне было лень перемещать его вверх.):

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

Разберу подробнее. Как точка рандомная берется, надеюсь объяснять не надо. Движение к точке: точно так же, как и стейт машина, просто крутим итератор MoveTo, который двигает врага, пока дистанция до точки больше 1. Далее назначаем новую точку и все по новой. Если же появился таргет, то цикл завершается и состояние меняется на Following.

Following:

  • Рассчитываем направление на таргет
  • Если подошли на дистанцию атаки, то меняем стейт на атаку. Если дистанция до цели больше, чем максимальная дистанция следования, выходим из цикла. Если ничто из вышеперечисленного не истинно, то двигаемся, обновляем направление ждем, повторяем все заново.
  • Если же вдруг цикл завершился, то тагрет обнуляем, и меняем стейт на патрулирование.

Атака(Attacking):

  • Снова определяем направление к таргету.
  • Меняем стейт на следование, если дистанция до цели меньше, чем дистанция, на которой можно атаковать. Наносим дамаг цели. Ожидаем при помощи таймера (о нем ниже). Повторяем.
  • Если цикл прерван, меняем состояние на патрулирование.

Таймер - то же самое, что и обычный WaitForSeconds, только этот можно прервать при выполнении какого-либо условия, в моем случае он прерывается, если дистанция до цели будет больше дистанции атаки. Он прерывается -> состояние меняется на Following.

Как видно из количества подпунктов в описании состояний, они делятся на 3 стадии Вход, Цикл/Действие, Выход. И т.к. все происходит в корутине, эти стадии можно выполнять за несколько кадров. Но нужно следить за мусором, который генерируешь, потому что, пока итератор не завершится, сборщик мусора ничего собирать не будет. Т.е., пока будет работать этот итератор:

private IEnumerable WaitHour()
{
    var superBigDataObject = new SuperBigDataObject();

    yield return new WaitForSeconds(3600);
}

superBigDataObject будет занимать супер много места в памяти.

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

Чтобы делать все по-человечески, нужно разделить все на классы. Стейты будут реализовывать интерфейс IEnumerable. На примере WaitForSeconds:

namespace PoluRakPoluAi
{
    public class WaitForSeconds : IEnumerable
    {
        private readonly Func<bool> _cancelCondition;
        private readonly float _time;

        public WaitForSeconds(float time, Func<bool> cancelCondition)
        {
            _cancelCondition = cancelCondition;
            _time = time;
        }

        public IEnumerator GetEnumerator()
        {
            var estimatedTime = 0f;

            while (estimatedTime < _time)
            {
                if (_cancelCondition() == true) yield break;
                
                estimatedTime += Time.deltaTime;
                yield return null;
            }
        }
    }
}
→ Ссылка