第11章 敌人AI系统构建:《暗黑王朝》智能战斗的完整实现

在《暗黑王朝》这样的写实射击手游中,敌人AI的品质直接影响游戏的可玩性和挑战性。一个优秀的AI系统,需要让敌人表现出符合其设定的行为——巡逻时警觉、发现玩家后追击、在合适距离发动攻击、血量过低时可能逃跑。本章将从最基础的"逃跑计划"脚本开始,逐步展开敌人状态机设计、玩家检测机制、血量系统绑定、远程与近战敌人的差异化实现等完整技术体系。结合《暗黑王朝》的实际开发经验,为读者呈现一套模块化、可扩展的移动平台AI解决方案。

11.1 基础敌人AI系统的构建

11.1.1 敌人模型导入与动画切分

在开始编写AI逻辑之前,需要将第7章制作的敌人模型导入Unity并进行正确的动画切分。根据第10章的经验,每个敌人模型需要包含以下基础动画状态:

  • Idle:待机状态,角色静止时的微动
  • Walk:普通移动
  • Run:追击时奔跑
  • Attack:攻击动作
  • Hurt:受击反应
  • Death:死亡

动画切分的关键步骤:

  1. 模型导入设置:在Inspector中打开Rig选项卡,Animation Type设置为Humanoid(人形)或Generic(非人型),点击Apply

  2. 动画分割:切换到Animation选项卡,勾选Import Animation。在Clips列表中,根据帧范围分割出上述动画剪辑。每个剪辑需勾选Loop Time(循环动画)确保无缝衔接

  3. Avatar配置:如果骨骼映射不准确,点击Configure Avatar进入配置模式,手动分配骨骼。确保模型处于T形姿势,Pose > Enforce T-Pose可修复姿势问题

完成动画分割后,将模型拖入场景创建预制体,准备编写AI逻辑。

11.1.2 "逃跑计划"脚本的实现

AI行为最核心的设计模式是有限状态机(Finite State Machine,FSM)。根据最新的游戏开发实践,基于FSM的AI架构能够实现动态的状态转换和智能行为。我们先从最简单的"逃跑计划"开始——当敌人发现玩家时,它会根据自身血量决定是追击还是逃跑。

以下是一个基础敌人AI控制器的实现:

using UnityEngine;
using UnityEngine.AI;

namespace DarkOrder.AI
{
    /// <summary>
    /// 敌人状态枚举
    /// </summary>
    public enum EnemyState
    {
        Idle,       // 待机
        Patrol,     // 巡逻
        Chase,      // 追击
        Attack,     // 攻击
        Flee,       // 逃跑
        Hurt,       // 受伤
        Dead        // 死亡
    }
    
    /// <summary>
    /// 基础敌人AI控制器 - 包含状态机和基础行为
    /// </summary>
    [RequireComponent(typeof(NavMeshAgent))]
    [RequireComponent(typeof(Animator))]
    [RequireComponent(typeof(EnemyHealth))]
    public class EnemyAIController : MonoBehaviour
    {
        [Header("组件引用")]
        [SerializeField] 
        protected NavMeshAgent m_NavAgent;
        
        [SerializeField] 
        protected Animator m_Animator;
        
        [SerializeField] 
        protected EnemyHealth m_Health;
        
        [Header("感知参数")]
        [SerializeField] 
        protected float m_SightRange = 15f;
        
        [SerializeField] 
        protected float m_FOVAngle = 60f;
        
        [SerializeField] 
        protected float m_HearingRange = 10f;
        
        [SerializeField] 
        protected LayerMask m_ObstacleLayers; // 视线遮挡层
        
        [Header("行为参数")]
        [SerializeField] 
        protected float m_WalkSpeed = 2f;
        
        [SerializeField] 
        protected float m_RunSpeed = 5f;
        
        [SerializeField] 
        protected float m_FleeSpeed = 6f;
        
        [SerializeField] 
        protected float m_FleeHealthThreshold = 30f; // 血量低于此值逃跑
        
        [Header("巡逻参数")]
        [SerializeField] 
        protected Transform[] m_PatrolPoints;
        
        [SerializeField] 
        protected float m_WaitTimeAtPoint = 2f;
        
        // 状态
        protected EnemyState m_CurrentState = EnemyState.Idle;
        protected Transform m_PlayerTransform;
        protected float m_StateEnterTime;
        
        // 巡逻相关
        protected int m_CurrentPatrolIndex = 0;
        protected float m_WaitTimer;
        
        protected virtual void Start()
        {
            // 获取组件
            if (m_NavAgent == null)
                m_NavAgent = GetComponent<NavMeshAgent>();
                
            if (m_Animator == null)
                m_Animator = GetComponent<Animator>();
                
            if (m_Health == null)
                m_Health = GetComponent<EnemyHealth>();
            
            // 查找玩家
            GameObject player = GameObject.FindGameObjectWithTag("Player");
            if (player != null)
            {
                m_PlayerTransform = player.transform;
            }
            
            // 初始状态
            ChangeState(EnemyState.Patrol);
        }
        
        protected virtual void Update()
        {
            // 死亡状态不更新
            if (m_CurrentState == EnemyState.Dead)
                return;
            
            // 状态更新
            switch (m_CurrentState)
            {
                case EnemyState.Idle:
                    UpdateIdle();
                    break;
                case EnemyState.Patrol:
                    UpdatePatrol();
                    break;
                case EnemyState.Chase:
                    UpdateChase();
                    break;
                case EnemyState.Attack:
                    UpdateAttack();
                    break;
                case EnemyState.Flee:
                    UpdateFlee();
                    break;
                case EnemyState.Hurt:
                    UpdateHurt();
                    break;
            }
            
            // 动画参数更新
            UpdateAnimator();
        }
        
