← Назад к вопросам

Реализовать систему патрулирования для врага

1.6 Junior🔥 211 комментариев
#C# и ООП#Unity Core#Анимация#Физика и математика

Условие

Реализуйте компонент патрулирования для ИИ врага.

Требования

  1. Враг перемещается между заданными точками патрулирования
  2. В каждой точке ненадолго останавливается
  3. Может двигаться циклично или возвращаться по тому же пути
  4. Плавный поворот в направлении движения
  5. Анимация ходьбы
  6. Визуализация пути в редакторе через Gizmos

Бонус

  • Случайный выбор следующей точки
  • Разная скорость на разных участках

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Система патрулирования врага AI

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

Архитектура системы

Основные компоненты:

  1. PatrolPath — хранит точки патрулирования
  2. PatrolController — управляет движением между точками
  3. AnimationController — синхронизирует анимации
  4. PathVisualizer — отображение пути в редакторе

PatrolPath - Определение маршрута

[System.Serializable]
public class PatrolWaypoint
{
    public Vector3 Position { get; set; }
    public float StopDuration { get; set; } = 1f;
    public float Speed { get; set; } = 5f;
    public float WaitRadius { get; set; } = 0.5f;
}

public class PatrolPath : MonoBehaviour
{
    [SerializeField] private List<PatrolWaypoint> waypoints = new List<PatrolWaypoint>();
    [SerializeField] private bool closeLoop = true;
    [SerializeField] private bool randomOrder = false;
    [SerializeField] private Color pathColor = Color.green;
    [SerializeField] private float waypointSize = 0.3f;
    
    public List<PatrolWaypoint> Waypoints => waypoints;
    public bool CloseLoop => closeLoop;
    public bool RandomOrder => randomOrder;
    
    public void AddWaypoint(Vector3 position)
    {
        waypoints.Add(new PatrolWaypoint { Position = position });
    }
    
    public void RemoveWaypoint(int index)
    {
        if (index >= 0 && index < waypoints.Count)
        {
            waypoints.RemoveAt(index);
        }
    }
    
    public PatrolWaypoint GetWaypoint(int index)
    {
        if (waypoints.Count == 0)
            return null;
        
        return waypoints[Mathf.Clamp(index, 0, waypoints.Count - 1)];
    }
    
    public int GetWaypointCount() => waypoints.Count;
    
    // Визуализация в редакторе
    private void OnDrawGizmos()
    {
        if (waypoints.Count == 0)
            return;
        
        Gizmos.color = pathColor;
        
        // Рисуем линии между точками
        for (int i = 0; i < waypoints.Count - 1; i++)
        {
            Gizmos.DrawLine(waypoints[i].Position, waypoints[i + 1].Position);
        }
        
        // Если замкнутый контур
        if (closeLoop && waypoints.Count > 1)
        {
            Gizmos.DrawLine(waypoints[waypoints.Count - 1].Position, waypoints[0].Position);
        }
        
        // Рисуем сферы в точках
        foreach (var waypoint in waypoints)
        {
            Gizmos.DrawSphere(waypoint.Position, waypointSize);
        }
    }
    
    private void OnDrawGizmosSelected()
    {
        if (waypoints.Count == 0)
            return;
        
        Gizmos.color = Color.yellow;
        
        foreach (var waypoint in waypoints)
        {
            Gizmos.DrawWireSphere(waypoint.Position, waypointSize * 1.5f);
        }
    }
}

PatrolController - Основная логика

public enum PatrolBehavior
{
    Loop,           // Циклический обход
    PingPong,       // Туда-сюда
    Random,         // Случайный выбор
    Sequential      // Последовательный
}

public class PatrolController : MonoBehaviour
{
    [SerializeField] private PatrolPath patrolPath;
    [SerializeField] private PatrolBehavior behavior = PatrolBehavior.Loop;
    [SerializeField] private float rotationSpeed = 5f;
    [SerializeField] private float stoppingDistance = 0.1f;
    
    private Animator animator;
    private CharacterController characterController;
    private int currentWaypointIndex = 0;
    private bool movingForward = true;
    private float stopTimer = 0f;
    private bool isStopped = false;
    private Vector3 currentDirection = Vector3.zero;
    private HashSet<int> visitedWaypoints = new HashSet<int>();
    
    private void Start()
    {
        animator = GetComponent<Animator>();
        characterController = GetComponent<CharacterController>();
        
        if (patrolPath == null)
        {
            Debug.LogError("PatrolPath not assigned!");
            enabled = false;
            return;
        }
        
        if (patrolPath.GetWaypointCount() == 0)
        {
            Debug.LogError("PatrolPath has no waypoints!");
            enabled = false;
            return;
        }
    }
    
    private void Update()
    {
        if (patrolPath == null || patrolPath.GetWaypointCount() == 0)
            return;
        
        PatrolWaypoint currentWaypoint = patrolPath.GetWaypoint(currentWaypointIndex);
        Vector3 waypointPosition = currentWaypoint.Position;
        float distanceToWaypoint = Vector3.Distance(transform.position, waypointPosition);
        
        // Если достигли точку
        if (distanceToWaypoint <= currentWaypoint.WaitRadius && !isStopped)
        {
            StartStop(currentWaypoint.StopDuration);
        }
        
        // Если ждём
        if (isStopped)
        {
            stopTimer -= Time.deltaTime;
            if (stopTimer <= 0)
            {
                isStopped = false;
                MoveToNextWaypoint();
            }
            animator.SetFloat("Speed", 0);
            return;
        }
        
        // Движение к точке
        MoveTowards(waypointPosition, currentWaypoint.Speed);
    }
    
