第11章 敌人AI系统构建:《暗黑王朝》智能战斗的完整实现
第11章 敌人AI系统构建:《暗黑王朝》智能战斗的完整实现
在《暗黑王朝》这样的写实射击手游中,敌人AI的品质直接影响游戏的可玩性和挑战性。一个优秀的AI系统,需要让敌人表现出符合其设定的行为——巡逻时警觉、发现玩家后追击、在合适距离发动攻击、血量过低时可能逃跑。本章将从最基础的"逃跑计划"脚本开始,逐步展开敌人状态机设计、玩家检测机制、血量系统绑定、远程与近战敌人的差异化实现等完整技术体系。结合《暗黑王朝》的实际开发经验,为读者呈现一套模块化、可扩展的移动平台AI解决方案。
11.1 基础敌人AI系统的构建
11.1.1 敌人模型导入与动画切分
在开始编写AI逻辑之前,需要将第7章制作的敌人模型导入Unity并进行正确的动画切分。根据第10章的经验,每个敌人模型需要包含以下基础动画状态:
- Idle:待机状态,角色静止时的微动
- Walk:普通移动
- Run:追击时奔跑
- Attack:攻击动作
- Hurt:受击反应
- Death:死亡
动画切分的关键步骤:
-
模型导入设置:在Inspector中打开Rig选项卡,Animation Type设置为Humanoid(人形)或Generic(非人型),点击Apply
-
动画分割:切换到Animation选项卡,勾选Import Animation。在Clips列表中,根据帧范围分割出上述动画剪辑。每个剪辑需勾选Loop Time(循环动画)确保无缝衔接
-
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实现了基础的视觉检测。但在实际游戏中,还需要考虑多种检测方式:
- 视觉检测:通过射线检测和视野角度判断
- 听觉检测:玩家移动、开火等行为产生的噪声
- 触发检测:玩家进入特定区域(如警戒区)
以下是一个增强的感知系统实现:
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 脚本与角色的绑定
将血量系统挂载到角色上的步骤:
- 添加组件:在敌人预制体上添加
EnemyHealth组件 - 设置参数:配置最大血量、无敌帧时长等
- 连接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来说,"眼睛"的位置决定了它从哪个点发射视线。正确的设置可以避免敌人被自己的身体遮挡视线。
在远程敌人预制体中:
- 创建一个空物体,命名为"Eyes"
- 将其放置在敌人头部附近(通常略高于眼睛)
- 在Inspector中将位置微调至正确位置
- 在
EnemyPerception组件中引用该Transform
设置要点:
- 眼睛位置应略高于实际眼睛,以便从掩体后观察
- 避免放在模型内部,否则射线可能被自身碰撞体阻挡
- 可以考虑添加多个眼睛点(如左右眼)并取平均值,提高检测准确性
11.3.4 动画系统的绑定
远程敌人的动画系统需要设置以下参数:
Attack:触发攻击动画Reload(可选):装弹动画Aiming(可选):瞄准姿势混合
在Animator Controller中创建状态机:
- 导入动画剪辑:Idle、Walk、Run、Attack、Hurt、Death
- 创建混合树:根据Speed参数在Idle、Walk、Run之间过渡
- 设置Attack层:通过Attack Trigger触发,播放完毕后返回原状态
- 设置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 "狼眼"位置的设置
对于"狼人"这样的近战敌人,"眼睛"位置应与远程敌人类似,但考虑到其体型(通常是四足或半蹲姿态),需要调整高度:
- 创建"Eyes"空物体,放置在头部前方偏上位置
- 考虑其移动姿态:奔跑时头部可能更低,应取平均高度
- 可以添加多个射线点(如双眼)提高检测鲁棒性
11.4.3 动画与参数的制定
近战敌人的动画系统需要更精细的设计:
- 攻击动画:轻击、重击、连击组合
- 受击动画:根据受击方向(左/右/前/后)播放不同反应
- 移动动画:根据Speed参数在Walk、Run、Sprint之间过渡
在Animator Controller中设置:
- 使用Blend Tree控制移动动画
- 使用Trigger控制攻击动画
- 使用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系统是游戏战斗体验的核心,《暗黑王朝》正是通过这些精心设计的敌人,为玩家带来了富有挑战性和真实感的战斗体验。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)