        /// <summary>
        /// 状态切换
        /// </summary>
        protected virtual void ChangeState(EnemyState newState)
        {
            if (m_CurrentState == newState)
                return;
            
            // 退出当前状态
            ExitState(m_CurrentState);
            
            // 进入新状态
            m_CurrentState = newState;
            m_StateEnterTime = Time.time;
            
            EnterState(newState);
        }
        
        /// <summary>
        /// 进入状态
        /// </summary>
        protected virtual void EnterState(EnemyState state)
        {
            switch (state)
            {
                case EnemyState.Idle:
                    m_NavAgent.isStopped = true;
                    break;
                    
                case EnemyState.Patrol:
                    m_NavAgent.speed = m_WalkSpeed;
                    m_NavAgent.isStopped = false;
                    GoToNextPatrolPoint();
                    break;
                    
                case EnemyState.Chase:
                    m_NavAgent.speed = m_RunSpeed;
                    m_NavAgent.isStopped = false;
                    break;
                    
                case EnemyState.Flee:
                    m_NavAgent.speed = m_FleeSpeed;
                    m_NavAgent.isStopped = false;
                    // 逃跑时朝向远离玩家的方向
                    SetFleeDestination();
                    break;
                    
                case EnemyState.Hurt:
                    m_NavAgent.isStopped = true;
                    break;
                    
                case EnemyState.Dead:
                    m_NavAgent.isStopped = true;
                    m_NavAgent.enabled = false;
                    break;
            }
        }
        
        /// <summary>
        /// 退出状态
        /// </summary>
        protected virtual void ExitState(EnemyState state) { }
        
        /// <summary>
        /// 待机状态更新
        /// </summary>
        protected virtual void UpdateIdle()
        {
            // 检测玩家
            if (CanSeePlayer())
            {
                // 根据血量决定追击或逃跑
                if (ShouldFlee())
                {
                    ChangeState(EnemyState.Flee);
                }
                else
                {
                    ChangeState(EnemyState.Chase);
                }
                return;
            }
            
            // 待机一段时间后返回巡逻
            if (Time.time > m_StateEnterTime + m_WaitTimeAtPoint)
            {
                ChangeState(EnemyState.Patrol);
            }
        }
        
        /// <summary>
        /// 巡逻状态更新
        /// </summary>
        protected virtual void UpdatePatrol()
        {
            // 检测玩家
            if (CanSeePlayer())
            {
                if (ShouldFlee())
                {
                    ChangeState(EnemyState.Flee);
                }
                else
                {
                    ChangeState(EnemyState.Chase);
                }
                return;
            }
            
            // 检查是否到达巡逻点
            if (!m_NavAgent.pathPending && m_NavAgent.remainingDistance < 0.5f)
            {
                // 进入待机状态
                ChangeState(EnemyState.Idle);
            }
        }
        
        /// <summary>
        /// 追击状态更新
        /// </summary>
        protected virtual void UpdateChase()
        {
            if (m_PlayerTransform == null)
            {
                ChangeState(EnemyState.Patrol);
                return;
            }
            
            // 检测是否还能看到玩家
            if (!CanSeePlayer())
            {
                // 丢失目标,回到最后看到的位置搜索
                ChangeState(EnemyState.Idle);
                return;
            }
            
            // 更新目标位置
            m_NavAgent.SetDestination(m_PlayerTransform.position);
            
            // 检测攻击距离
            float distance = Vector3.Distance(transform.position, m_PlayerTransform.position);
            if (distance <= GetAttackRange())
            {
                ChangeState(EnemyState.Attack);
            }
            
            // 追击过程中再次检查血量(可能被其他敌人打伤)
            if (ShouldFlee())
            {
                ChangeState(EnemyState.Flee);
            }
        }
        
        /// <summary>
        /// 攻击状态更新
        /// </summary>
        protected virtual void UpdateAttack()
        {
            if (m_PlayerTransform == null)
            {
                ChangeState(EnemyState.Patrol);
                return;
            }
            
            // 转向玩家
            Vector3 direction = (m_PlayerTransform.position - transform.position).normalized;
            direction.y = 0;
            if (direction != Vector3.zero)
            {
                transform.rotation = Quaternion.LookRotation(direction);
            }
            
            // 检查是否还在攻击范围内
            float distance = Vector3.Distance(transform.position, m_PlayerTransform.position);
            if (distance > GetAttackRange())
            {
                ChangeState(EnemyState.Chase);
                return;
            }
            
            // 攻击由动画事件触发,这里只负责检测
        }
        
        /// <summary>
        /// 逃跑状态更新
        /// </summary>
        protected virtual void UpdateFlee()
        {
            if (m_PlayerTransform == null)
            {
                ChangeState(EnemyState.Patrol);
                return;
            }
            
            // 如果血量恢复,不再逃跑
            if (!ShouldFlee())
            {
                ChangeState(EnemyState.Chase);
                return;
            }
            
            // 如果跑出太远或看不到玩家,回到巡逻
            float distance = Vector3.Distance(transform.position, m_PlayerTransform.position);
            if (distance > m_SightRange * 1.5f || !CanSeePlayer())
            {
                ChangeState(EnemyState.Patrol);
                return;
            }
            
            // 定期更新逃跑方向
            if (m_NavAgent.remainingDistance < 2f)
            {
                SetFleeDestination();
            }
        }
        
        /// <summary>
        /// 受伤状态更新
        /// </summary>
        protected virtual void UpdateHurt()
        {
            // 受伤动画播放完毕后自动切换状态
            // 由动画事件调用 OnHurtAnimationComplete
        }
        
        /// <summary>
        /// 动画参数更新
        /// </summary>
        protected virtual void UpdateAnimator()
        {
            if (m_Animator == null) return;
            
            // 获取当前速度
            float speed = m_NavAgent.velocity.magnitude;
            
            // 归一化速度用于动画混合
            float normalizedSpeed = 0f;
            switch (m_CurrentState)
            {
                case EnemyState.Patrol:
                    normalizedSpeed = speed / m_WalkSpeed;
                    break;
                case EnemyState.Chase:
                    normalizedSpeed = speed / m_RunSpeed;
                    break;
                case EnemyState.Flee:
                    normalizedSpeed = speed / m_FleeSpeed;
                    break;
                default:
                    normalizedSpeed = 0f;
                    break;
            }
            
            m_Animator.SetFloat("Speed", normalizedSpeed);
            m_Animator.SetBool("IsMoving", speed > 0.1f);
        }
        
