第2章 Unity3D人工智能分层架构设计与实现
本文介绍了Unity3D游戏开发中人工智能分层架构的设计与实现。首先阐述了分层架构的理论基础,包括关注点分离原则、组件化设计和认知科学依据。然后重点讨论了运动控制层的具体实现,详细分析了Unity导航系统的深度应用,包括路径寻路、避障、动画状态管理等核心功能。通过增强型导航控制器的代码示例,展示了如何扩展Unity导航系统功能,实现更精细的控制和性能优化。这种分层架构设计不仅提高了代码可维护性,还
第2章 Unity3D人工智能分层架构设计与实现
2.1 游戏AI分层架构的理论基础
在Unity3D游戏开发中,人工智能系统的架构设计直接决定了游戏角色的行为表现和整体游戏体验的丰富度。一个完善的人工智能架构应当采用分层设计理念,将复杂的智能行为分解为多个逻辑层次,每一层负责特定抽象级别的功能实现。这种分层架构不仅提高了代码的可维护性和扩展性,还能更好地模拟真实世界中生物的行为决策过程。
从软件工程角度分析,游戏AI的分层架构遵循了关注点分离原则。每个层次专注于解决特定类型的问题,同时通过标准化的接口与其他层次通信。这种设计模式在商业游戏项目中尤为重要,因为它允许不同专业背景的开发人员并行工作:动画师和物理程序员可以专注于运动层,游戏设计师可以定义决策层逻辑,而系统架构师则可以规划战略层的整体框架。
在Unity引擎中实现AI分层架构时,需要充分利用Unity的组件系统。每个AI层次可以作为独立的MonoBehaviour组件,通过游戏对象组合形成完整的智能体。这种组件化设计符合Unity的架构哲学,同时提供了良好的运行时配置灵活性。例如,同一个决策层组件可以与不同的运动层组件组合,快速创建具有不同移动能力但相同行为模式的敌人类型。
从认知科学的角度来看,分层AI架构模拟了生物智能的层次结构。低级反射行为由运动层处理,中级习惯性行为由决策层控制,高级策略性思考则由战略层管理。这种模拟不仅使游戏角色行为更加真实,还能根据游戏情境在不同层次之间动态切换,创造出丰富的行为变化。例如,在平静状态下NPC可能执行战略层的巡逻规划,但在遭遇玩家时立即切换到决策层的战斗行为,同时运动层负责具体的闪避和攻击动作。
在商业游戏项目中,分层AI架构还支持动态难度调整和玩家适应性。通过监控玩家表现,系统可以调整各层次的参数:降低运动层的精度、简化决策层的逻辑或限制战略层的资源,从而实现平滑的难度曲线。这种适应性设计在现代游戏中越来越常见,它确保不同技能水平的玩家都能获得合适的挑战。
2.2 运动控制层的设计与实现
运动控制层是AI架构的最底层,负责将高级指令转换为具体的移动和动作。这一层处理所有与物理运动和动画播放相关的任务,包括路径寻路、避障、动画状态管理和物理交互。在Unity中实现运动控制层需要综合运用导航系统、动画系统和物理引擎的功能。
从技术角度看,运动控制层需要解决的核心问题包括:如何将抽象的目标位置转换为具体的移动路径,如何在移动过程中避免障碍物,如何平滑过渡不同动画状态,以及如何处理与其他游戏对象的物理交互。这些问题的解决方案直接影响AI角色的表现力和可信度。
2.2.1 Unity导航系统的深度应用
Unity内置的NavMesh导航系统为运动控制层提供了强大的基础功能。然而,在商业项目中,直接使用基本导航功能往往无法满足复杂需求。以下是增强型导航控制器的实现,它扩展了Unity导航系统的功能,提供了更精细的控制和更好的性能优化。
using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;
public class EnhancedNavigationController : MonoBehaviour
{
private NavMeshAgent navMeshAgent;
private Animator characterAnimator;
private Vector3 currentDestination;
private bool isDestinationValid;
private List<Vector3> pathCorners;
private int currentCornerIndex;
private float recalculatePathTimer;
private const float recalculatePathInterval = 0.5f;
[Header("导航参数")]
[SerializeField] private float baseSpeed = 3.5f;
[SerializeField] private float angularSpeed = 360f;
[SerializeField] private float acceleration = 8f;
[SerializeField] private float stoppingDistance = 0.5f;
[SerializeField] private float obstacleAvoidanceQuality = 50f;
[Header("路径优化")]
[SerializeField] private bool usePathSmoothing = true;
[SerializeField] private float pathSmoothingFactor = 0.5f;
[SerializeField] private bool useDynamicObstacleAvoidance = true;
[SerializeField] private float avoidanceRadius = 2.0f;
[Header("动画参数")]
[SerializeField] private string speedParameterName = "Speed";
[SerializeField] private string turnParameterName = "Turn";
[SerializeField] private float animationBlendSpeed = 5.0f;
private float currentSpeed;
private float currentTurn;
void Awake()
{
InitializeComponents();
ConfigureNavigationAgent();
}
void Update()
{
UpdatePathFollowing();
UpdateAnimationParameters();
HandleDynamicObstacleAvoidance();
recalculatePathTimer += Time.deltaTime;
if (recalculatePathTimer >= recalculatePathInterval)
{
RecalculatePathIfNeeded();
recalculatePathTimer = 0f;
}
}
private void InitializeComponents()
{
navMeshAgent = GetComponent<NavMeshAgent>();
if (navMeshAgent == null)
{
navMeshAgent = gameObject.AddComponent<NavMeshAgent>();
}
characterAnimator = GetComponent<Animator>();
if (characterAnimator == null)
{
Debug.LogWarning("未找到Animator组件,动画控制将不可用");
}
pathCorners = new List<Vector3>();
}
private void ConfigureNavigationAgent()
{
navMeshAgent.speed = baseSpeed;
navMeshAgent.angularSpeed = angularSpeed;
navMeshAgent.acceleration = acceleration;
navMeshAgent.stoppingDistance = stoppingDistance;
navMeshAgent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance;
navMeshAgent.radius = 0.5f;
navMeshAgent.height = 2.0f;
navMeshAgent.baseOffset = 0.0f;
}
public bool SetDestination(Vector3 destination)
{
NavMeshHit navMeshHit;
if (NavMesh.SamplePosition(destination, out navMeshHit, 5.0f, NavMesh.AllAreas))
{
currentDestination = navMeshHit.position;
isDestinationValid = true;
if (navMeshAgent.SetDestination(currentDestination))
{
UpdatePathCorners();
return true;
}
}
isDestinationValid = false;
Debug.LogWarning($"无法导航到目标位置: {destination}");
return false;
}
private void UpdatePathCorners()
{
pathCorners.Clear();
NavMeshPath currentPath = navMeshAgent.path;
if (currentPath != null && currentPath.corners.Length > 1)
{
for (int i = 0; i < currentPath.corners.Length; i++)
{
pathCorners.Add(currentPath.corners[i]);
}
if (usePathSmoothing)
{
ApplyPathSmoothing();
}
currentCornerIndex = 1;
}
else
{
currentCornerIndex = 0;
}
}
private void ApplyPathSmoothing()
{
if (pathCorners.Count < 3) return;
List<Vector3> smoothedPath = new List<Vector3>();
smoothedPath.Add(pathCorners[0]);
for (int i = 1; i < pathCorners.Count - 1; i++)
{
Vector3 smoothedPoint = Vector3.Lerp(
pathCorners[i],
(pathCorners[i - 1] + pathCorners[i + 1]) * 0.5f,
pathSmoothingFactor
);
smoothedPath.Add(smoothedPoint);
}
smoothedPath.Add(pathCorners[pathCorners.Count - 1]);
pathCorners = smoothedPath;
}
private void UpdatePathFollowing()
{
if (!isDestinationValid || pathCorners.Count <= 1) return;
if (currentCornerIndex < pathCorners.Count)
{
Vector3 directionToCorner = pathCorners[currentCornerIndex] - transform.position;
directionToCorner.y = 0;
if (directionToCorner.magnitude < stoppingDistance * 1.5f)
{
currentCornerIndex++;
}
else
{
Vector3 moveDirection = directionToCorner.normalized;
if (navMeshAgent.isOnNavMesh)
{
navMeshAgent.SetDestination(pathCorners[currentCornerIndex]);
}
}
}
if (currentCornerIndex >= pathCorners.Count)
{
StopMovement();
}
}
private void UpdateAnimationParameters()
{
if (characterAnimator == null) return;
Vector3 velocity = navMeshAgent.velocity;
Vector3 localVelocity = transform.InverseTransformDirection(velocity);
float targetSpeed = localVelocity.z / baseSpeed;
float targetTurn = localVelocity.x / baseSpeed;
currentSpeed = Mathf.Lerp(currentSpeed, targetSpeed, animationBlendSpeed * Time.deltaTime);
currentTurn = Mathf.Lerp(currentTurn, targetTurn, animationBlendSpeed * Time.deltaTime);
characterAnimator.SetFloat(speedParameterName, currentSpeed);
characterAnimator.SetFloat(turnParameterName, currentTurn);
}
private void HandleDynamicObstacleAvoidance()
{
if (!useDynamicObstacleAvoidance) return;
Collider[] nearbyColliders = Physics.OverlapSphere(transform.position, avoidanceRadius);
Vector3 avoidanceVector = Vector3.zero;
foreach (Collider collider in nearbyColliders)
{
if (collider.gameObject == gameObject) continue;
NavMeshAgent otherAgent = collider.GetComponent<NavMeshAgent>();
if (otherAgent != null && otherAgent.isActiveAndEnabled)
{
Vector3 toOther = collider.transform.position - transform.position;
float distance = toOther.magnitude;
if (distance < avoidanceRadius)
{
float avoidanceStrength = 1.0f - (distance / avoidanceRadius);
avoidanceVector -= toOther.normalized * avoidanceStrength;
}
}
}
if (avoidanceVector.magnitude > 0.1f && navMeshAgent.isOnNavMesh)
{
Vector3 newDirection = (navMeshAgent.desiredVelocity.normalized + avoidanceVector.normalized * 0.3f).normalized;
navMeshAgent.velocity = newDirection * navMeshAgent.speed;
}
}
private void RecalculatePathIfNeeded()
{
if (!isDestinationValid || !navMeshAgent.hasPath || navMeshAgent.pathStatus != NavMeshPathStatus.PathComplete)
{
if (isDestinationValid)
{
SetDestination(currentDestination);
}
}
}
public void StopMovement()
{
if (navMeshAgent.isOnNavMesh)
{
navMeshAgent.isStopped = true;
navMeshAgent.velocity = Vector3.zero;
}
if (characterAnimator != null)
{
characterAnimator.SetFloat(speedParameterName, 0f);
characterAnimator.SetFloat(turnParameterName, 0f);
}
isDestinationValid = false;
pathCorners.Clear();
}
public void ResumeMovement()
{
if (navMeshAgent.isOnNavMesh)
{
navMeshAgent.isStopped = false;
}
}
public bool HasReachedDestination()
{
if (!isDestinationValid) return false;
float distanceToDestination = Vector3.Distance(transform.position, currentDestination);
return distanceToDestination <= stoppingDistance * 1.2f;
}
public float GetRemainingDistance()
{
if (!isDestinationValid || !navMeshAgent.hasPath) return float.MaxValue;
float remainingDistance = 0f;
for (int i = Mathf.Max(0, currentCornerIndex - 1); i < pathCorners.Count - 1; i++)
{
remainingDistance += Vector3.Distance(pathCorners[i], pathCorners[i + 1]);
}
remainingDistance += Vector3.Distance(transform.position, pathCorners[Mathf.Min(currentCornerIndex, pathCorners.Count - 1)]);
return remainingDistance;
}
public Vector3 GetCurrentDestination()
{
return currentDestination;
}
public bool IsDestinationValid()
{
return isDestinationValid;
}
public void SetMovementSpeed(float speedMultiplier)
{
if (navMeshAgent != null)
{
navMeshAgent.speed = baseSpeed * speedMultiplier;
}
}
}
在商业游戏项目中,运动控制层的实现还需要考虑性能优化和特殊情况处理。对于大量AI角色同时移动的场景,需要优化导航查询的频率和范围。可以使用空间分区技术将AI角色分组,只在必要时重新计算路径。此外,需要处理导航网格动态更新的情况,如可破坏环境或移动平台。
2.2.2 动画状态机的整合
运动控制层与动画系统的整合是创造逼真角色行为的关键。Unity的Animator Controller提供了强大的状态机功能,但需要精心设计才能与AI系统良好协作。以下是一个动画状态管理器的实现,它作为运动控制层和动画系统之间的桥梁。
using UnityEngine;
public class AIAnimationController : MonoBehaviour
{
private Animator animator;
private EnhancedNavigationController navigationController;
[Header("动画状态配置")]
[SerializeField] private string idleStateName = "Idle";
[SerializeField] private string walkStateName = "Walk";
[SerializeField] private string runStateName = "Run";
[SerializeField] private string combatIdleStateName = "CombatIdle";
[SerializeField] private string combatMoveStateName = "CombatMove";
[Header("动画参数")]
[SerializeField] private string speedParamName = "Speed";
[SerializeField] private string directionParamName = "Direction";
[SerializeField] private string isInCombatParamName = "IsInCombat";
[SerializeField] private string verticalVelocityParamName = "VerticalVelocity";
[Header("状态切换")]
[SerializeField] private float walkSpeedThreshold = 0.1f;
[SerializeField] private float runSpeedThreshold = 3.0f;
[SerializeField] private float combatTransitionTime = 0.2f;
private float currentSpeed;
private float targetSpeed;
private float speedSmoothVelocity;
private bool isInCombatMode;
private float combatModeTransition;
void Awake()
{
animator = GetComponent<Animator>();
if (animator == null)
{
animator = GetComponentInChildren<Animator>();
}
navigationController = GetComponent<EnhancedNavigationController>();
if (animator == null)
{
Debug.LogError("AIAnimationController需要Animator组件");
enabled = false;
}
}
void Update()
{
UpdateMovementAnimation();
UpdateCombatTransition();
UpdateSpecialAnimations();
}
private void UpdateMovementAnimation()
{
if (navigationController == null) return;
Vector3 velocity = Vector3.zero;
if (navigationController.IsDestinationValid())
{
NavMeshAgent agent = GetComponent<NavMeshAgent>();
if (agent != null)
{
velocity = agent.velocity;
}
}
float horizontalSpeed = new Vector3(velocity.x, 0, velocity.z).magnitude;
if (isInCombatMode)
{
targetSpeed = horizontalSpeed;
}
else
{
if (horizontalSpeed < walkSpeedThreshold)
{
targetSpeed = 0f;
}
else if (horizontalSpeed < runSpeedThreshold)
{
targetSpeed = 0.5f;
}
else
{
targetSpeed = 1f;
}
}
currentSpeed = Mathf.SmoothDamp(
currentSpeed,
targetSpeed,
ref speedSmoothVelocity,
0.1f
);
animator.SetFloat(speedParamName, currentSpeed);
if (horizontalSpeed > 0.1f)
{
Vector3 localVelocity = transform.InverseTransformDirection(velocity);
float direction = Mathf.Atan2(localVelocity.x, localVelocity.z) * Mathf.Rad2Deg / 180f;
animator.SetFloat(directionParamName, direction);
}
}
private void UpdateCombatTransition()
{
float targetCombatTransition = isInCombatMode ? 1f : 0f;
combatModeTransition = Mathf.MoveTowards(
combatModeTransition,
targetCombatTransition,
Time.deltaTime / combatTransitionTime
);
animator.SetFloat(isInCombatParamName, combatModeTransition);
if (combatModeTransition > 0.5f)
{
animator.SetLayerWeight(1, combatModeTransition);
}
}
private void UpdateSpecialAnimations()
{
if (navigationController == null) return;
bool isGrounded = true;
if (!isGrounded)
{
Rigidbody rb = GetComponent<Rigidbody>();
if (rb != null)
{
float verticalVelocity = rb.velocity.y;
animator.SetFloat(verticalVelocityParamName, verticalVelocity);
}
}
}
public void SetCombatMode(bool combatMode)
{
isInCombatMode = combatMode;
}
public void TriggerAnimation(string animationName)
{
if (animator != null)
{
animator.SetTrigger(animationName);
}
}
public void SetAnimationBool(string boolName, bool value)
{
if (animator != null)
{
animator.SetBool(boolName, value);
}
}
public float GetCurrentAnimationLength()
{
if (animator == null) return 0f;
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
return stateInfo.length;
}
public bool IsAnimationPlaying(string animationName)
{
if (animator == null) return false;
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
return stateInfo.IsName(animationName);
}
public void SetAnimationSpeed(float speedMultiplier)
{
if (animator != null)
{
animator.speed = speedMultiplier;
}
}
public void ResetAnimationTriggers()
{
if (animator == null) return;
AnimatorControllerParameter[] parameters = animator.parameters;
foreach (AnimatorControllerParameter param in parameters)
{
if (param.type == AnimatorControllerParameterType.Trigger)
{
animator.ResetTrigger(param.name);
}
}
}
}
在商业项目中,动画系统需要与游戏的其他系统深度整合。例如,当角色受伤时,动画系统需要播放受伤动画并可能中断当前动作。当角色死亡时,需要播放死亡动画并禁用运动控制。这些交互需要精心设计的状态转换逻辑和事件系统。
2.3 智能决策层的架构与实现
智能决策层是AI架构的核心,负责根据当前游戏状态选择适当的行为。这一层将高级目标分解为具体的动作序列,并处理行为之间的转换。决策层的设计质量直接决定了AI角色的智能水平和行为多样性。
从设计模式的角度看,决策层可以采用多种架构:有限状态机(FSM)、行为树(Behavior Tree)、效用系统(Utility System)或目标导向行为规划(GOAP)。每种架构都有其适用场景和优缺点。在商业游戏项目中,通常会根据具体需求选择或组合使用这些架构。
2.3.1 行为树决策系统的实现
行为树是一种流行的决策层架构,它通过树形结构组织行为节点,提供良好的可读性和可维护性。以下是行为树系统的核心实现,包括基本节点类型和行为树执行器。
using System.Collections.Generic;
using UnityEngine;
namespace AIDecisionSystem
{
public enum NodeStatus
{
Success,
Failure,
Running
}
public abstract class BehaviorNode
{
protected string nodeName;
protected NodeStatus currentStatus;
public BehaviorNode(string name)
{
nodeName = name;
currentStatus = NodeStatus.Failure;
}
public virtual NodeStatus Evaluate()
{
return NodeStatus.Failure;
}
public virtual void Reset()
{
currentStatus = NodeStatus.Failure;
}
public string GetNodeName()
{
return nodeName;
}
public NodeStatus GetCurrentStatus()
{
return currentStatus;
}
}
public class SequenceNode : BehaviorNode
{
private List<BehaviorNode> children;
private int currentChildIndex;
public SequenceNode(string name) : base(name)
{
children = new List<BehaviorNode>();
currentChildIndex = 0;
}
public void AddChild(BehaviorNode child)
{
children.Add(child);
}
public override NodeStatus Evaluate()
{
if (children.Count == 0)
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
while (currentChildIndex < children.Count)
{
NodeStatus childStatus = children[currentChildIndex].Evaluate();
if (childStatus == NodeStatus.Running)
{
currentStatus = NodeStatus.Running;
return currentStatus;
}
else if (childStatus == NodeStatus.Failure)
{
currentStatus = NodeStatus.Failure;
currentChildIndex = 0;
return currentStatus;
}
currentChildIndex++;
}
currentStatus = NodeStatus.Success;
currentChildIndex = 0;
return currentStatus;
}
public override void Reset()
{
base.Reset();
currentChildIndex = 0;
foreach (BehaviorNode child in children)
{
child.Reset();
}
}
}
public class SelectorNode : BehaviorNode
{
private List<BehaviorNode> children;
private int currentChildIndex;
public SelectorNode(string name) : base(name)
{
children = new List<BehaviorNode>();
currentChildIndex = 0;
}
public void AddChild(BehaviorNode child)
{
children.Add(child);
}
public override NodeStatus Evaluate()
{
if (children.Count == 0)
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
while (currentChildIndex < children.Count)
{
NodeStatus childStatus = children[currentChildIndex].Evaluate();
if (childStatus == NodeStatus.Running)
{
currentStatus = NodeStatus.Running;
return currentStatus;
}
else if (childStatus == NodeStatus.Success)
{
currentStatus = NodeStatus.Success;
currentChildIndex = 0;
return currentStatus;
}
currentChildIndex++;
}
currentStatus = NodeStatus.Failure;
currentChildIndex = 0;
return currentStatus;
}
public override void Reset()
{
base.Reset();
currentChildIndex = 0;
foreach (BehaviorNode child in children)
{
child.Reset();
}
}
}
public class ParallelNode : BehaviorNode
{
private List<BehaviorNode> children;
private int requiredSuccessCount;
private int requiredFailureCount;
public ParallelNode(string name, int successCount = 1, int failureCount = 1) : base(name)
{
children = new List<BehaviorNode>();
requiredSuccessCount = successCount;
requiredFailureCount = failureCount;
}
public void AddChild(BehaviorNode child)
{
children.Add(child);
}
public override NodeStatus Evaluate()
{
if (children.Count == 0)
{
currentStatus = NodeStatus.Failure;
return currentStatus;
}
int successCount = 0;
int failureCount = 0;
int runningCount = 0;
foreach (BehaviorNode child in children)
{
NodeStatus childStatus = child.Evaluate();
if (childStatus == NodeStatus.Success)
{
successCount++;
}
else if (childStatus == NodeStatus.Failure)
{
failureCount++;
}
else if (childStatus == NodeStatus.Running)
{
runningCount++;
}
}
if (successCount >= requiredSuccessCount)
{
currentStatus = NodeStatus.Success;
}
else if (failureCount >= requiredFailureCount)
{
currentStatus = NodeStatus.Failure;
}
else
{
currentStatus = NodeStatus.Running;
}
return currentStatus;
}
public override void Reset()
{
base.Reset();
foreach (BehaviorNode child in children)
{
child.Reset();
}
}
}
public class ConditionNode : BehaviorNode
{
public delegate bool ConditionDelegate();
private ConditionDelegate conditionCheck;
public ConditionNode(string name, ConditionDelegate condition) : base(name)
{
conditionCheck = condition;
}
public override NodeStatus Evaluate()
{
if (conditionCheck != null && conditionCheck())
{
currentStatus = NodeStatus.Success;
}
else
{
currentStatus = NodeStatus.Failure;
}
return currentStatus;
}
}
public class ActionNode : BehaviorNode
{
public delegate NodeStatus ActionDelegate();
private ActionDelegate action;
public ActionNode(string name, ActionDelegate actionDelegate) : base(name)
{
action = actionDelegate;
}
public override NodeStatus Evaluate()
{
if (action != null)
{
currentStatus = action();
}
else
{
currentStatus = NodeStatus.Failure;
}
return currentStatus;
}
}
public class BehaviorTree
{
private BehaviorNode rootNode;
private float updateInterval;
private float updateTimer;
private bool isRunning;
public BehaviorTree(BehaviorNode root, float interval = 0.1f)
{
rootNode = root;
updateInterval = interval;
updateTimer = 0f;
isRunning = false;
}
public void Start()
{
isRunning = true;
updateTimer = 0f;
}
public void Stop()
{
isRunning = false;
if (rootNode != null)
{
rootNode.Reset();
}
}
public void Update(float deltaTime)
{
if (!isRunning || rootNode == null) return;
updateTimer += deltaTime;
if (updateTimer >= updateInterval)
{
rootNode.Evaluate();
updateTimer = 0f;
}
}
public NodeStatus GetCurrentStatus()
{
if (rootNode != null)
{
return rootNode.GetCurrentStatus();
}
return NodeStatus.Failure;
}
public void SetRootNode(BehaviorNode newRoot)
{
if (rootNode != null)
{
rootNode.Reset();
}
rootNode = newRoot;
}
}
}
2.3.2 基于行为树的敌人AI实现
以下是基于行为树系统的完整敌人AI实现,展示了如何将行为树应用于实际游戏场景。
using UnityEngine;
using AIDecisionSystem;
public class EnemyAI : MonoBehaviour
{
private BehaviorTree behaviorTree;
private EnhancedNavigationController navigationController;
private AIAnimationController animationController;
private Transform playerTransform;
[Header("感知参数")]
[SerializeField] private float sightRange = 20f;
[SerializeField] private float sightAngle = 90f;
[SerializeField] private float hearingRange = 10f;
[SerializeField] private float attackRange = 2f;
[Header("战斗参数")]
[SerializeField] private float patrolSpeed = 2f;
[SerializeField] private float chaseSpeed = 5f;
[SerializeField] private float attackCooldown = 2f;
[SerializeField] private float maxHealth = 100f;
[Header("巡逻参数")]
[SerializeField] private Transform[] patrolPoints;
[SerializeField] private float waitTimeAtPoint = 3f;
private float currentHealth;
private float attackTimer;
private int currentPatrolIndex;
private float waitTimer;
private AIState currentState;
private enum AIState
{
Idle,
Patrol,
Chase,
Attack,
Flee,
Dead
}
void Start()
{
InitializeComponents();
InitializeBehaviorTree();
currentHealth = maxHealth;
currentState = AIState.Idle;
}
void Update()
{
if (behaviorTree != null && currentState != AIState.Dead)
{
behaviorTree.Update(Time.deltaTime);
}
UpdateTimers();
}
private void InitializeComponents()
{
navigationController = GetComponent<EnhancedNavigationController>();
animationController = GetComponent<AIAnimationController>();
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
}
if (navigationController == null)
{
Debug.LogError("EnemyAI需要EnhancedNavigationController组件");
enabled = false;
}
}
private void InitializeBehaviorTree()
{
BehaviorTreeBuilder builder = new BehaviorTreeBuilder();
SelectorNode rootSelector = new SelectorNode("RootSelector");
SequenceNode deathSequence = new SequenceNode("DeathSequence");
deathSequence.AddChild(new ConditionNode("IsDead", CheckIfDead));
deathSequence.AddChild(new ActionNode("Die", Die));
SequenceNode fleeSequence = new SequenceNode("FleeSequence");
fleeSequence.AddChild(new ConditionNode("ShouldFlee", CheckShouldFlee));
fleeSequence.AddChild(new ActionNode("Flee", Flee));
SequenceNode attackSequence = new SequenceNode("AttackSequence");
attackSequence.AddChild(new ConditionNode("CanAttack", CheckCanAttack));
attackSequence.AddChild(new ActionNode("Attack", Attack));
SequenceNode chaseSequence = new SequenceNode("ChaseSequence");
chaseSequence.AddChild(new ConditionNode("CanSeePlayer", CheckCanSeePlayer));
chaseSequence.AddChild(new ActionNode("Chase", ChasePlayer));
SequenceNode patrolSequence = new SequenceNode("PatrolSequence");
patrolSequence.AddChild(new ConditionNode("ShouldPatrol", CheckShouldPatrol));
patrolSequence.AddChild(new ActionNode("Patrol", Patrol));
SequenceNode idleSequence = new SequenceNode("IdleSequence");
idleSequence.AddChild(new ActionNode("Idle", Idle));
rootSelector.AddChild(deathSequence);
rootSelector.AddChild(fleeSequence);
rootSelector.AddChild(attackSequence);
rootSelector.AddChild(chaseSequence);
rootSelector.AddChild(patrolSequence);
rootSelector.AddChild(idleSequence);
behaviorTree = new BehaviorTree(rootSelector, 0.1f);
behaviorTree.Start();
}
private NodeStatus CheckIfDead()
{
return currentHealth <= 0 ? NodeStatus.Success : NodeStatus.Failure;
}
private NodeStatus Die()
{
currentState = AIState.Dead;
navigationController.StopMovement();
if (animationController != null)
{
animationController.TriggerAnimation("Die");
}
Collider collider = GetComponent<Collider>();
if (collider != null)
{
collider.enabled = false;
}
enabled = false;
return NodeStatus.Success;
}
private NodeStatus CheckShouldFlee()
{
if (currentHealth < maxHealth * 0.3f && CheckCanSeePlayer() == NodeStatus.Success)
{
return NodeStatus.Success;
}
return NodeStatus.Failure;
}
private NodeStatus Flee()
{
currentState = AIState.Flee;
if (playerTransform == null) return NodeStatus.Failure;
Vector3 awayFromPlayer = transform.position - playerTransform.position;
awayFromPlayer.y = 0;
awayFromPlayer.Normalize();
Vector3 fleePosition = transform.position + awayFromPlayer * 15f;
if (navigationController.SetDestination(fleePosition))
{
navigationController.SetMovementSpeed(1.2f);
if (animationController != null)
{
animationController.SetCombatMode(true);
}
return NodeStatus.Running;
}
return NodeStatus.Failure;
}
private NodeStatus CheckCanAttack()
{
if (playerTransform == null) return NodeStatus.Failure;
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
if (distanceToPlayer <= attackRange && attackTimer <= 0)
{
if (CheckCanSeePlayer() == NodeStatus.Success)
{
return NodeStatus.Success;
}
}
return NodeStatus.Failure;
}
private NodeStatus Attack()
{
currentState = AIState.Attack;
navigationController.StopMovement();
if (animationController != null)
{
animationController.SetCombatMode(true);
animationController.TriggerAnimation("Attack");
}
attackTimer = attackCooldown;
LookAtPlayer();
return NodeStatus.Success;
}
private NodeStatus CheckCanSeePlayer()
{
if (playerTransform == null) return NodeStatus.Failure;
Vector3 directionToPlayer = playerTransform.position - transform.position;
float distanceToPlayer = directionToPlayer.magnitude;
if (distanceToPlayer > sightRange) return NodeStatus.Failure;
float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
if (angleToPlayer > sightAngle / 2) return NodeStatus.Failure;
RaycastHit hit;
if (Physics.Raycast(transform.position + Vector3.up * 1.5f,
directionToPlayer.normalized, out hit, sightRange))
{
if (hit.transform == playerTransform)
{
return NodeStatus.Success;
}
}
if (distanceToPlayer <= hearingRange)
{
return NodeStatus.Success;
}
return NodeStatus.Failure;
}
private NodeStatus ChasePlayer()
{
currentState = AIState.Chase;
if (playerTransform == null) return NodeStatus.Failure;
if (navigationController.SetDestination(playerTransform.position))
{
navigationController.SetMovementSpeed(chaseSpeed / navigationController.baseSpeed);
if (animationController != null)
{
animationController.SetCombatMode(true);
}
LookAtPlayer();
return NodeStatus.Running;
}
return NodeStatus.Failure;
}
private NodeStatus CheckShouldPatrol()
{
if (patrolPoints != null && patrolPoints.Length > 0)
{
if (currentState != AIState.Patrol && currentState != AIState.Chase &&
currentState != AIState.Attack && currentState != AIState.Flee)
{
return NodeStatus.Success;
}
if (currentState == AIState.Patrol && !navigationController.IsDestinationValid())
{
return NodeStatus.Success;
}
}
return NodeStatus.Failure;
}
private NodeStatus Patrol()
{
currentState = AIState.Patrol;
if (patrolPoints == null || patrolPoints.Length == 0)
{
return NodeStatus.Failure;
}
if (!navigationController.IsDestinationValid() || navigationController.HasReachedDestination())
{
waitTimer += Time.deltaTime;
if (waitTimer >= waitTimeAtPoint)
{
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
if (navigationController.SetDestination(patrolPoints[currentPatrolIndex].position))
{
navigationController.SetMovementSpeed(patrolSpeed / navigationController.baseSpeed);
if (animationController != null)
{
animationController.SetCombatMode(false);
}
waitTimer = 0f;
return NodeStatus.Running;
}
}
else
{
return NodeStatus.Running;
}
}
return NodeStatus.Running;
}
private NodeStatus Idle()
{
currentState = AIState.Idle;
if (animationController != null)
{
animationController.SetCombatMode(false);
}
return NodeStatus.Success;
}
private void LookAtPlayer()
{
if (playerTransform == null) return;
Vector3 lookDirection = playerTransform.position - transform.position;
lookDirection.y = 0;
if (lookDirection.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
Time.deltaTime * 5f
);
}
}
private void UpdateTimers()
{
if (attackTimer > 0)
{
attackTimer -= Time.deltaTime;
}
}
public void TakeDamage(float damage)
{
if (currentState == AIState.Dead) return;
currentHealth -= damage;
if (animationController != null)
{
animationController.TriggerAnimation("TakeDamage");
}
if (currentHealth <= 0)
{
currentHealth = 0;
}
}
public float GetHealthPercentage()
{
return currentHealth / maxHealth;
}
public AIState GetCurrentState()
{
return currentState;
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, sightRange);
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, hearingRange);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRange);
Vector3 leftBoundary = Quaternion.Euler(0, -sightAngle / 2, 0) * transform.forward * sightRange;
Vector3 rightBoundary = Quaternion.Euler(0, sightAngle / 2, 0) * transform.forward * sightRange;
Gizmos.color = Color.green;
Gizmos.DrawRay(transform.position, leftBoundary);
Gizmos.DrawRay(transform.position, rightBoundary);
Gizmos.DrawLine(transform.position + leftBoundary, transform.position + rightBoundary);
}
}
public class BehaviorTreeBuilder
{
public BehaviorTreeBuilder()
{
}
}
在商业游戏项目中,决策层的实现需要考虑更多的复杂性和可扩展性。例如,可能需要支持行为树的动态加载和热重载,以便游戏设计师可以在不重启游戏的情况下调整AI行为。此外,决策层需要与游戏的其他系统(如任务系统、对话系统、经济系统)进行深度集成,以创建更连贯的游戏体验。
2.4 战略规划层的设计与集成
战略规划层是AI架构的最高层,负责制定长期目标和全局策略。这一层考虑的因素包括团队协作、资源管理、风险评估和机会识别。在多人游戏或大规模战斗中,战略规划层的作用尤为关键,它能够协调多个AI单位的行为,实现复杂的战术配合。
从架构设计角度看,战略规划层需要处理的信息比决策层更加抽象和宏观。它不关心具体的移动路径或攻击时机,而是关注整体局势、目标优先级和资源分配。在Unity中实现战略规划层时,通常需要建立世界状态模型、目标评估系统和规划算法。
2.4.1 团队AI协调系统的实现
以下是一个团队AI协调系统的实现,展示了如何实现AI单位之间的协作和战术配合。
using System.Collections.Generic;
using UnityEngine;
public class TeamAICoordinator : MonoBehaviour
{
[System.Serializable]
public class TeamMember
{
public GameObject memberObject;
public EnemyAI enemyAI;
public EnhancedNavigationController navigation;
public float combatEffectiveness;
public Vector3 lastKnownPosition;
public TeamMember(GameObject obj)
{
memberObject = obj;
enemyAI = obj.GetComponent<EnemyAI>();
navigation = obj.GetComponent<EnhancedNavigationController>();
combatEffectiveness = 1.0f;
lastKnownPosition = obj.transform.position;
}
}
[Header("团队配置")]
[SerializeField] private string teamTag = "EnemyTeam";
[SerializeField] private float coordinationUpdateInterval = 1.0f;
[SerializeField] private float communicationRange = 30f;
[Header("战术选项")]
[SerializeField] private bool enableFlanking = true;
[SerializeField] private bool enableCoverUsage = true;
[SerializeField] private bool enableTeamFormation = true;
[Header("阵型配置")]
[SerializeField] private FormationType defaultFormation = FormationType.Line;
[SerializeField] private float formationSpacing = 2.0f;
private List<TeamMember> teamMembers;
private List<GameObject> knownEnemies;
private Vector3 teamCenter;
private Vector3 teamForward;
private float coordinationTimer;
private FormationType currentFormation;
private enum FormationType
{
Line,
Wedge,
Column,
Skirmish
}
void Start()
{
InitializeTeam();
knownEnemies = new List<GameObject>();
coordinationTimer = 0f;
currentFormation = defaultFormation;
}
void Update()
{
coordinationTimer += Time.deltaTime;
if (coordinationTimer >= coordinationUpdateInterval)
{
UpdateTeamCoordination();
coordinationTimer = 0f;
}
}
private void InitializeTeam()
{
teamMembers = new List<TeamMember>();
GameObject[] teamObjects = GameObject.FindGameObjectsWithTag(teamTag);
foreach (GameObject obj in teamObjects)
{
if (obj != gameObject)
{
TeamMember member = new TeamMember(obj);
teamMembers.Add(member);
}
}
UpdateTeamCenter();
Debug.Log($"团队初始化完成,成员数量: {teamMembers.Count}");
}
private void UpdateTeamCoordination()
{
UpdateTeamCenter();
UpdateKnownEnemies();
DistributeRoles();
CoordinateFormation();
ShareInformation();
}
private void UpdateTeamCenter()
{
if (teamMembers.Count == 0) return;
Vector3 centerSum = Vector3.zero;
foreach (TeamMember member in teamMembers)
{
if (member.memberObject != null)
{
centerSum += member.memberObject.transform.position;
}
}
teamCenter = centerSum / teamMembers.Count;
Vector3 forwardSum = Vector3.zero;
foreach (TeamMember member in teamMembers)
{
if (member.memberObject != null)
{
forwardSum += member.memberObject.transform.forward;
}
}
teamForward = forwardSum.normalized;
}
private void UpdateKnownEnemies()
{
knownEnemies.Clear();
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
knownEnemies.Add(player);
}
foreach (TeamMember member in teamMembers)
{
if (member.enemyAI != null)
{
AIState memberState = member.enemyAI.GetCurrentState();
if (memberState == AIState.Chase || memberState == AIState.Attack)
{
Vector3 enemyPosition = member.enemyAI.transform.position +
member.enemyAI.transform.forward * 5f;
if (!knownEnemies.Contains(player))
{
knownEnemies.Add(player);
}
}
}
}
}
private void DistributeRoles()
{
if (knownEnemies.Count == 0) return;
Vector3 primaryEnemyPosition = knownEnemies[0].transform.position;
List<TeamMember> availableMembers = new List<TeamMember>();
foreach (TeamMember member in teamMembers)
{
if (member.memberObject != null && member.enemyAI != null)
{
AIState currentState = member.enemyAI.GetCurrentState();
if (currentState != AIState.Dead && currentState != AIState.Flee)
{
availableMembers.Add(member);
}
}
}
if (availableMembers.Count == 0) return;
if (enableFlanking && availableMembers.Count >= 3)
{
int flankersCount = Mathf.Min(2, availableMembers.Count / 2);
for (int i = 0; i < flankersCount; i++)
{
TeamMember flanker = availableMembers[i];
Vector3 flankDirection = Vector3.Cross(
(primaryEnemyPosition - teamCenter).normalized,
Vector3.up
).normalized * (i % 2 == 0 ? 1 : -1);
Vector3 flankPosition = primaryEnemyPosition + flankDirection * 10f;
if (flanker.navigation != null)
{
flanker.navigation.SetDestination(flankPosition);
flanker.navigation.SetMovementSpeed(1.0f);
}
}
}
if (enableCoverUsage && availableMembers.Count >= 2)
{
for (int i = 0; i < Mathf.Min(availableMembers.Count, 2); i++)
{
TeamMember member = availableMembers[i];
Vector3 coverPosition = FindCoverPosition(
member.memberObject.transform.position,
primaryEnemyPosition
);
if (coverPosition != Vector3.zero && member.navigation != null)
{
member.navigation.SetDestination(coverPosition);
}
}
}
}
private void CoordinateFormation()
{
if (!enableTeamFormation || teamMembers.Count < 2) return;
List<TeamMember> availableMembers = new List<TeamMember>();
foreach (TeamMember member in teamMembers)
{
if (member.memberObject != null && member.enemyAI != null)
{
AIState currentState = member.enemyAI.GetCurrentState();
if (currentState == AIState.Patrol || currentState == AIState.Idle)
{
availableMembers.Add(member);
}
}
}
if (availableMembers.Count < 2) return;
Vector3 formationCenter = teamCenter;
Vector3 formationForward = teamForward;
if (knownEnemies.Count > 0)
{
Vector3 toEnemy = knownEnemies[0].transform.position - formationCenter;
formationForward = toEnemy.normalized;
}
List<Vector3> formationPositions = CalculateFormationPositions(
availableMembers.Count,
formationCenter,
formationForward
);
for (int i = 0; i < availableMembers.Count; i++)
{
if (i < formationPositions.Count)
{
TeamMember member = availableMembers[i];
if (member.navigation != null)
{
member.navigation.SetDestination(formationPositions[i]);
}
}
}
}
private List<Vector3> CalculateFormationPositions(int memberCount, Vector3 center, Vector3 forward)
{
List<Vector3> positions = new List<Vector3>();
switch (currentFormation)
{
case FormationType.Line:
CalculateLineFormation(memberCount, center, forward, positions);
break;
case FormationType.Wedge:
CalculateWedgeFormation(memberCount, center, forward, positions);
break;
case FormationType.Column:
CalculateColumnFormation(memberCount, center, forward, positions);
break;
case FormationType.Skirmish:
CalculateSkirmishFormation(memberCount, center, forward, positions);
break;
}
return positions;
}
private void CalculateLineFormation(int memberCount, Vector3 center, Vector3 forward, List<Vector3> positions)
{
Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
for (int i = 0; i < memberCount; i++)
{
float offset = (i - (memberCount - 1) / 2.0f) * formationSpacing;
Vector3 position = center + right * offset;
positions.Add(position);
}
}
private void CalculateWedgeFormation(int memberCount, Vector3 center, Vector3 forward, List<Vector3> positions)
{
Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
int rows = Mathf.CeilToInt(Mathf.Sqrt(memberCount));
int currentMember = 0;
for (int row = 0; row < rows; row++)
{
int membersInRow = Mathf.Min(row + 1, memberCount - currentMember);
float rowDepth = row * formationSpacing * 0.8f;
for (int col = 0; col < membersInRow; col++)
{
float colOffset = (col - (membersInRow - 1) / 2.0f) * formationSpacing;
Vector3 position = center - forward * rowDepth + right * colOffset;
positions.Add(position);
currentMember++;
if (currentMember >= memberCount) break;
}
if (currentMember >= memberCount) break;
}
}
private void CalculateColumnFormation(int memberCount, Vector3 center, Vector3 forward, List<Vector3> positions)
{
for (int i = 0; i < memberCount; i++)
{
float offset = i * formationSpacing * 0.7f;
Vector3 position = center - forward * offset;
positions.Add(position);
}
}
private void CalculateSkirmishFormation(int memberCount, Vector3 center, Vector3 forward, List<Vector3> positions)
{
Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
for (int i = 0; i < memberCount; i++)
{
float angle = (i * 360f / memberCount) * Mathf.Deg2Rad;
float radius = formationSpacing * Mathf.Sqrt(memberCount) * 0.5f;
Vector3 offset = new Vector3(
Mathf.Cos(angle) * radius,
0,
Mathf.Sin(angle) * radius
);
Vector3 position = center + offset;
positions.Add(position);
}
}
private Vector3 FindCoverPosition(Vector3 fromPosition, Vector3 threatPosition)
{
Vector3 directionToThreat = (threatPosition - fromPosition).normalized;
float[] angles = { -45f, 45f, -90f, 90f, -135f, 135f };
foreach (float angle in angles)
{
Vector3 testDirection = Quaternion.Euler(0, angle, 0) * -directionToThreat;
RaycastHit hit;
if (Physics.Raycast(fromPosition, testDirection, out hit, 10f))
{
if (hit.collider.CompareTag("Cover") || hit.collider.CompareTag("Wall"))
{
Vector3 coverPosition = hit.point - testDirection * 1f;
NavMeshHit navHit;
if (NavMesh.SamplePosition(coverPosition, out navHit, 2f, NavMesh.AllAreas))
{
return navHit.position;
}
}
}
}
return Vector3.zero;
}
private void ShareInformation()
{
foreach (TeamMember sender in teamMembers)
{
if (sender.memberObject == null) continue;
foreach (TeamMember receiver in teamMembers)
{
if (receiver == sender || receiver.memberObject == null) continue;
float distance = Vector3.Distance(
sender.memberObject.transform.position,
receiver.memberObject.transform.position
);
if (distance <= communicationRange)
{
if (sender.enemyAI != null && receiver.enemyAI != null)
{
AIState senderState = sender.enemyAI.GetCurrentState();
if (senderState == AIState.Chase || senderState == AIState.Attack)
{
receiver.enemyAI.TakeDamage(0);
}
}
}
}
}
}
public void ChangeFormation(FormationType newFormation)
{
currentFormation = newFormation;
}
public void AddTeamMember(GameObject newMember)
{
TeamMember member = new TeamMember(newMember);
teamMembers.Add(member);
}
public void RemoveTeamMember(GameObject memberToRemove)
{
teamMembers.RemoveAll(member => member.memberObject == memberToRemove);
}
public int GetTeamSize()
{
return teamMembers.Count;
}
public Vector3 GetTeamCenter()
{
return teamCenter;
}
void OnDrawGizmos()
{
if (teamMembers != null && teamMembers.Count > 0)
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(teamCenter, 1f);
Gizmos.color = Color.blue;
Gizmos.DrawRay(teamCenter, teamForward * 3f);
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, communicationRange);
}
}
}
在商业游戏项目中,战略规划层需要更加复杂和精细的设计。可能需要实现基于效用的决策系统,为不同的战略选项分配分数,然后选择总分最高的策略。还需要考虑资源管理,如弹药、生命值和特殊能力的使用时机。此外,战略规划层可能需要与游戏的故事系统集成,根据剧情发展调整AI的战略行为。
2.5 AI架构支撑系统的实现
一个完整的AI架构不仅需要核心的层次结构,还需要一系列支撑系统来提供必要的服务和功能。这些支撑系统包括感知系统、记忆系统、通信系统和调试系统。它们为AI层次提供数据输入、状态存储、协调机制和开发工具。
2.5.1 感知系统的实现
感知系统负责收集和处理环境信息,为决策层和战略层提供输入。以下是一个综合感知系统的实现。
using System.Collections.Generic;
using UnityEngine;
public class AIPerceptionSystem : MonoBehaviour
{
[System.Serializable]
public class VisualStimulus
{
public GameObject source;
public Vector3 position;
public float intensity;
public float timestamp;
public StimulusType type;
public enum StimulusType
{
Visual,
Auditory,
Damage,
Other
}
public VisualStimulus(GameObject src, Vector3 pos, float inten, StimulusType stimType)
{
source = src;
position = pos;
intensity = inten;
timestamp = Time.time;
type = stimType;
}
}
[Header("视觉感知")]
[SerializeField] private float sightRange = 20f;
[SerializeField] private float peripheralVisionAngle = 180f;
[SerializeField] private float focusedVisionAngle = 60f;
[SerializeField] private LayerMask visionBlockingLayers = ~0;
[SerializeField] private LayerMask targetLayers = ~0;
[Header("听觉感知")]
[SerializeField] private float hearingRange = 15f;
[SerializeField] private float hearingSensitivity = 1.0f;
[Header("记忆系统")]
[SerializeField] private float stimulusMemoryDuration = 10f;
[SerializeField] private int maxStimuliMemory = 20;
private List<VisualStimulus> rememberedStimuli;
private List<GameObject> currentlyVisibleTargets;
private float visionCheckTimer;
private const float visionCheckInterval = 0.2f;
void Awake()
{
rememberedStimuli = new List<VisualStimulus>();
currentlyVisibleTargets = new List<GameObject>();
}
void Update()
{
UpdatePerception();
UpdateMemory();
}
private void UpdatePerception()
{
visionCheckTimer += Time.deltaTime;
if (visionCheckTimer >= visionCheckInterval)
{
UpdateVision();
visionCheckTimer = 0f;
}
UpdateHearing();
}
private void UpdateVision()
{
currentlyVisibleTargets.Clear();
Collider[] potentialTargets = Physics.OverlapSphere(
transform.position,
sightRange,
targetLayers
);
foreach (Collider targetCollider in potentialTargets)
{
GameObject target = targetCollider.gameObject;
if (target == gameObject) continue;
if (IsTargetVisible(target))
{
currentlyVisibleTargets.Add(target);
VisualStimulus stimulus = new VisualStimulus(
target,
target.transform.position,
1.0f,
VisualStimulus.StimulusType.Visual
);
RememberStimulus(stimulus);
}
}
}
private bool IsTargetVisible(GameObject target)
{
Vector3 directionToTarget = target.transform.position - transform.position;
float distanceToTarget = directionToTarget.magnitude;
if (distanceToTarget > sightRange) return false;
float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
float visionAngle = peripheralVisionAngle;
if (angleToTarget < focusedVisionAngle / 2)
{
visionAngle = focusedVisionAngle;
}
if (angleToTarget > visionAngle / 2) return false;
RaycastHit hit;
if (Physics.Raycast(
transform.position + Vector3.up * 1.5f,
directionToTarget.normalized,
out hit,
distanceToTarget,
visionBlockingLayers
))
{
if (hit.collider.gameObject != target)
{
return false;
}
}
return true;
}
private void UpdateHearing()
{
Collider[] soundSources = Physics.OverlapSphere(
transform.position,
hearingRange
);
foreach (Collider sourceCollider in soundSources)
{
AudioSource audioSource = sourceCollider.GetComponent<AudioSource>();
if (audioSource != null && audioSource.isPlaying)
{
float distance = Vector3.Distance(
transform.position,
sourceCollider.transform.position
);
float volume = audioSource.volume * (1 - distance / hearingRange);
if (volume > 0.1f * hearingSensitivity)
{
VisualStimulus stimulus = new VisualStimulus(
sourceCollider.gameObject,
sourceCollider.transform.position,
volume,
VisualStimulus.StimulusType.Auditory
);
RememberStimulus(stimulus);
}
}
}
}
public void RegisterDamageStimulus(GameObject damageSource, Vector3 damagePosition, float damageAmount)
{
VisualStimulus stimulus = new VisualStimulus(
damageSource,
damagePosition,
damageAmount,
VisualStimulus.StimulusType.Damage
);
RememberStimulus(stimulus);
}
private void RememberStimulus(VisualStimulus stimulus)
{
rememberedStimuli.Add(stimulus);
while (rememberedStimuli.Count > maxStimuliMemory)
{
rememberedStimuli.RemoveAt(0);
}
}
private void UpdateMemory()
{
for (int i = rememberedStimuli.Count - 1; i >= 0; i--)
{
if (Time.time - rememberedStimuli[i].timestamp > stimulusMemoryDuration)
{
rememberedStimuli.RemoveAt(i);
}
}
}
public List<GameObject> GetVisibleTargets()
{
return new List<GameObject>(currentlyVisibleTargets);
}
public List<VisualStimulus> GetRememberedStimuli()
{
return new List<VisualStimulus>(rememberedStimuli);
}
public VisualStimulus GetLatestStimulusOfType(VisualStimulus.StimulusType type)
{
for (int i = rememberedStimuli.Count - 1; i >= 0; i--)
{
if (rememberedStimuli[i].type == type)
{
return rememberedStimuli[i];
}
}
return null;
}
public bool CanSeeTarget(GameObject target)
{
return currentlyVisibleTargets.Contains(target);
}
public Vector3 GetLastKnownPosition(GameObject target)
{
for (int i = rememberedStimuli.Count - 1; i >= 0; i--)
{
if (rememberedStimuli[i].source == target)
{
return rememberedStimuli[i].position;
}
}
return Vector3.zero;
}
public void SetSightRange(float newRange)
{
sightRange = newRange;
}
public void SetHearingSensitivity(float sensitivity)
{
hearingSensitivity = sensitivity;
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(transform.position, sightRange);
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, hearingRange);
Vector3 leftPeripheral = Quaternion.Euler(0, -peripheralVisionAngle / 2, 0) *
transform.forward * sightRange;
Vector3 rightPeripheral = Quaternion.Euler(0, peripheralVisionAngle / 2, 0) *
transform.forward * sightRange;
Gizmos.color = Color.blue;
Gizmos.DrawRay(transform.position, leftPeripheral);
Gizmos.DrawRay(transform.position, rightPeripheral);
Vector3 leftFocused = Quaternion.Euler(0, -focusedVisionAngle / 2, 0) *
transform.forward * sightRange;
Vector3 rightFocused = Quaternion.Euler(0, focusedVisionAngle / 2, 0) *
transform.forward * sightRange;
Gizmos.color = Color.red;
Gizmos.DrawRay(transform.position, leftFocused);
Gizmos.DrawRay(transform.position, rightFocused);
}
}
在商业游戏项目中,感知系统需要更加精细和高效。可能需要实现视锥体剔除、空间分区查询优化、感知衰减和注意力机制。此外,感知系统可能需要支持不同的感知配置文件,以便不同类型的AI角色具有不同的感知能力。例如,侦察兵可能具有更远的视觉范围,而守卫可能具有更好的听觉感知。
2.5.2 AI调试与可视化系统
调试系统对于AI开发至关重要,它帮助开发者理解AI的内部状态和行为决策过程。以下是一个AI调试可视化系统的实现。
using System.Collections.Generic;
using UnityEngine;
public class AIDebugVisualizer : MonoBehaviour
{
[System.Serializable]
public class DebugVisual
{
public string label;
public Vector3 position;
public Color color;
public float size;
public float duration;
public float timestamp;
public DebugVisual(string lbl, Vector3 pos, Color col, float sz, float dur)
{
label = lbl;
position = pos;
color = col;
size = sz;
duration = dur;
timestamp = Time.time;
}
}
[Header("调试选项")]
[SerializeField] private bool enableDebugVisualization = true;
[SerializeField] private float defaultVisualDuration = 2.0f;
[SerializeField] private float textHeightOffset = 2.0f;
[Header("颜色配置")]
[SerializeField] private Color pathColor = Color.blue;
[SerializeField] private Color targetColor = Color.red;
[SerializeField] private Color decisionColor = Color.green;
[SerializeField] private Color perceptionColor = Color.yellow;
private List<DebugVisual> debugVisuals;
private EnhancedNavigationController navigationController;
private EnemyAI enemyAI;
private AIPerceptionSystem perceptionSystem;
void Awake()
{
debugVisuals = new List<DebugVisual>();
navigationController = GetComponent<EnhancedNavigationController>();
enemyAI = GetComponent<EnemyAI>();
perceptionSystem = GetComponent<AIPerceptionSystem>();
}
void Update()
{
UpdateDebugVisuals();
}
void OnDrawGizmos()
{
if (!enableDebugVisualization) return;
DrawCurrentDebugVisuals();
DrawAIStateInfo();
}
private void UpdateDebugVisuals()
{
for (int i = debugVisuals.Count - 1; i >= 0; i--)
{
if (Time.time - debugVisuals[i].timestamp > debugVisuals[i].duration)
{
debugVisuals.RemoveAt(i);
}
}
}
private void DrawCurrentDebugVisuals()
{
foreach (DebugVisual visual in debugVisuals)
{
Gizmos.color = visual.color;
Gizmos.DrawSphere(visual.position, visual.size);
#if UNITY_EDITOR
UnityEditor.Handles.color = visual.color;
UnityEditor.Handles.Label(
visual.position + Vector3.up * textHeightOffset,
visual.label
);
#endif
}
}
private void DrawAIStateInfo()
{
if (navigationController != null && navigationController.IsDestinationValid())
{
Gizmos.color = pathColor;
Vector3 currentPosition = transform.position;
Vector3 destination = navigationController.GetCurrentDestination();
Gizmos.DrawLine(currentPosition, destination);
Gizmos.DrawWireSphere(destination, 0.5f);
#if UNITY_EDITOR
UnityEditor.Handles.color = pathColor;
UnityEditor.Handles.Label(
destination + Vector3.up * 1.5f,
"目标点"
);
#endif
}
if (enemyAI != null)
{
AIState currentState = enemyAI.GetCurrentState();
Color stateColor = GetStateColor(currentState);
Gizmos.color = stateColor;
Gizmos.DrawWireSphere(transform.position, 1.0f);
#if UNITY_EDITOR
UnityEditor.Handles.color = stateColor;
UnityEditor.Handles.Label(
transform.position + Vector3.up * 2.5f,
$"状态: {currentState}"
);
#endif
}
if (perceptionSystem != null)
{
List<GameObject> visibleTargets = perceptionSystem.GetVisibleTargets();
Gizmos.color = perceptionColor;
foreach (GameObject target in visibleTargets)
{
if (target != null)
{
Gizmos.DrawLine(transform.position, target.transform.position);
}
}
}
}
private Color GetStateColor(AIState state)
{
switch (state)
{
case AIState.Idle: return Color.gray;
case AIState.Patrol: return Color.blue;
case AIState.Chase: return Color.red;
case AIState.Attack: return Color.magenta;
case AIState.Flee: return Color.yellow;
case AIState.Dead: return Color.black;
default: return Color.white;
}
}
public void AddDebugVisual(string label, Vector3 position, Color color, float size = 0.5f, float duration = -1)
{
if (!enableDebugVisualization) return;
float visualDuration = duration > 0 ? duration : defaultVisualDuration;
DebugVisual visual = new DebugVisual(label, position, color, size, visualDuration);
debugVisuals.Add(visual);
}
public void VisualizePath(List<Vector3> path, Color color)
{
if (!enableDebugVisualization || path == null || path.Count < 2) return;
for (int i = 0; i < path.Count - 1; i++)
{
AddDebugVisual($"路径点{i}", path[i], color, 0.3f, 1.0f);
Gizmos.color = color;
Gizmos.DrawLine(path[i], path[i + 1]);
}
AddDebugVisual("路径终点", path[path.Count - 1], color, 0.5f, 1.0f);
}
public void VisualizeDecision(string decision, Color color)
{
if (!enableDebugVisualization) return;
AddDebugVisual(decision, transform.position + Vector3.up * 1.0f, color, 0.3f, 1.5f);
Debug.Log($"[{gameObject.name}] 决策: {decision}");
}
public void VisualizePerception(GameObject perceivedObject, Color color)
{
if (!enableDebugVisualization || perceivedObject == null) return;
AddDebugVisual("感知目标", perceivedObject.transform.position, color, 0.4f, 0.5f);
}
public void ToggleDebugVisualization(bool enabled)
{
enableDebugVisualization = enabled;
}
public void ClearAllVisuals()
{
debugVisuals.Clear();
}
}
在商业游戏项目中,调试系统需要更加完善和用户友好。可能需要实现调试控制面板,允许实时调整AI参数、查看内部状态和触发特定行为。此外,可能需要记录AI行为日志,支持回放和分析。对于大型团队,可能需要将调试信息通过网络发送到远程监控工具,以便多个开发者同时观察AI行为。
2.6 第一人称/第三人称射击游戏AI系统剖析
FPS/TPS游戏对AI系统提出了独特的要求和挑战。这些游戏的AI需要快速反应、精准射击、战术移动和逼真的战场行为。在这一节中,我们将深入分析FPS/TPS游戏AI的特殊需求,并提供相应的解决方案。
2.6.1 FPS/TPS中的运动控制层特殊性
FPS/TPS游戏的移动控制需要更加精细和响应迅速。AI角色不仅需要基本的导航能力,还需要掌握战术移动技巧,如掩体使用、侧翼移动、跳跃和攀爬。此外,射击游戏中的移动通常更加动态,需要频繁的加速、减速和方向改变。
以下是FPS/TPS专用运动控制器的实现,它扩展了基础导航功能,添加了战术移动能力。
using UnityEngine;
using UnityEngine.AI;
public class FPSMovementController : MonoBehaviour
{
private NavMeshAgent navMeshAgent;
private CharacterController characterController;
private Vector3 moveDirection;
private float verticalVelocity;
private bool isGrounded;
private bool isCrouching;
private bool isSprinting;
[Header("移动参数")]
[SerializeField] private float walkSpeed = 3.0f;
[SerializeField] private float runSpeed = 6.0f;
[SerializeField] private float crouchSpeed = 1.5f;
[SerializeField] private float jumpForce = 5.0f;
[SerializeField] private float gravity = 20.0f;
[SerializeField] private float acceleration = 10.0f;
[SerializeField] private float deceleration = 15.0f;
[Header("战术移动")]
[SerializeField] private float coverMoveSpeed = 2.0f;
[SerializeField] private float strafeSpeed = 4.0f;
[SerializeField] private float backpedalSpeed = 2.5f;
[Header("掩体参数")]
[SerializeField] private float coverCheckDistance = 1.5f;
[SerializeField] private LayerMask coverLayerMask = ~0;
private Cover currentCover;
private CoverType currentCoverType;
private enum CoverType
{
None,
Low,
High,
Corner
}
private class Cover
{
public Vector3 position;
public Vector3 forward;
public CoverType type;
public bool isLeftCorner;
public bool isRightCorner;
public Cover(Vector3 pos, Vector3 fwd, CoverType coverType)
{
position = pos;
forward = fwd;
type = coverType;
isLeftCorner = false;
isRightCorner = false;
}
}
void Awake()
{
InitializeComponents();
}
void Update()
{
UpdateGroundedStatus();
ApplyGravity();
UpdateMovement();
UpdateCoverStatus();
}
private void InitializeComponents()
{
navMeshAgent = GetComponent<NavMeshAgent>();
characterController = GetComponent<CharacterController>();
if (characterController == null)
{
characterController = gameObject.AddComponent<CharacterController>();
characterController.height = 2.0f;
characterController.radius = 0.5f;
characterController.center = new Vector3(0, 1.0f, 0);
}
if (navMeshAgent != null)
{
navMeshAgent.updatePosition = false;
navMeshAgent.updateRotation = false;
}
}
public void MoveToPosition(Vector3 targetPosition, bool useTacticalMovement = false)
{
if (navMeshAgent != null && navMeshAgent.isActiveAndEnabled)
{
navMeshAgent.SetDestination(targetPosition);
if (useTacticalMovement)
{
UseTacticalMovementTowards(targetPosition);
}
}
}
private void UseTacticalMovementTowards(Vector3 targetPosition)
{
Vector3 directionToTarget = (targetPosition - transform.position).normalized;
float distanceToTarget = Vector3.Distance(transform.position, targetPosition);
if (currentCoverType != CoverType.None && distanceToTarget < 10f)
{
MoveUsingCover(directionToTarget);
}
else if (distanceToTarget < 5f)
{
StrafeAroundTarget(targetPosition);
}
else
{
MoveDirectly(directionToTarget);
}
}
private void MoveUsingCover(Vector3 directionToTarget)
{
Vector3 coverForward = currentCover.forward;
Vector3 coverRight = Vector3.Cross(Vector3.up, coverForward).normalized;
float dotProduct = Vector3.Dot(directionToTarget, coverForward);
if (Mathf.Abs(dotProduct) < 0.7f)
{
float rightDot = Vector3.Dot(directionToTarget, coverRight);
if (rightDot > 0 && currentCover.isRightCorner)
{
moveDirection = coverRight;
}
else if (rightDot < 0 && currentCover.isLeftCorner)
{
moveDirection = -coverRight;
}
else
{
moveDirection = coverForward * Mathf.Sign(dotProduct);
}
}
else
{
if (dotProduct > 0)
{
moveDirection = coverForward;
}
else
{
moveDirection = -coverForward;
}
}
moveDirection = moveDirection.normalized * coverMoveSpeed;
}
private void StrafeAroundTarget(Vector3 targetPosition)
{
Vector3 toTarget = targetPosition - transform.position;
toTarget.y = 0;
Vector3 right = Vector3.Cross(Vector3.up, toTarget.normalized);
float strafeDirection = Mathf.Sin(Time.time * 2f);
moveDirection = right * strafeDirection * strafeSpeed;
Vector3 forwardMovement = toTarget.normalized * backpedalSpeed * 0.5f;
moveDirection += forwardMovement;
}
private void MoveDirectly(Vector3 direction)
{
if (isSprinting)
{
moveDirection = direction * runSpeed;
}
else if (isCrouching)
{
moveDirection = direction * crouchSpeed;
}
else
{
moveDirection = direction * walkSpeed;
}
}
private void UpdateMovement()
{
Vector3 desiredMovement = moveDirection;
if (navMeshAgent != null && navMeshAgent.hasPath)
{
Vector3 agentDesiredVelocity = navMeshAgent.desiredVelocity;
if (isSprinting)
{
agentDesiredVelocity = agentDesiredVelocity.normalized * runSpeed;
}
desiredMovement = Vector3.Lerp(
desiredMovement,
agentDesiredVelocity,
acceleration * Time.deltaTime
);
}
else
{
desiredMovement = Vector3.Lerp(
desiredMovement,
Vector3.zero,
deceleration * Time.deltaTime
);
}
desiredMovement.y = verticalVelocity;
if (characterController != null && characterController.enabled)
{
CollisionFlags collisionFlags = characterController.Move(desiredMovement * Time.deltaTime);
if ((collisionFlags & CollisionFlags.Below) != 0)
{
verticalVelocity = -gravity * Time.deltaTime;
}
}
if (navMeshAgent != null)
{
navMeshAgent.velocity = characterController.velocity;
navMeshAgent.nextPosition = transform.position;
}
}
private void UpdateGroundedStatus()
{
if (characterController != null)
{
isGrounded = characterController.isGrounded;
}
}
private void ApplyGravity()
{
if (!isGrounded)
{
verticalVelocity -= gravity * Time.deltaTime;
}
else if (verticalVelocity < 0)
{
verticalVelocity = -gravity * Time.deltaTime;
}
}
private void UpdateCoverStatus()
{
RaycastHit hit;
if (Physics.Raycast(
transform.position + Vector3.up * 0.5f,
transform.forward,
out hit,
coverCheckDistance,
coverLayerMask
))
{
DetermineCoverType(hit);
}
else
{
currentCoverType = CoverType.None;
currentCover = null;
}
}
private void DetermineCoverType(RaycastHit hit)
{
float coverHeight = hit.point.y - transform.position.y;
if (coverHeight < 1.0f)
{
currentCoverType = CoverType.Low;
}
else if (coverHeight < 2.0f)
{
currentCoverType = CoverType.High;
CheckForCorners(hit);
}
else
{
currentCoverType = CoverType.None;
}
if (currentCoverType != CoverType.None)
{
Vector3 coverNormal = hit.normal;
Vector3 coverForward = Vector3.Cross(Vector3.up, coverNormal).normalized;
currentCover = new Cover(hit.point, coverForward, currentCoverType);
}
}
private void CheckForCorners(RaycastHit centerHit)
{
if (currentCover == null) return;
Vector3 right = Vector3.Cross(Vector3.up, currentCover.forward).normalized;
RaycastHit leftHit;
RaycastHit rightHit;
bool leftClear = !Physics.Raycast(
transform.position + Vector3.up * 0.5f + -right * 0.5f,
transform.forward,
out leftHit,
coverCheckDistance,
coverLayerMask
);
bool rightClear = !Physics.Raycast(
transform.position + Vector3.up * 0.5f + right * 0.5f,
transform.forward,
out rightHit,
coverCheckDistance,
coverLayerMask
);
currentCover.isLeftCorner = leftClear;
currentCover.isRightCorner = rightClear;
}
public void SetCrouching(bool crouch)
{
isCrouching = crouch;
if (characterController != null)
{
if (crouch)
{
characterController.height = 1.0f;
characterController.center = new Vector3(0, 0.5f, 0);
}
else
{
characterController.height = 2.0f;
characterController.center = new Vector3(0, 1.0f, 0);
}
}
}
public void SetSprinting(bool sprint)
{
isSprinting = sprint;
}
public void Jump()
{
if (isGrounded)
{
verticalVelocity = jumpForce;
}
}
public bool IsInCover()
{
return currentCoverType != CoverType.None;
}
public CoverType GetCurrentCoverType()
{
return currentCoverType;
}
public Vector3 GetCoverPosition()
{
if (currentCover != null)
{
return currentCover.position;
}
return Vector3.zero;
}
public bool CanSeeFromCover(Vector3 targetPosition)
{
if (currentCover == null) return false;
Vector3 peekPosition = transform.position;
if (currentCover.isLeftCorner)
{
Vector3 right = Vector3.Cross(Vector3.up, currentCover.forward).normalized;
peekPosition += -right * 0.5f;
}
else if (currentCover.isRightCorner)
{
Vector3 right = Vector3.Cross(Vector3.up, currentCover.forward).normalized;
peekPosition += right * 0.5f;
}
Vector3 directionToTarget = targetPosition - peekPosition;
RaycastHit hit;
if (Physics.Raycast(
peekPosition + Vector3.up * 1.5f,
directionToTarget.normalized,
out hit,
directionToTarget.magnitude
))
{
return true;
}
return false;
}
}
在商业FPS/TPS游戏中,运动控制层还需要处理更多复杂情况,如攀爬梯子、游泳、驾驶载具等。此外,需要实现动画重定向系统,确保角色在不同移动状态下的动画自然过渡。网络同步也是重要考虑因素,在多人游戏中需要预测和补偿网络延迟对AI移动的影响。
2.6.2 FPS/TPS中的智能决策层特殊性
射击游戏的决策层需要处理快速变化的战斗情况,做出实时的战术决策。这包括目标选择、攻击时机、移动策略和团队协作。决策层还需要考虑弹药管理、生命值状态和特殊能力使用。
以下是FPS/TPS专用决策系统的实现,它结合了行为树和效用系统,提供快速而智能的决策能力。
using UnityEngine;
using System.Collections.Generic;
using AIDecisionSystem;
public class FPSCombatAI : MonoBehaviour
{
private BehaviorTree combatBehaviorTree;
private FPSMovementController movementController;
private AIPerceptionSystem perceptionSystem;
private WeaponSystem weaponSystem;
private Transform currentTarget;
[Header("战斗参数")]
[SerializeField] private float engagementRange = 30f;
[SerializeField] private float preferredCombatRange = 15f;
[SerializeField] private float maxAimError = 5f;
[SerializeField] private float reactionTime = 0.3f;
[Header("战术偏好")]
[SerializeField] private float aggressionLevel = 0.7f;
[SerializeField] private float accuracyLevel = 0.8f;
[SerializeField] private float cautionLevel = 0.5f;
[Header("武器配置")]
[SerializeField] private float reloadThreshold = 0.3f;
[SerializeField] private float switchWeaponThreshold = 0.4f;
private float timeSinceLastShot;
private float timeSinceLastDecision;
private Vector3 lastTargetPosition;
private CombatState currentCombatState;
private enum CombatState
{
Idle,
Searching,
Aiming,
Firing,
Reloading,
TakingCover,
Flanking,
Retreating
}
void Start()
{
InitializeComponents();
InitializeCombatBehaviorTree();
currentCombatState = CombatState.Idle;
}
void Update()
{
if (combatBehaviorTree != null)
{
combatBehaviorTree.Update(Time.deltaTime);
}
UpdateTimers();
UpdateAim();
}
private void InitializeComponents()
{
movementController = GetComponent<FPSMovementController>();
perceptionSystem = GetComponent<AIPerceptionSystem>();
weaponSystem = GetComponent<WeaponSystem>();
if (weaponSystem == null)
{
weaponSystem = gameObject.AddComponent<WeaponSystem>();
}
}
private void InitializeCombatBehaviorTree()
{
SelectorNode rootSelector = new SelectorNode("CombatRoot");
SequenceNode retreatSequence = new SequenceNode("Retreat");
retreatSequence.AddChild(new ConditionNode("ShouldRetreat", CheckShouldRetreat));
retreatSequence.AddChild(new ActionNode("Retreat", Retreat));
SequenceNode reloadSequence = new SequenceNode("Reload");
reloadSequence.AddChild(new ConditionNode("ShouldReload", CheckShouldReload));
reloadSequence.AddChild(new ActionNode("Reload", Reload));
SequenceNode takeCoverSequence = new SequenceNode("TakeCover");
takeCoverSequence.AddChild(new ConditionNode("ShouldTakeCover", CheckShouldTakeCover));
takeCoverSequence.AddChild(new ActionNode("TakeCover", TakeCover));
SequenceNode flankSequence = new SequenceNode("Flank");
flankSequence.AddChild(new ConditionNode("ShouldFlank", CheckShouldFlank));
flankSequence.AddChild(new ActionNode("Flank", Flank));
SequenceNode engageSequence = new SequenceNode("Engage");
engageSequence.AddChild(new ConditionNode("HasTarget", CheckHasTarget));
engageSequence.AddChild(new ActionNode("EngageTarget", EngageTarget));
SequenceNode searchSequence = new SequenceNode("Search");
searchSequence.AddChild(new ConditionNode("ShouldSearch", CheckShouldSearch));
searchSequence.AddChild(new ActionNode("Search", Search));
rootSelector.AddChild(retreatSequence);
rootSelector.AddChild(reloadSequence);
rootSelector.AddChild(takeCoverSequence);
rootSelector.AddChild(flankSequence);
rootSelector.AddChild(engageSequence);
rootSelector.AddChild(searchSequence);
combatBehaviorTree = new BehaviorTree(rootSelector, 0.05f);
combatBehaviorTree.Start();
}
private NodeStatus CheckShouldRetreat()
{
if (weaponSystem == null) return NodeStatus.Failure;
float healthPercentage = GetHealthPercentage();
float ammoPercentage = weaponSystem.GetAmmoPercentage();
if (healthPercentage < 0.3f && currentTarget != null)
{
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.position);
if (distanceToTarget < preferredCombatRange * 0.5f)
{
return NodeStatus.Success;
}
}
return NodeStatus.Failure;
}
private NodeStatus Retreat()
{
currentCombatState = CombatState.Retreating;
if (currentTarget != null)
{
Vector3 retreatDirection = transform.position - currentTarget.position;
retreatDirection.y = 0;
retreatDirection.Normalize();
Vector3 retreatPosition = transform.position + retreatDirection * 20f;
if (movementController != null)
{
movementController.MoveToPosition(retreatPosition, true);
movementController.SetSprinting(true);
}
return NodeStatus.Running;
}
return NodeStatus.Failure;
}
private NodeStatus CheckShouldReload()
{
if (weaponSystem == null) return NodeStatus.Failure;
if (weaponSystem.GetAmmoPercentage() <= reloadThreshold &&
weaponSystem.CanReload() &&
currentCombatState != CombatState.Reloading)
{
return NodeStatus.Success;
}
return NodeStatus.Failure;
}
private NodeStatus Reload()
{
currentCombatState = CombatState.Reloading;
if (weaponSystem != null && weaponSystem.Reload())
{
if (movementController != null && movementController.IsInCover())
{
movementController.SetCrouching(true);
}
return NodeStatus.Success;
}
return NodeStatus.Failure;
}
private NodeStatus CheckShouldTakeCover()
{
if (movementController == null || currentTarget == null) return NodeStatus.Failure;
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.position);
if (distanceToTarget < preferredCombatRange && !movementController.IsInCover())
{
if (Random.value < cautionLevel || GetHealthPercentage() < 0.6f)
{
return NodeStatus.Success;
}
}
return NodeStatus.Failure;
}
private NodeStatus TakeCover()
{
currentCombatState = CombatState.TakingCover;
if (movementController != null && currentTarget != null)
{
Vector3 toTarget = currentTarget.position - transform.position;
Vector3 potentialCoverPosition = FindNearbyCover(toTarget);
if (potentialCoverPosition != Vector3.zero)
{
movementController.MoveToPosition(potentialCoverPosition, false);
return NodeStatus.Running;
}
}
return NodeStatus.Failure;
}
private Vector3 FindNearbyCover(Vector3 threatDirection)
{
Vector3[] testDirections =
{
threatDirection.normalized,
Quaternion.Euler(0, 45, 0) * threatDirection.normalized,
Quaternion.Euler(0, -45, 0) * threatDirection.normalized,
Quaternion.Euler(0, 90, 0) * threatDirection.normalized,
Quaternion.Euler(0, -90, 0) * threatDirection.normalized
};
float testDistance = 10f;
foreach (Vector3 direction in testDirections)
{
RaycastHit hit;
if (Physics.Raycast(
transform.position + Vector3.up * 1.0f,
direction,
out hit,
testDistance
))
{
if (hit.collider.CompareTag("Cover") || hit.collider.CompareTag("Wall"))
{
Vector3 coverPosition = hit.point - direction * 1.0f;
NavMeshHit navHit;
if (NavMesh.SamplePosition(coverPosition, out navHit, 2f, NavMesh.AllAreas))
{
return navHit.position;
}
}
}
}
return Vector3.zero;
}
private NodeStatus CheckShouldFlank()
{
if (currentTarget == null || movementController == null) return NodeStatus.Failure;
if (movementController.IsInCover() && currentCombatState != CombatState.Flanking)
{
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.position);
if (distanceToTarget > preferredCombatRange * 1.5f && Random.value < aggressionLevel)
{
if (!movementController.CanSeeFromCover(currentTarget.position))
{
return NodeStatus.Success;
}
}
}
return NodeStatus.Failure;
}
private NodeStatus Flank()
{
currentCombatState = CombatState.Flanking;
if (currentTarget != null && movementController != null)
{
Vector3 toTarget = currentTarget.position - transform.position;
Vector3 flankDirection = Vector3.Cross(Vector3.up, toTarget).normalized;
float flankMultiplier = Random.value > 0.5f ? 1f : -1f;
Vector3 flankPosition = currentTarget.position + flankDirection * 10f * flankMultiplier;
movementController.MoveToPosition(flankPosition, true);
return NodeStatus.Running;
}
return NodeStatus.Failure;
}
private NodeStatus CheckHasTarget()
{
if (perceptionSystem == null) return NodeStatus.Failure;
List<GameObject> visibleTargets = perceptionSystem.GetVisibleTargets();
if (visibleTargets.Count > 0)
{
currentTarget = SelectBestTarget(visibleTargets);
if (currentTarget != null)
{
lastTargetPosition = currentTarget.position;
return NodeStatus.Success;
}
}
else if (currentTarget != null)
{
float timeSinceLastSeen = Time.time - perceptionSystem.GetLatestStimulusOfType(
AIPerceptionSystem.VisualStimulus.StimulusType.Visual
)?.timestamp ?? Mathf.Infinity;
if (timeSinceLastSeen < 5f)
{
return NodeStatus.Success;
}
else
{
currentTarget = null;
}
}
return NodeStatus.Failure;
}
private Transform SelectBestTarget(List<GameObject> targets)
{
Transform bestTarget = null;
float bestScore = float.MinValue;
foreach (GameObject target in targets)
{
float score = CalculateTargetScore(target);
if (score > bestScore)
{
bestScore = score;
bestTarget = target.transform;
}
}
return bestTarget;
}
private float CalculateTargetScore(GameObject target)
{
if (target == null) return 0f;
float distance = Vector3.Distance(transform.position, target.transform.position);
float distanceScore = Mathf.Clamp01(1 - distance / engagementRange);
Vector3 directionToTarget = target.transform.position - transform.position;
float angleScore = Mathf.Clamp01(1 - Vector3.Angle(transform.forward, directionToTarget) / 180f);
float threatScore = 1f;
PlayerHealth targetHealth = target.GetComponent<PlayerHealth>();
if (targetHealth != null)
{
threatScore = 1 - targetHealth.GetHealthPercentage();
}
float finalScore = distanceScore * 0.4f + angleScore * 0.3f + threatScore * 0.3f;
return finalScore;
}
private NodeStatus EngageTarget()
{
if (currentTarget == null) return NodeStatus.Failure;
float distanceToTarget = Vector3.Distance(transform.position, currentTarget.position);
if (distanceToTarget > engagementRange)
{
currentCombatState = CombatState.Searching;
return NodeStatus.Failure;
}
if (distanceToTarget > preferredCombatRange && !movementController.IsInCover())
{
Vector3 movePosition = currentTarget.position -
(currentTarget.position - transform.position).normalized * preferredCombatRange * 0.8f;
movementController.MoveToPosition(movePosition, true);
currentCombatState = CombatState.Searching;
}
else
{
movementController.MoveToPosition(transform.position, false);
if (CanShootTarget())
{
currentCombatState = CombatState.Aiming;
if (timeSinceLastShot >= reactionTime)
{
currentCombatState = CombatState.Firing;
if (weaponSystem != null && weaponSystem.Fire())
{
timeSinceLastShot = 0f;
}
}
}
}
return NodeStatus.Running;
}
private bool CanShootTarget()
{
if (currentTarget == null || weaponSystem == null) return false;
Vector3 aimDirection = currentTarget.position - weaponSystem.GetFirePoint();
float aimAngle = Vector3.Angle(transform.forward, aimDirection);
return aimAngle <= maxAimError;
}
private NodeStatus CheckShouldSearch()
{
if (currentTarget == null && perceptionSystem != null)
{
List<AIPerceptionSystem.VisualStimulus> rememberedStimuli =
perceptionSystem.GetRememberedStimuli();
if (rememberedStimuli.Count > 0)
{
return NodeStatus.Success;
}
}
return NodeStatus.Failure;
}
private NodeStatus Search()
{
currentCombatState = CombatState.Searching;
if (perceptionSystem != null)
{
AIPerceptionSystem.VisualStimulus lastStimulus =
perceptionSystem.GetLatestStimulusOfType(
AIPerceptionSystem.VisualStimulus.StimulusType.Visual
);
if (lastStimulus != null)
{
if (movementController != null)
{
movementController.MoveToPosition(lastStimulus.position, true);
}
return NodeStatus.Running;
}
}
return NodeStatus.Failure;
}
private void UpdateTimers()
{
timeSinceLastShot += Time.deltaTime;
timeSinceLastDecision += Time.deltaTime;
}
private void UpdateAim()
{
if (currentTarget != null && weaponSystem != null)
{
Vector3 targetPosition = currentTarget.position + Vector3.up * 1.0f;
Vector3 aimDirection = targetPosition - weaponSystem.GetFirePoint();
float aimError = maxAimError * (1 - accuracyLevel);
Vector3 errorOffset = new Vector3(
Random.Range(-aimError, aimError),
Random.Range(-aimError, aimError * 0.5f),
0
);
Vector3 finalAimDirection = aimDirection + errorOffset;
transform.rotation = Quaternion.Slerp(
transform.rotation,
Quaternion.LookRotation(finalAimDirection),
Time.deltaTime * 10f
);
}
}
private float GetHealthPercentage()
{
PlayerHealth health = GetComponent<PlayerHealth>();
if (health != null)
{
return health.GetHealthPercentage();
}
return 1f;
}
public CombatState GetCurrentCombatState()
{
return currentCombatState;
}
public Transform GetCurrentTarget()
{
return currentTarget;
}
}
public class WeaponSystem : MonoBehaviour
{
[SerializeField] private float fireRate = 0.2f;
[SerializeField] private int maxAmmo = 30;
[SerializeField] private float reloadTime = 2.0f;
private int currentAmmo;
private float fireTimer;
private float reloadTimer;
private bool isReloading;
void Start()
{
currentAmmo = maxAmmo;
}
void Update()
{
if (fireTimer > 0)
{
fireTimer -= Time.deltaTime;
}
if (isReloading)
{
reloadTimer -= Time.deltaTime;
if (reloadTimer <= 0)
{
isReloading = false;
currentAmmo = maxAmmo;
}
}
}
public bool Fire()
{
if (fireTimer > 0 || currentAmmo <= 0 || isReloading)
{
return false;
}
fireTimer = fireRate;
currentAmmo--;
return true;
}
public bool Reload()
{
if (isReloading || currentAmmo == maxAmmo)
{
return false;
}
isReloading = true;
reloadTimer = reloadTime;
return true;
}
public float GetAmmoPercentage()
{
return (float)currentAmmo / maxAmmo;
}
public bool CanReload()
{
return !isReloading;
}
public Vector3 GetFirePoint()
{
return transform.position + transform.forward * 0.5f + Vector3.up * 1.5f;
}
}
public class PlayerHealth : MonoBehaviour
{
[SerializeField] private float maxHealth = 100f;
private float currentHealth;
void Start()
{
currentHealth = maxHealth;
}
public float GetHealthPercentage()
{
return currentHealth / maxHealth;
}
}
在商业FPS/TPS游戏中,决策层需要更加复杂和精细。可能需要实现基于机器学习的自适应行为,使AI能够从与玩家的互动中学习。此外,可能需要支持脚本化行为序列,允许设计师创建特定的遭遇战场景。网络同步也是重要考虑因素,在多人游戏中需要确保AI决策在所有客户端一致。
2.6.3 FPS/TPS中的战略规划层特殊性
射击游戏的战略规划层需要考虑团队战术、地图控制、目标优先级和资源分配。在团队为基础的游戏中,AI需要与队友协作,执行复杂的战术动作,如交叉火力、包抄和掩护。战略规划层还需要考虑游戏模式特定的目标,如夺旗、占领据点和护送目标。
以下是FPS/TPS团队战术系统的实现,它扩展了基础团队协调系统,添加了射击游戏特定的战术功能。
using System.Collections.Generic;
using UnityEngine;
public class FPSTeamTactics : MonoBehaviour
{
[System.Serializable]
public class TacticalObjective
{
public string objectiveName;
public Vector3 objectivePosition;
public float priority;
public ObjectiveType type;
public float captureProgress;
public enum ObjectiveType
{
CapturePoint,
DefendArea,
AttackTarget,
FlankPosition,
ProvideCover
}
public TacticalObjective(string name, Vector3 position, float pri, ObjectiveType objType)
{
objectiveName = name;
objectivePosition = position;
priority = pri;
type = objType;
captureProgress = 0f;
}
}
[Header("团队配置")]
[SerializeField] private string teamName = "Alpha";
[SerializeField] private int teamSize = 5;
[SerializeField] private float tacticalUpdateInterval = 2.0f;
[Header("战术参数")]
[SerializeField] private float aggressionMultiplier = 1.0f;
[SerializeField] private float defenseMultiplier = 1.0f;
[SerializeField] private float objectivePriorityWeight = 0.6f;
[SerializeField] private float threatPriorityWeight = 0.4f;
private List<FPSCombatAI> teamMembers;
private List<TacticalObjective> activeObjectives;
private Dictionary<FPSCombatAI, TacticalObjective> memberAssignments;
private float tacticalTimer;
private TeamStrategy currentStrategy;
private enum TeamStrategy
{
Aggressive,
Defensive,
Balanced,
Opportunistic
}
void Start()
{
InitializeTeam();
InitializeObjectives();
memberAssignments = new Dictionary<FPSCombatAI, TacticalObjective>();
currentStrategy = TeamStrategy.Balanced;
}
void Update()
{
tacticalTimer += Time.deltaTime;
if (tacticalTimer >= tacticalUpdateInterval)
{
UpdateTacticalAssignments();
tacticalTimer = 0f;
}
UpdateObjectiveProgress();
}
private void InitializeTeam()
{
teamMembers = new List<FPSCombatAI>();
FPSCombatAI[] allAIs = FindObjectsOfType<FPSCombatAI>();
int membersAdded = 0;
foreach (FPSCombatAI ai in allAIs)
{
if (membersAdded >= teamSize) break;
if (ai.gameObject != gameObject)
{
teamMembers.Add(ai);
membersAdded++;
}
}
Debug.Log($"战术团队初始化: {teamName} 有 {teamMembers.Count} 名成员");
}
private void InitializeObjectives()
{
activeObjectives = new List<TacticalObjective>();
GameObject[] capturePoints = GameObject.FindGameObjectsWithTag("CapturePoint");
foreach (GameObject point in capturePoints)
{
TacticalObjective objective = new TacticalObjective(
point.name,
point.transform.position,
0.8f,
TacticalObjective.ObjectiveType.CapturePoint
);
activeObjectives.Add(objective);
}
GameObject[] defensePoints = GameObject.FindGameObjectsWithTag("DefensePoint");
foreach (GameObject point in defensePoints)
{
TacticalObjective objective = new TacticalObjective(
point.name,
point.transform.position,
0.7f,
TacticalObjective.ObjectiveType.DefendArea
);
activeObjectives.Add(objective);
}
}
private void UpdateTacticalAssignments()
{
if (teamMembers.Count == 0 || activeObjectives.Count == 0) return;
UpdateObjectivePriorities();
List<TacticalObjective> availableObjectives = new List<TacticalObjective>(activeObjectives);
List<FPSCombatAI> unassignedMembers = new List<FPSCombatAI>(teamMembers);
availableObjectives.Sort((a, b) => b.priority.CompareTo(a.priority));
memberAssignments.Clear();
foreach (TacticalObjective objective in availableObjectives)
{
if (unassignedMembers.Count == 0) break;
List<FPSCombatAI> suitableMembers = FindSuitableMembersForObjective(
objective,
unassignedMembers
);
if (suitableMembers.Count > 0)
{
int membersToAssign = CalculateMembersNeededForObjective(objective);
membersToAssign = Mathf.Min(membersToAssign, suitableMembers.Count);
for (int i = 0; i < membersToAssign; i++)
{
FPSCombatAI member = suitableMembers[i];
memberAssignments[member] = objective;
unassignedMembers.Remove(member);
AssignObjectiveToMember(member, objective);
}
}
}
foreach (FPSCombatAI unassignedMember in unassignedMembers)
{
AssignDefaultBehavior(unassignedMember);
}
}
private void UpdateObjectivePriorities()
{
foreach (TacticalObjective objective in activeObjectives)
{
float basePriority = objective.priority;
float threatLevel = CalculateThreatLevelAtPosition(objective.objectivePosition);
float teamPresence = CalculateTeamPresenceAtPosition(objective.objectivePosition);
float strategyMultiplier = 1.0f;
switch (currentStrategy)
{
case TeamStrategy.Aggressive:
if (objective.type == TacticalObjective.ObjectiveType.AttackTarget)
{
strategyMultiplier = 1.5f;
}
break;
case TeamStrategy.Defensive:
if (objective.type == TacticalObjective.ObjectiveType.DefendArea)
{
strategyMultiplier = 1.5f;
}
break;
}
float adjustedPriority = basePriority * strategyMultiplier;
adjustedPriority += threatLevel * threatPriorityWeight;
adjustedPriority -= teamPresence * 0.3f;
objective.priority = Mathf.Clamp01(adjustedPriority);
}
}
private float CalculateThreatLevelAtPosition(Vector3 position)
{
float threatLevel = 0f;
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
foreach (GameObject enemy in enemies)
{
float distance = Vector3.Distance(position, enemy.transform.position);
if (distance < 20f)
{
threatLevel += 1f - (distance / 20f);
}
}
return Mathf.Clamp01(threatLevel);
}
private float CalculateTeamPresenceAtPosition(Vector3 position)
{
float presence = 0f;
foreach (FPSCombatAI member in teamMembers)
{
if (member != null)
{
float distance = Vector3.Distance(position, member.transform.position);
if (distance < 15f)
{
presence += 1f - (distance / 15f);
}
}
}
return Mathf.Clamp01(presence);
}
private List<FPSCombatAI> FindSuitableMembersForObjective(
TacticalObjective objective,
List<FPSCombatAI> availableMembers
)
{
List<FPSCombatAI> suitableMembers = new List<FPSCombatAI>();
foreach (FPSCombatAI member in availableMembers)
{
if (member == null) continue;
float distanceToObjective = Vector3.Distance(
member.transform.position,
objective.objectivePosition
);
float memberHealth = 1f;
PlayerHealth health = member.GetComponent<PlayerHealth>();
if (health != null)
{
memberHealth = health.GetHealthPercentage();
}
bool isSuitable = false;
switch (objective.type)
{
case TacticalObjective.ObjectiveType.AttackTarget:
isSuitable = memberHealth > 0.5f && distanceToObjective < 30f;
break;
case TacticalObjective.ObjectiveType.DefendArea:
isSuitable = memberHealth > 0.3f;
break;
case TacticalObjective.ObjectiveType.CapturePoint:
isSuitable = distanceToObjective < 25f;
break;
case TacticalObjective.ObjectiveType.FlankPosition:
isSuitable = memberHealth > 0.6f;
break;
case TacticalObjective.ObjectiveType.ProvideCover:
isSuitable = true;
break;
}
if (isSuitable)
{
suitableMembers.Add(member);
}
}
suitableMembers.Sort((a, b) =>
Vector3.Distance(a.transform.position, objective.objectivePosition)
.CompareTo(Vector3.Distance(b.transform.position, objective.objectivePosition))
);
return suitableMembers;
}
private int CalculateMembersNeededForObjective(TacticalObjective objective)
{
switch (objective.type)
{
case TacticalObjective.ObjectiveType.AttackTarget:
return Mathf.CeilToInt(3 * aggressionMultiplier);
case TacticalObjective.ObjectiveType.DefendArea:
return Mathf.CeilToInt(2 * defenseMultiplier);
case TacticalObjective.ObjectiveType.CapturePoint:
return 2;
case TacticalObjective.ObjectiveType.FlankPosition:
return 1;
case TacticalObjective.ObjectiveType.ProvideCover:
return 1;
default:
return 1;
}
}
private void AssignObjectiveToMember(FPSCombatAI member, TacticalObjective objective)
{
if (member == null) return;
FPSMovementController movement = member.GetComponent<FPSMovementController>();
if (movement != null)
{
movement.MoveToPosition(objective.objectivePosition, true);
}
Debug.Log($"分配目标 {objective.objectiveName} 给 {member.gameObject.name}");
}
private void AssignDefaultBehavior(FPSCombatAI member)
{
if (member == null) return;
Vector3 defaultPosition = FindDefaultPosition(member);
FPSMovementController movement = member.GetComponent<FPSMovementController>();
if (movement != null)
{
movement.MoveToPosition(defaultPosition, false);
}
}
private Vector3 FindDefaultPosition(FPSCombatAI member)
{
if (activeObjectives.Count > 0)
{
float closestDistance = float.MaxValue;
Vector3 closestObjective = Vector3.zero;
foreach (TacticalObjective objective in activeObjectives)
{
float distance = Vector3.Distance(member.transform.position, objective.objectivePosition);
if (distance < closestDistance)
{
closestDistance = distance;
closestObjective = objective.objectivePosition;
}
}
return closestObjective;
}
return member.transform.position + Random.insideUnitSphere * 10f;
}
private void UpdateObjectiveProgress()
{
foreach (TacticalObjective objective in activeObjectives)
{
if (objective.type == TacticalObjective.ObjectiveType.CapturePoint)
{
float teamPresence = CalculateTeamPresenceAtPosition(objective.objectivePosition);
float enemyPresence = CalculateThreatLevelAtPosition(objective.objectivePosition);
if (teamPresence > enemyPresence * 1.5f)
{
objective.captureProgress += Time.deltaTime * 0.1f;
}
else if (enemyPresence > teamPresence * 1.5f)
{
objective.captureProgress -= Time.deltaTime * 0.05f;
}
objective.captureProgress = Mathf.Clamp01(objective.captureProgress);
}
}
}
public void SetTeamStrategy(TeamStrategy newStrategy)
{
currentStrategy = newStrategy;
switch (newStrategy)
{
case TeamStrategy.Aggressive:
aggressionMultiplier = 1.5f;
defenseMultiplier = 0.7f;
break;
case TeamStrategy.Defensive:
aggressionMultiplier = 0.7f;
defenseMultiplier = 1.5f;
break;
case TeamStrategy.Balanced:
aggressionMultiplier = 1.0f;
defenseMultiplier = 1.0f;
break;
case TeamStrategy.Opportunistic:
aggressionMultiplier = 1.2f;
defenseMultiplier = 0.8f;
break;
}
}
public void AddObjective(TacticalObjective newObjective)
{
activeObjectives.Add(newObjective);
}
public void RemoveObjective(string objectiveName)
{
activeObjectives.RemoveAll(obj => obj.objectiveName == objectiveName);
}
public List<TacticalObjective> GetActiveObjectives()
{
return new List<TacticalObjective>(activeObjectives);
}
public TeamStrategy GetCurrentStrategy()
{
return currentStrategy;
}
void OnDrawGizmos()
{
if (activeObjectives != null)
{
foreach (TacticalObjective objective in activeObjectives)
{
Color objectiveColor = Color.white;
switch (objective.type)
{
case TacticalObjective.ObjectiveType.CapturePoint:
objectiveColor = Color.yellow;
break;
case TacticalObjective.ObjectiveType.DefendArea:
objectiveColor = Color.blue;
break;
case TacticalObjective.ObjectiveType.AttackTarget:
objectiveColor = Color.red;
break;
case TacticalObjective.ObjectiveType.FlankPosition:
objectiveColor = Color.green;
break;
case TacticalObjective.ObjectiveType.ProvideCover:
objectiveColor = Color.cyan;
break;
}
Gizmos.color = objectiveColor;
Gizmos.DrawWireSphere(objective.objectivePosition, 2f);
#if UNITY_EDITOR
UnityEditor.Handles.color = objectiveColor;
UnityEditor.Handles.Label(
objective.objectivePosition + Vector3.up * 3f,
$"{objective.objectiveName}\n优先级: {objective.priority:F2}"
);
#endif
}
}
}
}
在商业FPS/TPS游戏中,战略规划层需要更加复杂和动态。可能需要实现基于态势评估的动态策略调整,使AI团队能够根据战局变化改变战术。此外,可能需要支持多层指挥结构,允许高级AI指挥官协调多个小队的行为。与游戏事件系统的集成也很重要,确保AI能够响应游戏状态变化,如目标更新、增援到达或任务阶段转换。
2.6.4 FPS/TPS中AI架构的支撑系统
FPS/TPS游戏的AI支撑系统需要特别关注性能优化和网络同步。射击游戏通常具有快节奏的游戏玩法和大量动态元素,AI系统需要在严格的时间预算内运行。此外,在多人游戏中,AI行为需要在所有客户端一致,这需要精心的网络架构设计。
以下是FPS/TPS优化AI管理器的实现,它提供了性能优化和网络同步功能。
using System.Collections.Generic;
using UnityEngine;
public class FPSAIManager : MonoBehaviour
{
[System.Serializable]
public class AIPerformanceProfile
{
public string profileName;
public float updateInterval;
public int maxActiveAIs;
public bool useLOD;
public float lodDistance;
public AIPerformanceProfile(string name, float interval, int maxAI, bool lod, float distance)
{
profileName = name;
updateInterval = interval;
maxActiveAIs = maxAI;
useLOD = lod;
lodDistance = distance;
}
}
[Header("性能配置")]
[SerializeField] private AIPerformanceProfile[] performanceProfiles;
[SerializeField] private string currentProfileName = "High";
[Header("网络同步")]
[SerializeField] private bool enableNetworkSync = false;
[SerializeField] private float syncInterval = 0.1f;
private Dictionary<FPSCombatAI, AIControllerData> aiControllers;
private List<FPSCombatAI> activeAIs;
private List<FPSCombatAI> inactiveAIs;
private float updateTimer;
private float syncTimer;
private AIPerformanceProfile currentProfile;
private Transform playerTransform;
private class AIControllerData
{
public float updateTimer;
public float timeSinceLastUpdate;
public int lodLevel;
public bool isActive;
public AIControllerData()
{
updateTimer = 0f;
timeSinceLastUpdate = 0f;
lodLevel = 0;
isActive = true;
}
}
void Start()
{
InitializeAIManager();
LoadPerformanceProfile(currentProfileName);
}
void Update()
{
updateTimer += Time.deltaTime;
if (updateTimer >= GetProfileUpdateInterval())
{
UpdateAIControllers();
updateTimer = 0f;
}
if (enableNetworkSync)
{
syncTimer += Time.deltaTime;
if (syncTimer >= syncInterval)
{
SyncAIStates();
syncTimer = 0f;
}
}
}
private void InitializeAIManager()
{
aiControllers = new Dictionary<FPSCombatAI, AIControllerData>();
activeAIs = new List<FPSCombatAI>();
inactiveAIs = new List<FPSCombatAI>();
FPSCombatAI[] allAIs = FindObjectsOfType<FPSCombatAI>();
foreach (FPSCombatAI ai in allAIs)
{
AIControllerData data = new AIControllerData();
aiControllers[ai] = data;
if (data.isActive)
{
activeAIs.Add(ai);
}
else
{
inactiveAIs.Add(ai);
}
}
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
playerTransform = player.transform;
}
Debug.Log($"AI管理器初始化: {aiControllers.Count} 个AI控制器已注册");
}
private void LoadPerformanceProfile(string profileName)
{
foreach (AIPerformanceProfile profile in performanceProfiles)
{
if (profile.profileName == profileName)
{
currentProfile = profile;
Debug.Log($"已加载AI性能配置: {profileName}");
return;
}
}
Debug.LogWarning($"未找到AI性能配置: {profileName},使用默认配置");
currentProfile = new AIPerformanceProfile("Default", 0.1f, 20, true, 50f);
}
private void UpdateAIControllers()
{
if (playerTransform == null) return;
UpdateAIActivityStates();
int updatedThisFrame = 0;
int maxUpdatesPerFrame = Mathf.CeilToInt(activeAIs.Count * 0.1f);
foreach (FPSCombatAI ai in activeAIs)
{
if (updatedThisFrame >= maxUpdatesPerFrame) break;
if (aiControllers.TryGetValue(ai, out AIControllerData data))
{
data.updateTimer += Time.deltaTime;
float updateInterval = GetAIUpdateInterval(ai);
if (data.updateTimer >= updateInterval)
{
UpdateAIController(ai, data);
data.updateTimer = 0f;
updatedThisFrame++;
}
}
}
}
private void UpdateAIActivityStates()
{
if (playerTransform == null || currentProfile == null) return;
List<FPSCombatAI> aiList = new List<FPSCombatAI>(aiControllers.Keys);
foreach (FPSCombatAI ai in aiList)
{
if (ai == null) continue;
float distanceToPlayer = Vector3.Distance(ai.transform.position, playerTransform.position);
bool shouldBeActive = ShouldAIActive(ai, distanceToPlayer);
if (aiControllers.TryGetValue(ai, out AIControllerData data))
{
if (shouldBeActive && !data.isActive)
{
ActivateAI(ai, data);
}
else if (!shouldBeActive && data.isActive)
{
DeactivateAI(ai, data);
}
if (currentProfile.useLOD)
{
UpdateAILOD(ai, data, distanceToPlayer);
}
}
}
LimitActiveAICount();
}
private bool ShouldAIActive(FPSCombatAI ai, float distanceToPlayer)
{
if (ai == null) return false;
if (distanceToPlayer > currentProfile.lodDistance * 2f)
{
return false;
}
AIPerceptionSystem perception = ai.GetComponent<AIPerceptionSystem>();
if (perception != null)
{
if (perception.CanSeeTarget(playerTransform.gameObject))
{
return true;
}
}
if (distanceToPlayer < currentProfile.lodDistance)
{
return true;
}
return false;
}
private void UpdateAILOD(FPSCombatAI ai, AIControllerData data, float distanceToPlayer)
{
if (distanceToPlayer < currentProfile.lodDistance * 0.3f)
{
data.lodLevel = 0;
}
else if (distanceToPlayer < currentProfile.lodDistance * 0.6f)
{
data.lodLevel = 1;
}
else if (distanceToPlayer < currentProfile.lodDistance)
{
data.lodLevel = 2;
}
else
{
data.lodLevel = 3;
}
}
private float GetAIUpdateInterval(FPSCombatAI ai)
{
if (aiControllers.TryGetValue(ai, out AIControllerData data))
{
switch (data.lodLevel)
{
case 0: return currentProfile.updateInterval * 0.5f;
case 1: return currentProfile.updateInterval;
case 2: return currentProfile.updateInterval * 2f;
case 3: return currentProfile.updateInterval * 4f;
default: return currentProfile.updateInterval;
}
}
return currentProfile.updateInterval;
}
private void UpdateAIController(FPSCombatAI ai, AIControllerData data)
{
if (ai == null) return;
switch (data.lodLevel)
{
case 0:
UpdateAIHighDetail(ai);
break;
case 1:
UpdateAIMediumDetail(ai);
break;
case 2:
UpdateAILowDetail(ai);
break;
case 3:
UpdateAIMinimalDetail(ai);
break;
}
data.timeSinceLastUpdate = 0f;
}
private void UpdateAIHighDetail(FPSCombatAI ai)
{
if (ai.enabled)
{
ai.Update();
}
AIPerceptionSystem perception = ai.GetComponent<AIPerceptionSystem>();
if (perception != null)
{
perception.Update();
}
}
private void UpdateAIMediumDetail(FPSCombatAI ai)
{
if (ai.enabled)
{
ai.Update();
}
}
private void UpdateAILowDetail(FPSCombatAI ai)
{
}
private void UpdateAIMinimalDetail(FPSCombatAI ai)
{
}
private void ActivateAI(FPSCombatAI ai, AIControllerData data)
{
data.isActive = true;
if (!activeAIs.Contains(ai))
{
activeAIs.Add(ai);
}
if (inactiveAIs.Contains(ai))
{
inactiveAIs.Remove(ai);
}
ai.enabled = true;
Debug.Log($"激活AI: {ai.gameObject.name}");
}
private void DeactivateAI(FPSCombatAI ai, AIControllerData data)
{
data.isActive = false;
if (activeAIs.Contains(ai))
{
activeAIs.Remove(ai);
}
if (!inactiveAIs.Contains(ai))
{
inactiveAIs.Add(ai);
}
ai.enabled = false;
Debug.Log($"停用AI: {ai.gameObject.name}");
}
private void LimitActiveAICount()
{
if (activeAIs.Count <= currentProfile.maxActiveAIs) return;
activeAIs.Sort((a, b) =>
Vector3.Distance(a.transform.position, playerTransform.position)
.CompareTo(Vector3.Distance(b.transform.position, playerTransform.position))
);
for (int i = currentProfile.maxActiveAIs; i < activeAIs.Count; i++)
{
FPSCombatAI ai = activeAIs[i];
if (aiControllers.TryGetValue(ai, out AIControllerData data))
{
DeactivateAI(ai, data);
}
}
}
private void SyncAIStates()
{
if (!enableNetworkSync) return;
foreach (FPSCombatAI ai in activeAIs)
{
if (ai == null) continue;
SyncAIData syncData = new SyncAIData();
syncData.position = ai.transform.position;
syncData.rotation = ai.transform.rotation;
syncData.state = (int)ai.GetCurrentCombatState();
Transform target = ai.GetCurrentTarget();
if (target != null)
{
syncData.targetPosition = target.position;
}
BroadcastAISyncData(ai.gameObject.name, syncData);
}
}
private void BroadcastAISyncData(string aiName, SyncAIData data)
{
}
private float GetProfileUpdateInterval()
{
if (currentProfile != null)
{
return currentProfile.updateInterval;
}
return 0.1f;
}
public void RegisterAI(FPSCombatAI ai)
{
if (ai == null) return;
if (!aiControllers.ContainsKey(ai))
{
AIControllerData data = new AIControllerData();
aiControllers[ai] = data;
if (data.isActive)
{
activeAIs.Add(ai);
}
else
{
inactiveAIs.Add(ai);
}
}
}
public void UnregisterAI(FPSCombatAI ai)
{
if (ai == null) return;
if (aiControllers.ContainsKey(ai))
{
aiControllers.Remove(ai);
activeAIs.Remove(ai);
inactiveAIs.Remove(ai);
}
}
public void SetPerformanceProfile(string profileName)
{
LoadPerformanceProfile(profileName);
}
public int GetActiveAICount()
{
return activeAIs.Count;
}
public int GetTotalAICount()
{
return aiControllers.Count;
}
public float GetAIActivityPercentage()
{
if (aiControllers.Count == 0) return 0f;
return (float)activeAIs.Count / aiControllers.Count;
}
private class SyncAIData
{
public Vector3 position;
public Quaternion rotation;
public int state;
public Vector3 targetPosition;
}
}
在商业FPS/TPS游戏中,AI支撑系统需要更加完善和健壮。可能需要实现预测性资源加载,确保AI角色在进入玩家视野时已经完成初始化。还需要实现异常处理机制,确保单个AI的崩溃不会影响整个系统。此外,可能需要提供详细的分析工具,帮助开发者优化AI性能和调试复杂问题。
总结而言,Unity3D中的AI分层架构设计是一个复杂但必要的工程实践。通过将AI系统分解为运动控制层、智能决策层和战略规划层,并辅以完善的支撑系统,开发者可以创建出既高性能又易于维护的智能角色。在FPS/TPS等特定游戏类型中,还需要针对性地调整和扩展各层功能,以满足特定游戏玩法的需求。随着游戏复杂度的提高和玩家期望的增长,良好的AI架构设计将成为决定游戏成功的关键因素之一。
更多推荐



所有评论(0)