Любой искусственный интеллект, возможности которого выходят за рамки "принеси подай иди подальше не мешай", проще будет сделать через 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.
Теперь нужна какая-то цель, чтобы бот за ней шел. Интерфейс:
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.
Теперь в 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, потому что места все равно не хватило, продолжение в комментах.