        /// <summary>
        /// 检测是否能看到玩家
        /// </summary>
        protected virtual bool CanSeePlayer()
        {
            if (m_PlayerTransform == null)
                return false;
            
            Vector3 directionToPlayer = m_PlayerTransform.position - transform.position;
            float distance = directionToPlayer.magnitude;
            
            // 距离检测
            if (distance > m_SightRange)
                return false;
            
            // 角度检测
            float angle = Vector3.Angle(transform.forward, directionToPlayer.normalized);
            if (angle > m_FOVAngle / 2)
                return false;
            
            // 视线检测(是否有遮挡)
            RaycastHit hit;
            if (Physics.Raycast(transform.position + Vector3.up, directionToPlayer.normalized, out hit, distance, m_ObstacleLayers))
            {
                // 如果打到的不是玩家,说明被遮挡
                if (!hit.collider.CompareTag("Player"))
                {
                    return false;
                }
            }
            
            return true;
        }
        
        /// <summary>
        /// 判断是否应该逃跑
        /// </summary>
        protected virtual bool ShouldFlee()
        {
            if (m_Health == null)
                return false;
                
            float healthPercent = (float)m_Health.CurrentHealth / m_Health.MaxHealth * 100f;
            return healthPercent < m_FleeHealthThreshold;
        }
        
        /// <summary>
        /// 获取攻击范围(由子类重写)
        /// </summary>
        protected virtual float GetAttackRange()
        {
            return 2f; // 默认近战范围
        }
        
        /// <summary>
        /// 设置逃跑目的地(远离玩家)
        /// </summary>
        protected virtual void SetFleeDestination()
        {
            if (m_PlayerTransform == null) return;
            
            // 计算远离玩家的方向
            Vector3 fleeDirection = transform.position - m_PlayerTransform.position;
            fleeDirection.Normalize();
            
            // 随机偏移,避免直线逃跑
            fleeDirection = Quaternion.Euler(0, Random.Range(-45f, 45f), 0) * fleeDirection;
            
            // 计算逃跑目标点
            Vector3 fleePosition = transform.position + fleeDirection * 20f;
            
            // 在NavMesh上查找有效点
            NavMeshHit hit;
            if (NavMesh.SamplePosition(fleePosition, out hit, 5f, NavMesh.AllAreas))
            {
                m_NavAgent.SetDestination(hit.position);
            }
        }
        
        /// <summary>
        /// 前往下一个巡逻点
        /// </summary>
        protected virtual void GoToNextPatrolPoint()
        {
            if (m_PatrolPoints.Length == 0)
                return;
            
            m_CurrentPatrolIndex = (m_CurrentPatrolIndex + 1) % m_PatrolPoints.Length;
            m_NavAgent.SetDestination(m_PatrolPoints[m_CurrentPatrolIndex].position);
        }
        
        /// <summary>
        /// 动画事件 - 攻击命中
        /// </summary>
        public virtual void OnAttackHit()
        {
            // 在攻击动画的特定帧调用,处理伤害
            Debug.Log("敌人攻击命中");
        }
        
        /// <summary>
        /// 动画事件 - 受伤动画完成
        /// </summary>
        public virtual void OnHurtAnimationComplete()
        {
            // 回到追击或逃跑状态
            if (ShouldFlee())
            {
                ChangeState(EnemyState.Flee);
            }
            else
            {
                ChangeState(EnemyState.Chase);
            }
        }
        
        /// <summary>
        /// 动画事件 - 死亡动画完成
        /// </summary>
        public virtual void OnDeathAnimationComplete()
        {
            // 销毁敌人或进入等待复活状态
            Destroy(gameObject, 2f);
        }
        
        /// <summary>
        /// 公共方法 - 敌人受伤时调用
        /// </summary>
        public virtual void TakeDamage(int damage, bool isHeadshot)
        {
            if (m_Health == null || m_CurrentState == EnemyState.Dead)
                return;
            
            // 扣血
            m_Health.TakeDamage(damage, isHeadshot);
            
            // 切换到受伤状态
            ChangeState(EnemyState.Hurt);
            
            // 触发受伤动画
            m_Animator.SetTrigger("Hurt");
            
            // 如果死亡
            if (m_Health.IsDead)
            {
                ChangeState(EnemyState.Dead);
                m_Animator.SetTrigger("Death");
            }
        }
    }
}

这个基础AI控制器实现了以下核心功能:

  • 状态机管理:通过ChangeState方法切换状态,每个状态有独立的Update方法
  • 感知系统CanSeePlayer方法结合距离、角度和射线检测,模拟敌人的视觉
  • 逃跑逻辑ShouldFlee根据血量百分比决定是否逃跑,SetFleeDestination计算远离玩家的方向
  • 动画同步UpdateAnimator将NavMeshAgent的速度传递给动画系统
  • 扩展性:关键方法标记为virtual,子类可以重写实现不同类型敌人的差异化行为

11.1.3 敌人检测玩家并追击的实现

上述代码中的CanSeePlayer实现了基础的视觉检测。但在实际游戏中,还需要考虑多种检测方式:

  1. 视觉检测:通过射线检测和视野角度判断
  2. 听觉检测:玩家移动、开火等行为产生的噪声
  3. 触发检测:玩家进入特定区域(如警戒区)

以下是一个增强的感知系统实现:

using UnityEngine;

