Реализовать конечный автомат (FSM) для ИИ врага
Условие
Реализуйте систему конечного автомата (Finite State Machine) для управления поведением ИИ врага в игре.
Требования
- Создайте базовый класс State с методами:
- OnEnter() - вызывается при входе в состояние
- OnUpdate() - вызывается каждый кадр
- OnExit() - вызывается при выходе из состояния
- Создайте класс StateMachine для управления переходами
- Реализуйте состояния для врага:
- IdleState - враг стоит на месте
- PatrolState - враг патрулирует по точкам
- ChaseState - враг преследует игрока
- AttackState - враг атакует игрока
- Переходы между состояниями должны зависеть от расстояния до игрока
Пример структуры
public abstract class State
{
protected StateMachine stateMachine;
public virtual void OnEnter() { }
public virtual void OnUpdate() { }
public virtual void OnExit() { }
}
public class StateMachine : MonoBehaviour
{
private State currentState;
public void ChangeState(State newState) { /* ... */ }
}
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Что такое FSM (Finite State Machine)?
Конечный автомат — фундаментальный паттерн для управления поведением ИИ в играх. FSM позволяет разбить сложное поведение на дискретные состояния и чётко определённые переходы между ними. Это делает код предсказуемым, легко тестируемым и расширяемым. Враг может быть в одном состоянии одновременно (Idle, Patrol, Chase или Attack), и логика переходов определяется условиями окружения.
Базовая реализация FSM
using UnityEngine;
using System;
/// <summary>Базовый класс для состояния в FSM</summary>
public abstract class State
{
protected StateMachine stateMachine;
protected GameObject gameObject;
protected Transform transform;
public State(StateMachine machine)
{
stateMachine = machine;
gameObject = machine.gameObject;
transform = machine.transform;
}
/// <summary>Вызывается при входе в состояние</summary>
public virtual void OnEnter()
{
}
/// <summary>Вызывается каждый кадр</summary>
public virtual void OnUpdate()
{
}
/// <summary>Вызывается в FixedUpdate для физики</summary>
public virtual void OnFixedUpdate()
{
}
/// <summary>Вызывается при выходе из состояния</summary>
public virtual void OnExit()
{
}
}
/// <summary>Управляет переходами между состояниями</summary>
public class StateMachine : MonoBehaviour
{
private State currentState;
private State previousState;
protected virtual void Update()
{
if (currentState != null)
{
currentState.OnUpdate();
}
}
protected virtual void FixedUpdate()
{
if (currentState != null)
{
currentState.OnFixedUpdate();
}
}
/// <summary>Переключить состояние</summary>
public void ChangeState(State newState)
{
if (newState == currentState)
return; // Не переключаемся на то же состояние
// Вызываем OnExit для текущего состояния
currentState?.OnExit();
// Сохраняем предыдущее состояние
previousState = currentState;
currentState = newState;
// Вызываем OnEnter для нового состояния
currentState?.OnEnter();
Debug.Log($"FSM: Переход {previousState?.GetType().Name} -> {currentState.GetType().Name}");
}
/// <summary>Получить текущее состояние</summary>
public State GetCurrentState() => currentState;
/// <summary>Проверить является ли текущее состояние типом T</summary>
public bool IsInState<T>() where T : State => currentState is T;
}
Реализация ИИ врага
public class Enemy : StateMachine
{
[Header("Movement")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float patrolDistance = 10f;
[Header("Detection")]
[SerializeField] private Transform player;
[SerializeField] private float detectionRange = 15f;
[SerializeField] private float attackRange = 2f;
[Header("Combat")]
[SerializeField] private float attackCooldown = 1.5f;
[SerializeField] private int attackDamage = 10;
private float lastAttackTime = -Mathf.Infinity;
private Rigidbody rb;
private Animator animator;
// Состояния
private IdleState idleState;
private PatrolState patrolState;
private ChaseState chaseState;
private AttackState attackState;
void Start()
{
rb = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
// Инициализируем состояния
idleState = new IdleState(this, 3f);
patrolState = new PatrolState(this, transform.position, patrolDistance);
chaseState = new ChaseState(this, player, moveSpeed);
attackState = new AttackState(this, player, attackDamage, attackCooldown);
// Запускаем с состояния Idle
ChangeState(idleState);
}
void Update()
{
base.Update();
UpdateStateTransitions();
}
void FixedUpdate()
{
base.FixedUpdate();
}
/// <summary>Логика переходов между состояниями</summary>
private void UpdateStateTransitions()
{
if (player == null)
return;
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// Враг может атаковать
if (distanceToPlayer <= attackRange)
{
if (!IsInState<AttackState>())
{
ChangeState(attackState);
}
}
// Враг видит игрока и преследует его
else if (distanceToPlayer <= detectionRange)
{
if (!IsInState<ChaseState>())
{
ChangeState(chaseState);
}
}
// Игрок вне видимости - патруль
else if (!IsInState<PatrolState>())
{
ChangeState(patrolState);
}
}
public void TakeDamage(int damage)
{
Debug.Log($"Enemy took {damage} damage");
// Реализуй логику получения урона
}
public float GetMoveSpeed() => moveSpeed;
public float GetDetectionRange() => detectionRange;
public float GetAttackRange() => attackRange;
}
/// <summary>Состояние: враг стоит на месте</summary>
public class IdleState : State
{
private float idleDuration;
private float idleTimer;
public IdleState(StateMachine machine, float duration) : base(machine)
{
idleDuration = duration;
}
public override void OnEnter()
{
idleTimer = 0f;
if (gameObject.TryGetComponent<Animator>(out var animator))
{
animator.SetBool("IsMoving", false);
}
}
public override void OnUpdate()
{
idleTimer += Time.deltaTime;
// После истечения времени переходим в патруль
if (idleTimer >= idleDuration)
{
// Переход будет обработан в UpdateStateTransitions
}
}
}
/// <summary>Состояние: враг патрулирует</summary>
public class PatrolState : State
{
private Vector3 patrolCenter;
private float patrolRadius;
private Vector3 targetPatrolPoint;
private float moveSpeed;
public PatrolState(StateMachine machine, Vector3 center, float radius) : base(machine)
{
patrolCenter = center;
patrolRadius = radius;
GenerateNewPatrolPoint();
}
public override void OnEnter()
{
if (gameObject.TryGetComponent<Animator>(out var animator))
{
animator.SetBool("IsMoving", true);
}
moveSpeed = (stateMachine as Enemy).GetMoveSpeed();
GenerateNewPatrolPoint();
}
public override void OnUpdate()
{
// Движение к целевой точке патруля
Vector3 direction = (targetPatrolPoint - transform.position).normalized;
// Вычисляем расстояние
float distanceToTarget = Vector3.Distance(transform.position, targetPatrolPoint);
if (distanceToTarget < 1f)
{
GenerateNewPatrolPoint();
}
else
{
// Простое движение
transform.position += direction * moveSpeed * Time.deltaTime;
}
}
private void GenerateNewPatrolPoint()
{
// Генерируем случайную точку в пределах патрульной зоны
Vector2 randomPoint = Random.insideUnitCircle * patrolRadius;
targetPatrolPoint = patrolCenter + new Vector3(randomPoint.x, 0, randomPoint.y);
}
}
/// <summary>Состояние: враг преследует игрока</summary>
public class ChaseState : State
{
private Transform player;
private float moveSpeed;
public ChaseState(StateMachine machine, Transform playerTransform, float speed) : base(machine)
{
player = playerTransform;
moveSpeed = speed;
}
public override void OnEnter()
{
if (gameObject.TryGetComponent<Animator>(out var animator))
{
animator.SetBool("IsMoving", true);
animator.SetBool("IsChasing", true);
}
}
public override void OnUpdate()
{
if (player == null)
return;
// Направление к игроку
Vector3 directionToPlayer = (player.position - transform.position).normalized;
// Движение к игроку
transform.position += directionToPlayer * moveSpeed * Time.deltaTime;
// Поворот в сторону игрока
if (directionToPlayer != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(directionToPlayer);
}
}
public override void OnExit()
{
if (gameObject.TryGetComponent<Animator>(out var animator))
{
animator.SetBool("IsChasing", false);
}
}
}
/// <summary>Состояние: враг атакует игрока</summary>
public class AttackState : State
{
private Transform player;
private int attackDamage;
private float attackCooldown;
private float lastAttackTime;
public AttackState(StateMachine machine, Transform playerTransform, int damage, float cooldown) : base(machine)
{
player = playerTransform;
attackDamage = damage;
attackCooldown = cooldown;
lastAttackTime = -Mathf.Infinity;
}
public override void OnEnter()
{
if (gameObject.TryGetComponent<Animator>(out var animator))
{
animator.SetBool("IsMoving", false);
animator.SetTrigger("Attack");
}
}
public override void OnUpdate()
{
if (player == null)
return;
// Поворот в сторону игрока
Vector3 directionToPlayer = (player.position - transform.position).normalized;
transform.rotation = Quaternion.LookRotation(directionToPlayer);
// Атака с промежутком времени
if (Time.time - lastAttackTime >= attackCooldown)
{
DealDamage();
lastAttackTime = Time.time;
}
}
private void DealDamage()
{
// Проверяем есть ли у игрока компонент для получения урона
if (player.TryGetComponent<IHealth>(out var health))
{
health.TakeDamage(attackDamage);
Debug.Log($"Enemy dealt {attackDamage} damage to player");
}
}
}
/// <summary>Интерфейс для объектов которые могут получать урон</summary>
public interface IHealth
{
void TakeDamage(int damage);
}
Ключевые особенности
1. Иерархия состояний — все состояния наследуют от базового класса State, что обеспечивает единообразный интерфейс.
2. Чёткие переходы — логика переходов централизована в UpdateStateTransitions(), что упрощает отладку.
3. Разделение ответственности — каждое состояние отвечает только за свою логику (Idle, Patrol, Chase, Attack).
4. Гибкость — легко добавить новые состояния, просто создав новый класс, наследующий State.
5. Отладка — логирование переходов помогает отследить поведение ИИ.
Расширение системы
Можешь добавить дополнительные состояния:
- DamagedState — враг отступает после получения урона
- DeadState — враг повалился
- FleeState — враг убегает при низком здоровье
Эта реализация обеспечивает гибкость, расширяемость и лёгкую отладку ИИ поведения.