@Gera01
Unity, С# и больше ничего.

Почему бот идет к цели не всегда?

Есть код мозгов, зрения и атаки. Проблема в том, что мозги бота не всегда дают ему команду идти к enemyTarget, когда бот не может атаковать цель. Посмотрел. Иногда боты не идут к цели, а с самого начала когда игрок появляется там, где они его уже видят - они к нему не идут.

Мозги:

public class KnightBrain : MonoBehaviour, IBrain, IFindTarget
    {
        private BotSee _botsee_cs;

        public Transform enemyTarget;

        public IHealth _health;

        private KnightMove _knightMove_cs;
        private KnightAttack _knightAttack_cs;
        private KnightAnimation _knightAnimation_cs;

        void Start()
        {
            _botsee_cs = GetComponent<BotSee>();
            _health = GetComponent<IHealth>();

            _knightMove_cs = GetComponent<KnightMove>();
            _knightAttack_cs = GetComponent<KnightAttack>();
            _knightAnimation_cs = GetComponent<KnightAnimation>();
        }

        void Update()
        {
            if (enemyTarget != null && _botsee_cs.DoISeeObject(enemyTarget.gameObject) && enemyTarget.TryGetComponent(out IHealth health))
            {
                if (_knightAttack_cs.CanAttack(enemyTarget))
                {
                    _knightMove_cs.StopMove();
                    _knightMove_cs.RotateToAttackPosition(enemyTarget.position);

                    _knightAnimation_cs.SetBool("Run", false);
                    _knightAttack_cs.Attack();
                }
                else
                {
                    GoToTarget(enemyTarget.position);
                }

            }
            else
            {
                if (FindTarget() != null)
                {
                    SetTargetToAttack(FindTarget().transform);
                }
                else
                {
                    if (_knightMove_cs.lastPositionTarget != Vector3.zero)
                    {
                        if (transform.position.x == _knightMove_cs.lastPositionTarget.x && transform.position.z == _knightMove_cs.lastPositionTarget.z)
                        {
                            _knightMove_cs.lastPositionTarget = Vector3.zero;
                        }
                        else
                        {
                            GoToTarget(_knightMove_cs.lastPositionTarget);
                        }
                    }
                    else
                    {
                        if (_knightMove_cs.mainTarget != null)
                        {
                            if (_knightMove_cs.mainTarget.position == Vector3.zero)
                            {
                                _knightMove_cs.RandomMove();
                            }
                            else
                            {
                                _knightMove_cs.SetTarget(_knightMove_cs.mainTarget.position);
                            }
                        }
                        else
                        {
                            _knightMove_cs.RandomMove();
                        }
                    }
                }
            }
        }

        public void GoToTarget(Vector3 target)
        {
            _knightMove_cs.SetTarget(target);
        }

        public GameObject FindTarget()
        {
            foreach (GameObject obj in _botsee_cs.GetObjectThatISee())
            {
                if (obj.GetComponent<IHealth>() != null)
                {
                    if (obj != gameObject)
                    {
                        if (obj.TryGetComponent(out WarSideManager warSideManager))
                        {
                            if (obj.GetComponent<WarSideManager>().GetWarSide() != GetComponent<WarSideManager>().GetWarSide())
                            {
                                return obj;
                            }
                        }
                    }
                }
            }

            return null;
        }

        public void SetTargetToAttack(Transform target)
        {
            enemyTarget = target;
            _knightMove_cs.SetTarget(target.position);
        }
    }

Зрение:
public class BotSee : MonoBehaviour
{
    [SerializeField] 
    private float _seeRange;

    public Transform raycastStartPoint;
    
    [SerializeField] 
    private int _deviationRayFromCenter;

    [SerializeField]
    private List<GameObject> _objectsThatISee;

    public List<GameObject> GetObjectThatISee()
    {
        See();
        return _objectsThatISee;
    }

    public bool DoISeeObject(GameObject _objectToCheak)
    {
        See();
        
        foreach (GameObject obj in GetObjectThatISee())
        {
            if (obj == _objectToCheak)
            {
                return true;
            }
        }

        return false;
    }
    
    public bool DistanseToObject(GameObject _objectToCheak, float _distanseToAttack)
    {
        float oldDistanse = _seeRange;
        _seeRange = _distanseToAttack;
        See();
        foreach (GameObject obj in GetObjectThatISee())
        {
            if (obj == _objectToCheak)
            {
                _seeRange = oldDistanse;
                return true;
            }
        }
        _seeRange = oldDistanse;
        return false;
    }
    