namespace DarkOrder.AI
{
    /// <summary>
    /// 敌人感知系统 - 扩展基础AI的检测能力
    /// </summary>
    public class EnemyPerception : MonoBehaviour
    {
        [Header("视觉参数")]
        [SerializeField] 
        private float m_SightRange = 15f;
        
        [SerializeField] 
        private float m_FOVAngle = 60f;
        
        [SerializeField] 
        private float m_DetectionTime = 1f; // 从发现到确认需要的时间
        
        [Header("听觉参数")]
        [SerializeField] 
        private float m_HearingRange = 10f;
        
        [SerializeField] 
        private float m_AlertDuration = 3f; // 听到声音后保持警觉的时间
        
        [Header("引用")]
        [SerializeField] 
        private Transform m_EyesPosition; // 眼睛位置
        
        private EnemyAIController m_Controller;
        private Transform m_Player;
        private float m_DetectionProgress = 0f;
        private float m_LastHearTime = -999f;
        private Vector3 m_LastKnownPosition;
        
        private void Start()
        {
            m_Controller = GetComponent<EnemyAIController>();
            m_Player = GameObject.FindGameObjectWithTag("Player")?.transform;
            
            if (m_EyesPosition == null)
                m_EyesPosition = transform;
        }
        
        private void Update()
        {
            if (m_Player == null) return;
            
            // 视觉检测
            bool canSee = CheckVision();
            
            if (canSee)
            {
                m_DetectionProgress += Time.deltaTime;
                m_LastKnownPosition = m_Player.position;
                
                if (m_DetectionProgress >= m_DetectionTime)
                {
                    // 确认看到玩家
                    m_Controller.OnPlayerDetected(m_LastKnownPosition);
                }
            }
            else
            {
                m_DetectionProgress = Mathf.Max(0, m_DetectionProgress - Time.deltaTime * 2f);
            }
            
            // 听觉检测由外部调用 ReportNoise 触发
        }
        
        /// <summary>
        /// 视觉检测
        /// </summary>
        private bool CheckVision()
        {
            Vector3 directionToPlayer = m_Player.position - m_EyesPosition.position;
            float distance = directionToPlayer.magnitude;
            
            if (distance > m_SightRange)
                return false;
            
            float angle = Vector3.Angle(m_EyesPosition.forward, directionToPlayer.normalized);
            if (angle > m_FOVAngle / 2)
                return false;
            
            RaycastHit hit;
            if (Physics.Raycast(m_EyesPosition.position, directionToPlayer.normalized, out hit, distance))
            {
                if (hit.collider.CompareTag("Player"))
                {
                    return true;
                }
            }
            
            return false;
        }
        
        /// <summary>
        /// 报告噪声
        /// </summary>
        public void ReportNoise(Vector3 noisePosition, float noiseIntensity)
        {
            float distance = Vector3.Distance(transform.position, noisePosition);
            
            if (distance <= m_HearingRange * noiseIntensity)
            {
                m_LastHearTime = Time.time;
                m_LastKnownPosition = noisePosition;
                
                // 让敌人警觉
                m_Controller.OnNoiseHeard(noisePosition);
            }
        }
        
        /// <summary>
        /// 获取最后已知的玩家位置
        /// </summary>
        public Vector3 GetLastKnownPosition()
        {
            return m_LastKnownPosition;
        }
        
        /// <summary>
        /// 是否处于警觉状态
        /// </summary>
        public bool IsAlerted()
        {
            return Time.time - m_LastHearTime < m_AlertDuration;
        }
        
        private void OnDrawGizmosSelected()
        {
            // 绘制视野范围
            Gizmos.color = Color.yellow;
            Gizmos.DrawWireSphere(transform.position, m_SightRange);
            
            // 绘制视野锥形
            Vector3 forward = m_EyesPosition.forward;
            float halfAngle = m_FOVAngle / 2;
            Quaternion leftRay = Quaternion.Euler(0, -halfAngle, 0);
            Quaternion rightRay = Quaternion.Euler(0, halfAngle, 0);
            
            Vector3 leftDirection = leftRay * forward;
            Vector3 rightDirection = rightRay * forward;
            
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(m_EyesPosition.position, leftDirection * m_SightRange);
            Gizmos.DrawRay(m_EyesPosition.position, rightDirection * m_SightRange);
        }
    }
}

11.1.4 Unity AI系统总结与展望

Unity的AI系统主要由三个层次构成:

1. 导航网格(NavMesh):通过烘焙场景中的静态几何体,生成可移动区域。这是所有移动的基础,相当于给AI一张"地图"。烘焙时需考虑Agent Radius(代理半径)、Agent Height(高度)、Max Slope(最大坡度)、Step Height(台阶高度)等参数。

2. 导航网格代理(NavMeshAgent):附加到角色上的组件,负责路径寻路和避障。只需设置agent.destination = targetPosition,代理会自动计算最优路径并移动。

3. 行为逻辑:通过脚本实现的状态机或行为树,决定AI在何时做什么。

对于《暗黑王朝》这样的商业项目,我们采用了分层设计:NavMeshAgent负责底层移动,自定义状态机负责高层决策。这种设计的优势是:

  • 职责分离:移动和决策逻辑解耦,便于调试
  • 性能优化:可通过调整NavMeshAgent的更新频率控制性能开销
  • 扩展性:可以方便地接入行为树等高级AI架构

未来展望方面,Unity正在推动基于DOTS的AI解决方案,可以处理海量AI单位。但对于移动平台,传统FSM配合对象池仍然是性能与效果的最佳平衡点。

11.2 角色血量系统的绑定

11.2.1 血量系统脚本的编写

血量系统是AI与玩家交互的基础。以下是一个通用的血量系统实现:

using UnityEngine;
using UnityEngine.Events;

namespace DarkOrder.Core
{
    /// <summary>
    /// 血量系统 - 可挂载到任何需要生命值的对象
    /// </summary>
    public class HealthSystem : MonoBehaviour
    {
        [Header("基础属性")]
        [SerializeField] 
        protected int m_MaxHealth = 100;
        