    private void MoveTowards(Vector3 targetPosition, float speed)
    {
        Vector3 direction = (targetPosition - transform.position).normalized;
        currentDirection = direction;
        
        // Плавный поворот
        if (direction != Vector3.zero)
        {
            Quaternion targetRotation = Quaternion.LookRotation(direction);
            transform.rotation = Quaternion.Slerp(
                transform.rotation,
                targetRotation,
                rotationSpeed * Time.deltaTime
            );
        }
        
        // Движение
        Vector3 movement = direction * speed * Time.deltaTime;
        characterController.Move(movement);
        
        // Анимация
        animator.SetFloat("Speed", speed);
    }
    
    private void StartStop(float duration)
    {
        isStopped = true;
        stopTimer = duration;
        animator.SetFloat("Speed", 0);
    }
    
    private void MoveToNextWaypoint()
    {
        switch (behavior)
        {
            case PatrolBehavior.Loop:
                currentWaypointIndex = (currentWaypointIndex + 1) % patrolPath.GetWaypointCount();
                break;
            
            case PatrolBehavior.PingPong:
                if (movingForward)
                {
                    currentWaypointIndex++;
                    if (currentWaypointIndex >= patrolPath.GetWaypointCount() - 1)
                    {
                        movingForward = false;
                        currentWaypointIndex = patrolPath.GetWaypointCount() - 1;
                    }
                }
                else
                {
                    currentWaypointIndex--;
                    if (currentWaypointIndex <= 0)
                    {
                        movingForward = true;
                        currentWaypointIndex = 0;
                    }
                }
                break;
            
            case PatrolBehavior.Random:
                int randomIndex = Random.Range(0, patrolPath.GetWaypointCount());
                currentWaypointIndex = randomIndex;
                break;
            
            case PatrolBehavior.Sequential:
                currentWaypointIndex++;
                if (currentWaypointIndex >= patrolPath.GetWaypointCount())
                {
                    currentWaypointIndex = 0; // Начинаем сначала
                }
                break;
        }
    }
    
    public void SetPatrolPath(PatrolPath newPath)
    {
        patrolPath = newPath;
        currentWaypointIndex = 0;
        isStopped = false;
        visitedWaypoints.Clear();
    }
    
    public Vector3 GetCurrentWaypointPosition()
    {
        return patrolPath.GetWaypoint(currentWaypointIndex).Position;
    }
    
    public int GetCurrentWaypointIndex() => currentWaypointIndex;
}

AnimationController - Синхронизация анимаций

public class EnemyAnimationController : MonoBehaviour
{
    private Animator animator;
    private PatrolController patrolController;
    
    private int speedHash = Animator.StringToHash("Speed");
    private int directionHash = Animator.StringToHash("Direction");
    private int stopHash = Animator.StringToHash("Stop");
    
    private void Start()
    {
        animator = GetComponent<Animator>();
        patrolController = GetComponent<PatrolController>();
    }
    
    public void SetMovementSpeed(float speed)
    {
        animator.SetFloat(speedHash, speed);
    }
    
    public void SetDirection(Vector3 direction)
    {
        if (direction != Vector3.zero)
        {
            animator.SetFloat(directionHash, direction.x);
        }
    }
    
    public void PlayStopAnimation()
    {
        animator.SetTrigger(stopHash);
    }
}

Использование в сцене

public class EnemySetup : MonoBehaviour
{
    private void Start()
    {
        // Создаём маршрут
        PatrolPath path = gameObject.AddComponent<PatrolPath>();
        path.AddWaypoint(new Vector3(0, 0, 0));
        path.AddWaypoint(new Vector3(5, 0, 0));
        path.AddWaypoint(new Vector3(5, 0, 5));
        path.AddWaypoint(new Vector3(0, 0, 5));
        
        // Добавляем патруль контроллер
        PatrolController patrol = gameObject.AddComponent<PatrolController>();
        patrol.SetPatrolPath(path);
    }
}

Ключевые особенности

1. Гибкие режимы движения — Loop, PingPong, Random, Sequential

2. Плавный поворот — использование Quaternion.Slerp для реалистичного вращения

3. Гибкие остановки — каждая точка имеет свою длительность паузы

4. Переменная скорость — разная скорость на разных участках маршрута

5. Визуализация пути — линии и сферы в редакторе через Gizmos

6. Синхронизация анимаций — параметры Speed и Direction для Animator

7. Масштабируемость — легко добавлять новые режимы поведения

Расширение возможностей

  • Обнаружение игрока — переключение на режим преследования
  • Динамическое изменение маршрута — добавление точек во время игры
  • Звуковые эффекты — шаги, дыхание при остановке
  • Состояния врага — усталость, ранения влияют на скорость
  • Слежение за игроком — если врагом обнаружен, переход в режим преследования

Эта система обеспечивает реалистичное движение врагов с минимальными затратами на производительность.

Реализовать систему патрулирования для врага | PrepBro