    private void See()
    {
        _objectsThatISee = new List<GameObject>();
        
        for (float i = 0f; i < _deviationRayFromCenter;)
        {
            i += 0.1f;
            RayOfVision(Vector3.left, i);
            RayOfVision(Vector3.right, i);
        }
        for (float i = 0f; i < _deviationRayFromCenter / 2;)
        {
            i += 0.02f;
            RayOfVision(Vector3.left + Vector3.down, i);
            RayOfVision(Vector3.left + Vector3.up, i);
            RayOfVision(Vector3.right + Vector3.up, i);
            RayOfVision(Vector3.right + Vector3.down, i);
        }
    }

    private void RayOfVision(Vector3 addVector, float addToVector)
    {
        RaycastHit hit;
        if (Physics.Raycast(raycastStartPoint.position, transform.forward + addVector * addToVector, out hit, _seeRange))
        {
            foreach (GameObject obj in _objectsThatISee)
            {
                if (hit.collider.gameObject == obj)
                {
                    return;
                }
                if (hit.collider.gameObject == gameObject)
                {
                    return;
                }
            }
            _objectsThatISee.Add(hit.collider.gameObject);
        }
    }
}

KnightAttack.CanAttack:
public bool CanAttack(Transform objectToCheak)
    {
        if (GetComponent<BotSee>().DistanseToObject(objectToCheak.gameObject, attackDistanse))
        {
            return true;
        }
        return false;
    }
  • Вопрос задан
  • 192 просмотра
Решения вопроса 1
K0TlK
@K0TlK
Буллю людей.
Любой искусственный интеллект, возможности которого выходят за рамки "принеси подай иди подальше не мешай", проще будет сделать через Behaviour Tree. Без него, чем сложнее у тебя будет бот, тем больше кода придется писать и тем больше будет каких-то непонятных условий, что у тебя, собственно, и проявляется в апдейте, полностью состоящем из if else if else if else. С Behaviour Tree все куда проще, есть много разных классов, отвечающих за что-то одно, все они совмещаются в разные деревья.
Пример я покажу с использованием ассета Behaviour Designer, т.к., если я буду делать свою реализацию BT, то, боюсь, места для текста в ответе не хватит. Стоит Designer 90$, к счастью, пиратить софт очень плохо и так делают только плохие люди.

Для того, чтобы бот ходил, нужно направление движения, соответственно, это направление нужно откуда-то брать:
using UnityEngine;

namespace BehaviourTree
{
    public interface IMovementInput
    {
        Vector3 Direction { get; }
    }
}


Ну и, собственно, сам бот:
using UnityEngine;

namespace BehaviourTree
{
    [RequireComponent(typeof(Rigidbody))]
    public class Bot : MonoBehaviour
    {
        [SerializeField] private MonoBehaviour _botInput = null;
        [SerializeField] private float _speed = 10f;

        private Rigidbody _rb;

        private IMovementInput BotInput => (IMovementInput) _botInput;

        private void OnValidate()
        {
            if (_botInput is IMovementInput) return;

            Debug.LogError($"{nameof(_botInput)} should implement {nameof(IMovementInput)}");
            _botInput = null;
        }

        private void Awake()
        {
            _rb = GetComponent<Rigidbody>();
        }

        private void FixedUpdate()
        {
            var velocity = new Vector3(BotInput.Direction.x, 0, BotInput.Direction.z) * _speed;
            _rb.velocity = velocity;
        }

        public void LookAt(Vector3 direction)
        {
            var rotation = Quaternion.LookRotation(direction);

            transform.rotation = rotation;
        }
    }
}


Инжектим интерфейс ввода через инспектор и в FixedUpdate задаем скорость. Чтобы посмотреть в каком-то направлении, нужно вызвать метод LookAt. Чтобы прокинуть интерфейс через инспектор нужен какой-то монобех, который будет реализовывать интерфейс. Вот он:
using UnityEngine;

namespace BehaviourTree
{
    public class BotInput : MonoBehaviour, IMovementInput
    {
        public Vector3 Direction { get; set; }
    }
}

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

62c017ffaf2a1016283427.png

Теперь нужна какая-то цель, чтобы бот за ней шел. Интерфейс:
using UnityEngine;

namespace BehaviourTree
{
    public interface ITarget
    {
        Vector3 Position { get; }
    }
}

Реализация:
using UnityEngine;

namespace BehaviourTree
{
    public class SomeTarget : MonoBehaviour, ITarget
    {
        public Vector3 Position => transform.position;
    }
}

Все то же самое, что и с ботом. Поместить на какой-нибудь капсюль, отличный от капсюля бота.

Ну а теперь BT. Добавляешь своему боту компонент Behaviour Tree: в инспекторе бота -> Add Component -> Behaviour Designer -> Behaviour Tree.
Далее нужно как-то туда добавить инпут бота, для этого создаем скрипт:
using BehaviorDesigner.Runtime;