        [SerializeField] 
        protected int m_CurrentHealth;
        
        [Header("无敌帧")]
        [SerializeField] 
        protected bool m_EnableInvincibility = false;
        
        [SerializeField] 
        protected float m_InvincibilityDuration = 1f;
        
        [Header("事件")]
        public UnityEvent<int, int> OnHealthChanged; // 当前/最大
        public UnityEvent OnDamaged;
        public UnityEvent OnHealed;
        public UnityEvent OnDeath;
        
        protected bool m_IsInvincible = false;
        protected float m_LastDamageTime = -999f;
        
        public int MaxHealth => m_MaxHealth;
        public int CurrentHealth => m_CurrentHealth;
        public bool IsDead => m_CurrentHealth <= 0;
        
        protected virtual void Start()
        {
            m_CurrentHealth = m_MaxHealth;
        }
        
        /// <summary>
        /// 受到伤害
        /// </summary>
        public virtual void TakeDamage(int damage, GameObject damageSource = null)
        {
            if (m_IsInvincible || IsDead)
                return;
            
            // 无敌帧检测
            if (m_EnableInvincibility)
            {
                if (Time.time < m_LastDamageTime + m_InvincibilityDuration)
                    return;
                    
                m_LastDamageTime = Time.time;
                StartCoroutine(InvincibilityRoutine());
            }
            
            // 扣血
            m_CurrentHealth = Mathf.Max(0, m_CurrentHealth - damage);
            
            // 触发事件
            OnHealthChanged?.Invoke(m_CurrentHealth, m_MaxHealth);
            OnDamaged?.Invoke();
            
            // 死亡检测
            if (m_CurrentHealth <= 0)
            {
                OnDeath?.Invoke();
            }
        }
        
        /// <summary>
        /// 治疗
        /// </summary>
        public virtual void Heal(int amount)
        {
            if (IsDead)
                return;
            
            m_CurrentHealth = Mathf.Min(m_MaxHealth, m_CurrentHealth + amount);
            
            OnHealthChanged?.Invoke(m_CurrentHealth, m_MaxHealth);
            OnHealed?.Invoke();
        }
        
        /// <summary>
        /// 设置最大血量
        /// </summary>
        public virtual void SetMaxHealth(int maxHealth, bool healToFull = false)
        {
            m_MaxHealth = maxHealth;
            
            if (healToFull)
            {
                m_CurrentHealth = m_MaxHealth;
            }
            else
            {
                m_CurrentHealth = Mathf.Min(m_CurrentHealth, m_MaxHealth);
            }
            
            OnHealthChanged?.Invoke(m_CurrentHealth, m_MaxHealth);
        }
        
        protected System.Collections.IEnumerator InvincibilityRoutine()
        {
            m_IsInvincible = true;
            yield return new WaitForSeconds(m_InvincibilityDuration);
            m_IsInvincible = false;
        }
    }
    
    /// <summary>
    /// 敌人血量系统 - 扩展基础血量,增加AI通知
    /// </summary>
    public class EnemyHealth : HealthSystem
    {
        [Header("敌人特有")]
        [SerializeField] 
        private EnemyAIController m_AIController;
        
        protected override void Start()
        {
            base.Start();
            
            if (m_AIController == null)
                m_AIController = GetComponent<EnemyAIController>();
            
            // 绑定事件
            OnDamaged.AddListener(NotifyAIDamaged);
            OnDeath.AddListener(NotifyAIDeath);
        }
        
        private void NotifyAIDamaged()
        {
            if (m_AIController != null)
            {
                // 通知AI受到攻击
                m_AIController.TakeDamage(0, false); // 实际伤害在HealthSystem处理
            }
        }
        
        private void NotifyAIDeath()
        {
            if (m_AIController != null)
            {
                // 通知AI死亡
                m_AIController.OnDeath();
            }
        }
    }
}

11.2.2 脚本与角色的绑定

将血量系统挂载到角色上的步骤:

  1. 添加组件:在敌人预制体上添加EnemyHealth组件
  2. 设置参数:配置最大血量、无敌帧时长等
  3. 连接AI:将EnemyAIController引用拖入m_AIController字段(或自动获取)

为了在编辑器中方便配置,可以创建血量数据资产:

using UnityEngine;

[CreateAssetMenu(fileName = "NewHealthConfig", menuName = "DarkOrder/Health Config")]
public class HealthConfig : ScriptableObject
{
    public int maxHealth = 100;
    public bool enableInvincibility = true;
    public float invincibilityDuration = 0.5f;
}

// 在EnemyHealth中添加
[Header("配置")]
[SerializeField] private HealthConfig m_Config;

protected override void Start()
{
    if (m_Config != null)
    {
        m_MaxHealth = m_Config.maxHealth;
        m_EnableInvincibility = m_Config.enableInvincibility;
        m_InvincibilityDuration = m_Config.invincibilityDuration;
    }
    base.Start();
}

11.3 远距离攻击敌人的制作

11.3.1 AI系统的导入与配置

对于远程敌人,我们需要在基础AI上增加以下能力:

  • 保持距离:在攻击范围内但不靠太近
  • 投射物发射:生成子弹或抛射物
  • 瞄准预测:如果玩家移动,需要提前量计算

首先,创建远程敌人的配置资产:

using UnityEngine;

[CreateAssetMenu(fileName = "NewRangedConfig", menuName = "DarkOrder/AI/Ranged Config")]
public class RangedEnemyConfig : ScriptableObject
{
    [Header("攻击参数")]
    public float attackRange = 20f;
    public float preferredDistance = 15f; // 最佳攻击距离
    public float attackCooldown = 1.5f;
    public int damagePerShot = 15;
    public float projectileSpeed = 30f;
    public float accuracy = 0.9f; // 精度 0-1
    
    [Header("投射物")]
    public GameObject projectilePrefab;
    public Transform projectileSpawnPoint;
}

11.3.2 远程敌人AI的实现

using UnityEngine;

namespace DarkOrder.AI
{
    /// <summary>
    /// 远程敌人AI控制器
    /// </summary>
    public class RangedEnemyAI : EnemyAIController
    {
        [Header("远程配置")]
        [SerializeField] 
        private RangedEnemyConfig m_RangedConfig;
        
        [Header("投射物生成点")]
        [SerializeField] 
        private Transform m_ProjectileSpawnPoint;
        
        private float m_LastAttackTime = -999f;
        
        protected override void Start()
        {
            base.Start();
            
            if (m_RangedConfig != null)
            {
                m_RangedConfig.projectileSpawnPoint = m_ProjectileSpawnPoint;
            }
        }
        
        /// <summary>
        /// 重写攻击范围
        /// </summary>
        protected override float GetAttackRange()
        {
            return m_RangedConfig != null ? m_RangedConfig.attackRange : 15f;
        }
        
        /// <summary>
        /// 重写追击状态
        /// </summary>
        protected override void UpdateChase()
        {
            if (m_PlayerTransform == null)
            {
                ChangeState(EnemyState.Patrol);
                return;
            }
            
            // 检测是否能看见玩家
            if (!CanSeePlayer())
            {
                ChangeState(EnemyState.Idle);
                return;
            }
            
            float distance = Vector3.Distance(transform.position, m_PlayerTransform.position);
            
            // 如果太远,靠近
            if (distance > m_RangedConfig.preferredDistance)
            {
                m_NavAgent.SetDestination(m_PlayerTransform.position);
            }
            // 如果太近,后退
            else if (distance < m_RangedConfig.preferredDistance * 0.7f)
            {
                // 远离玩家的方向
                Vector3 fleeDirection = transform.position - m_PlayerTransform.position;
                Vector3 targetPos = transform.position + fleeDirection.normalized * m_RangedConfig.preferredDistance;
                
                NavMeshHit hit;
                if (NavMesh.SamplePosition(targetPos, out hit, 5f, NavMesh.AllAreas))
                {
                    m_NavAgent.SetDestination(hit.position);
                }
            }
            else
            {
                // 在理想距离内,停止移动准备攻击
                m_NavAgent.isStopped = true;
                
                // 转向玩家
                Vector3 direction = (m_PlayerTransform.position - transform.position).normalized;
                direction.y = 0;
                transform.rotation = Quaternion.LookRotation(direction);
            }
            
            // 攻击检测
            if (distance <= GetAttackRange() && Time.time > m_LastAttackTime + m_RangedConfig.attackCooldown)
            {
                PerformRangedAttack();
            }
        }
        
        /// <summary>
        /// 执行远程攻击
        /// </summary>
        protected virtual void PerformRangedAttack()
        {
            m_LastAttackTime = Time.time;
            
            // 触发攻击动画
            m_Animator.SetTrigger("Attack");
        }
        
        /// <summary>
        /// 动画事件 - 发射投射物
        /// </summary>
        public void FireProjectile()
        {
            if (m_RangedConfig == null || m_ProjectileSpawnPoint == null || m_PlayerTransform == null)
                return;
            
            // 计算瞄准方向(带精度)
            Vector3 targetPosition = m_PlayerTransform.position + Vector3.up * 1.5f; // 瞄准身体中心
            
            // 精度影响
            float accuracyFactor = 1f - m_RangedConfig.accuracy;
            targetPosition += new Vector3(
                Random.Range(-accuracyFactor, accuracyFactor) * 2f,
                Random.Range(-accuracyFactor, accuracyFactor) * 1f,
                Random.Range(-accuracyFactor, accuracyFactor) * 2f
            );
            
            Vector3 direction = (targetPosition - m_ProjectileSpawnPoint.position).normalized;
            
            // 实例化投射物
            GameObject projectile = Instantiate(
                m_RangedConfig.projectilePrefab,
                m_ProjectileSpawnPoint.position,
                Quaternion.LookRotation(direction)
            );
            
            // 设置投射物属性
            Projectile proj = projectile.GetComponent<Projectile>();
            if (proj != null)
            {
                proj.Initialize(m_RangedConfig.damagePerShot, m_RangedConfig.projectileSpeed);
            }
            
            // 播放音效
            // AudioManager.Instance.PlaySound("enemy_gunshot", transform.position);
        }
    }
}

11.3.3 "眼睛"位置的设置

对于AI来说,"眼睛"的位置决定了它从哪个点发射视线。正确的设置可以避免敌人被自己的身体遮挡视线。

在远程敌人预制体中:

  1. 创建一个空物体,命名为"Eyes"
  2. 将其放置在敌人头部附近(通常略高于眼睛)
  3. 在Inspector中将位置微调至正确位置
  4. EnemyPerception组件中引用该Transform

设置要点:

  • 眼睛位置应略高于实际眼睛,以便从掩体后观察
  • 避免放在模型内部,否则射线可能被自身碰撞体阻挡
  • 可以考虑添加多个眼睛点(如左右眼)并取平均值,提高检测准确性

11.3.4 动画系统的绑定

远程敌人的动画系统需要设置以下参数:

  • Attack:触发攻击动画
  • Reload(可选):装弹动画
  • Aiming(可选):瞄准姿势混合

在Animator Controller中创建状态机:

  1. 导入动画剪辑:Idle、Walk、Run、Attack、Hurt、Death
  2. 创建混合树:根据Speed参数在Idle、Walk、Run之间过渡
  3. 设置Attack层:通过Attack Trigger触发,播放完毕后返回原状态
  4. 设置Hurt和Death层:优先级最高,中断其他动画

11.3.5 攻击目标与其他数值的配置

在Unity编辑器中,为每个远程敌人预制体配置:

  • 感知参数:Sight Range(视野距离)、FOV Angle(视野角度)
  • 移动参数:Walk Speed、Run Speed
  • 攻击参数:Attack Range、Attack Cooldown、Damage Per Shot
  • 投射物:Projectile Prefab、Spawn Point