namespace BehaviourTree.Brain
{
    public class SharedBotInput : SharedVariable<BotInput>
    {
        public static implicit operator SharedBotInput(BotInput input) => new SharedBotInput {Value = input};
    }
}

Теперь BotInput можно прокидывать в переменные Behaviour Designer. Кстати о них. Открываем Tools -> Behaviour Designer -> Editor, далее тыкаем на бота, к которому прикреплен компонент Behaviour Tree, в дизайнере открываем Variables в поле Name вводим botInput, в поле Type выбираем BotInput, нажимает Add. Далее в инспекторе бота перетаскиваем компонент BotInput в поле botInput компонента Behaviour Tree.

62c018177e9a0543052458.png

Теперь в Behavoiur Designer можно устанавливать значение Direction компонента BotInput.
Сначала я покажу весь код, что я написал для управления ботом, а потом то, как это все выглядит в дизайнере.
Бот будет очень простой - он будет просто ходить в рандомном направлении, а если увидит свой таргет, то будет тупо бежать в него.

Собственно, хождение в рандомном направлении:
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
using UnityEngine;

namespace BehaviourTree.Brain
{
    public class SetRandomDirection : Action
    {
        public SharedVector3 Direction;

        public override TaskStatus OnUpdate()
        {
            Direction.Value = Vector3.Scale(Random.insideUnitSphere.normalized, new Vector3(1, 0, 1));
            return TaskStatus.Success;
        }
    }
}

Наследуясь от Action, SetRandomDirection появляется в нодах(Tasks) дизайнера в выпадающем меню Actions
Теперь нужно как-то установить направление движения бота.
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;

namespace BehaviourTree.Brain
{
    public class SetInput : Action
    {
        public SharedBotInput SharedInput;
        public SharedVector3 Input;

        public override TaskStatus OnUpdate()
        {
            SharedInput.Value.Direction = Input.Value;
            return TaskStatus.Success;
        }
    }
}

Собственно, здесь все понятно. Берем значение из Input и присваиваем его в SharedInput.
Поворот в определенном направлении:
using BehaviorDesigner.Runtime.Tasks;

namespace BehaviourTree.Brain
{
    public class LookAtDirection : Action
    {
        public SharedBotInput Direction;
        public Bot Bot;

        public override TaskStatus OnUpdate()
        {
            Bot.LookAt(Direction.Value.Direction);
            return TaskStatus.Success;
        }
    }
}

Далее идет нахождение цели и следование за ней, соответственно цель эту надо тоже добавить в дизайнер.
using BehaviorDesigner.Runtime;

namespace BehaviourTree.Brain
{
    public class SharedTarget : SharedVariable<SomeTarget>
    {
        public static implicit operator SharedTarget(SomeTarget target) => new SharedTarget {Value = target};
    }
}

Установка направления к цели:
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
using UnityEngine;

namespace BehaviourTree.Brain
{
    public class SetDirectionToTarget : Action
    {
        public SomeTarget Target;
        public Bot Origin;
        public SharedVector3 Direction;

        public override TaskStatus OnUpdate()
        {
            var direction = (Target.Position - Origin.transform.position).normalized;
            Direction.Value = new Vector3(direction.x, 0, direction.z);
            return TaskStatus.Success;
        }
    }
}


Теперь нужно как-то дать возможность боту увидеть цель. Интерфейс:
namespace BehaviourTree.Brain
{
    public interface IEye
    {
        bool InSight(ITarget target);
    }
}

Реализация:
using UnityEngine;

namespace BehaviourTree.Brain
{
    public class Eye : IEye
    {
        private readonly float _fov;
        private readonly Transform _origin;

        public Eye(float fov, Transform origin)
        {
            _fov = fov;
            _origin = origin;
        }

        public bool InSight(ITarget target)
        {
            var direction = target.Position - _origin.position;

            var angle = Mathf.Acos(Vector3.Dot(_origin.forward.normalized, direction.normalized)) * Mathf.Rad2Deg;

            return angle <= _fov;
        }
    }
}

Здесь все просто. Глаз, у которого есть Field of View в методе InSight проверяем, входит ли позиция таргета в этот фов. Проверяется все очень просто. Находим направление от бота к таргету, вычисляем угол между этим вектором направления и направлением взгляда бота, сравниваем. Чтобы вычислить угол нужно знать всего лишь формулу скалярного произведения векторов из нее выводим то, что у меня в коде умножение на Mathf.Rad2Deg потому что Mathf.Acos возвращает угол в радианах, а я его перевожу в градусы.
Ну и последнее - нахождение цели:

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

Войдите, чтобы написать ответ

Похожие вопросы