使用ScriptableObject管理不同敌人类型的配置,便于批量调整和复用。

11.3.6 攻击组件的绑定

投射物脚本的实现:

using UnityEngine;

namespace DarkOrder.Weapons
{
    public class Projectile : MonoBehaviour
    {
        [Header("飞行参数")]
        [SerializeField] 
        private float m_Speed = 30f;
        
        [SerializeField] 
        private float m_MaxLifeTime = 5f;
        
        [SerializeField] 
        private GameObject m_HitEffect;
        
        private int m_Damage;
        private Vector3 m_Direction;
        private bool m_IsInitialized = false;
        
        private void Update()
        {
            if (!m_IsInitialized) return;
            
            // 移动
            transform.position += m_Direction * m_Speed * Time.deltaTime;
        }
        
        /// <summary>
        /// 初始化投射物
        /// </summary>
        public void Initialize(int damage, float speed)
        {
            m_Damage = damage;
            m_Speed = speed;
            m_Direction = transform.forward;
            m_IsInitialized = true;
            
            Destroy(gameObject, m_MaxLifeTime);
        }
        
        private void OnTriggerEnter(Collider other)
        {
            if (!m_IsInitialized) return;
            
            // 忽略敌人自身
            if (other.CompareTag("Enemy"))
                return;
            
            // 伤害玩家
            if (other.CompareTag("Player"))
            {
                HealthSystem health = other.GetComponent<HealthSystem>();
                if (health != null)
                {
                    health.TakeDamage(m_Damage, gameObject);
                }
            }
            
            // 生成命中特效
            if (m_HitEffect != null)
            {
                Instantiate(m_HitEffect, transform.position, Quaternion.identity);
            }
            
            Destroy(gameObject);
        }
    }
}

11.4 制作近距离攻击"狼人"敌人

11.4.1 近战敌人AI系统的绑定

近战敌人需要更复杂的攻击逻辑,包括连击、重击、攻击范围检测等。以下是一个近战敌人的实现:

using UnityEngine;

namespace DarkOrder.AI
{
    /// <summary>
    /// 近战敌人配置
    /// </summary>
    [CreateAssetMenu(fileName = "NewMeleeConfig", menuName = "DarkOrder/AI/Melee Config")]
    public class MeleeEnemyConfig : ScriptableObject
    {
        [Header("攻击参数")]
        public float attackRange = 2.5f;
        public float attackCooldown = 1.2f;
        public int lightAttackDamage = 10;
        public int heavyAttackDamage = 25;
        public float heavyAttackChance = 0.3f; // 重击概率
        
        [Header("连击参数")]
        public bool enableCombo = true;
        public int maxComboCount = 3;
        public float comboWindow = 1.5f; // 连击时间窗口
    }
    
    /// <summary>
    /// 近战敌人AI控制器
    /// </summary>
    public class MeleeEnemyAI : EnemyAIController
    {
        [Header("近战配置")]
        [SerializeField] 
        private MeleeEnemyConfig m_MeleeConfig;
        
        [Header("攻击检测")]
        [SerializeField] 
        private Transform m_AttackPoint;
        
        [SerializeField] 
        private float m_AttackRadius = 1.5f;
        
        [SerializeField] 
        private LayerMask m_PlayerLayer;
        
        // 连击状态
        private int m_CurrentCombo = 0;
        private float m_LastAttackTime = -999f;
        private float m_ComboTimeout = 0f;
        
        protected override float GetAttackRange()
        {
            return m_MeleeConfig != null ? m_MeleeConfig.attackRange : 2f;
        }
        
        protected override void UpdateAttack()
        {
            if (m_PlayerTransform == null)
            {
                ChangeState(EnemyState.Patrol);
                return;
            }
            
            // 转向玩家
            Vector3 direction = (m_PlayerTransform.position - transform.position).normalized;
            direction.y = 0;
            if (direction != Vector3.zero)
            {
                transform.rotation = Quaternion.Slerp(
                    transform.rotation,
                    Quaternion.LookRotation(direction),
                    Time.deltaTime * 5f
                );
            }
            
            // 检查是否还在攻击范围内
            float distance = Vector3.Distance(transform.position, m_PlayerTransform.position);
            if (distance > GetAttackRange())
            {
                ChangeState(EnemyState.Chase);
                return;
            }
            
            // 攻击冷却检测
            if (Time.time > m_LastAttackTime + m_MeleeConfig.attackCooldown)
            {
                // 决定攻击类型
                bool isHeavy = Random.value < m_MeleeConfig.heavyAttackChance;
                PerformMeleeAttack(isHeavy);
            }
            
            // 连击超时检测
            if (m_ComboTimeout > 0 && Time.time > m_ComboTimeout)
            {
                ResetCombo();
            }
        }
        
        /// <summary>
        /// 执行近战攻击
        /// </summary>
        private void PerformMeleeAttack(bool isHeavy)
        {
            m_LastAttackTime = Time.time;
            
            // 触发相应动画
            if (isHeavy)
            {
                m_Animator.SetTrigger("HeavyAttack");
            }
            else
            {
                m_Animator.SetTrigger("LightAttack");
                
                // 连击逻辑
                if (m_MeleeConfig.enableCombo)
                {
                    m_CurrentCombo++;
                    if (m_CurrentCombo >= m_MeleeConfig.maxComboCount)
                    {
                        ResetCombo();
                    }
                    else
                    {
                        m_ComboTimeout = Time.time + m_MeleeConfig.comboWindow;
                    }
                }
            }
        }
        
        /// <summary>
        /// 动画事件 - 攻击命中
        /// </summary>
        public void OnAttackHit()
        {
            // 检测攻击范围内的玩家
            Collider[] hits = Physics.OverlapSphere(
                m_AttackPoint.position,
                m_AttackRadius,
                m_PlayerLayer
            );
            
            foreach (Collider hit in hits)
            {
                HealthSystem health = hit.GetComponent<HealthSystem>();
                if (health != null)
                {
                    // 根据当前连击阶段决定伤害
                    int damage = GetCurrentAttackDamage();
                    health.TakeDamage(damage, gameObject);
                    
                    // 击退效果
                    ApplyKnockback(hit.transform);
                }
            }
        }
        
        /// <summary>
        /// 获取当前攻击伤害
        /// </summary>
        private int GetCurrentAttackDamage()
        {
            // 简单实现:连击越高伤害越高
            if (m_CurrentCombo >= 2)
            {
                return m_MeleeConfig.heavyAttackDamage;
            }
            else
            {
                return m_MeleeConfig.lightAttackDamage;
            }
        }
        
        /// <summary>
        /// 应用击退
        /// </summary>
        private void ApplyKnockback(Transform target)
        {
            CharacterController controller = target.GetComponent<CharacterController>();
            if (controller != null)
            {
                Vector3 knockbackDir = (target.position - transform.position).normalized;
                knockbackDir.y = 0.5f; // 轻微向上
                
                // 简单的击退实现
                StartCoroutine(KnockbackRoutine(controller, knockbackDir));
            }
        }
        
        private System.Collections.IEnumerator KnockbackRoutine(CharacterController controller, Vector3 direction)
        {
            float force = 5f;
            float duration = 0.2f;
            float timer = 0;
            
            while (timer < duration)
            {
                controller.Move(direction * force * Time.deltaTime);
                timer += Time.deltaTime;
                yield return null;
            }
        }
        
        private void ResetCombo()
        {
            m_CurrentCombo = 0;
            m_ComboTimeout = 0f;
        }
        
        private void OnDrawGizmosSelected()
        {
            if (m_AttackPoint != null)
            {
                Gizmos.color = Color.red;
                Gizmos.DrawWireSphere(m_AttackPoint.position, m_AttackRadius);
            }
        }
    }
}

11.4.2 "狼眼"位置的设置

对于"狼人"这样的近战敌人,"眼睛"位置应与远程敌人类似,但考虑到其体型(通常是四足或半蹲姿态),需要调整高度:

  1. 创建"Eyes"空物体,放置在头部前方偏上位置
  2. 考虑其移动姿态:奔跑时头部可能更低,应取平均高度
  3. 可以添加多个射线点(如双眼)提高检测鲁棒性

11.4.3 动画与参数的制定

近战敌人的动画系统需要更精细的设计:

  • 攻击动画:轻击、重击、连击组合
  • 受击动画:根据受击方向(左/右/前/后)播放不同反应
  • 移动动画:根据Speed参数在Walk、Run、Sprint之间过渡

在Animator Controller中设置:

  1. 使用Blend Tree控制移动动画
  2. 使用Trigger控制攻击动画
  3. 使用Layer实现上下半身分离(如上半身攻击时下半身继续移动)

11.4.4 攻击力量的制定

攻击力量不仅是伤害数值,还包括:

  • 攻击力:基础伤害值
  • 击退力:命中时对玩家的推力
  • 硬直时间:命中后敌人的恢复时间
  • 破防能力:能否打破玩家的防御

在配置资产中定义这些参数:

[System.Serializable]
public class AttackData
{
    public string attackName;
    public int damage = 10;
    public float knockbackForce = 5f;
    public float selfStunDuration = 0.5f; // 自身硬直
    public float playerStunDuration = 0.3f; // 玩家硬直
    public bool canBreakGuard = false;
}

11.4.5 攻击环境的检测

近战攻击需要考虑地形和障碍物影响:

/// <summary>
/// 检测攻击环境 - 是否有障碍物阻挡
/// </summary>
private bool CanAttackPlayer()
{
    if (m_PlayerTransform == null) return false;
    
    Vector3 direction = m_PlayerTransform.position - m_AttackPoint.position;
    float distance = direction.magnitude;
    
    // 射线检测,检查是否有障碍物阻挡
    RaycastHit hit;
    if (Physics.Raycast(m_AttackPoint.position, direction.normalized, out hit, distance))
    {
        // 如果打到的是玩家,可以攻击
        if (hit.collider.CompareTag("Player"))
        {
            return true;
        }
        // 否则被障碍物阻挡
        return false;
    }
    
    return true;
}

11.5 本章小结

本章围绕《暗黑王朝》的敌人AI系统,从基础状态机到远程和近战敌人的差异化实现,系统讲解了完整的AI构建流程。

在基础AI部分,我们通过有限状态机实现了敌人的核心行为循环:待机-巡逻-追击-攻击-逃跑-受伤-死亡。通过CanSeePlayer方法实现了基于射线检测的视觉系统,通过ShouldFlee实现了动态的血量判断逃跑逻辑。NavMeshAgent的使用让敌人能够在复杂地形中自动寻路。

血量系统的实现采用分层设计:基础HealthSystem提供通用生命值功能,EnemyHealth扩展添加AI通知机制。通过ScriptableObject配置血量参数,实现了数据的复用和批量调整。

远程敌人AI在基础状态上增加了保持距离、投射物攻击、精度计算等特性。通过重写UpdateChase方法,实现了在最佳攻击距离停留的逻辑。投射物脚本处理飞行、碰撞、伤害等完整流程。

近战敌人"狼人"的实现更加复杂,包含了连击系统、重击概率、攻击范围检测、击退效果等。通过OverlapSphere检测攻击范围内的玩家,通过动画事件精确控制伤害判定时机。

通过本章的学习,读者应当能够独立构建一套完整的、可扩展的敌人AI系统,并理解不同类型敌人的设计差异。AI系统是游戏战斗体验的核心,《暗黑王朝》正是通过这些精心设计的敌人,为玩家带来了富有挑战性和真实感的战斗体验。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