第7章 智能体感知系统构建
智能体感知系统构建摘要 本章探讨了Unity中智能体感知系统的设计与实现。感知系统作为智能体与环境交互的核心模块,通过视觉、听觉等感官输入支持决策制定。重点介绍了两种主要实现方法:基于碰撞系统的感知(使用射线投射检测视线)和基于网格图的感知(提供更高精度)。视觉感知模块包含视野参数配置、目标检测与记忆功能,通过射线投射模拟生物视觉。系统还支持感知调试和性能优化,适用于潜行类游戏等需要复杂NPC行为
第7章 智能体感知系统构建
7.1 感知系统概述
在游戏人工智能开发中,感知系统是实现智能体与环境交互的核心模块。一个完善的感知系统能够让非玩家角色(NPC)像真实生物一样感知周围世界,根据视觉、听觉、嗅觉等多种感官输入做出合理的决策反应。本章将深入探讨Unity引擎中智能体感知系统的设计与实现,涵盖基于碰撞系统和基于网格图的两种主要实现方法,并最终将这些技术应用于潜行类游戏的开发实践。
感知系统在游戏AI中的作用至关重要。它不仅是智能体获取环境信息的窗口,更是决策系统的基础输入。在商业游戏项目中,一个设计良好的感知系统能够显著提升游戏的真实感和沉浸感。例如,在《刺客信条》系列中,敌人的感知系统决定了玩家是否被发现;在《最后生还者》中,感染者的听觉感知创造了紧张的游戏氛围。
Unity引擎提供了多种实现感知系统的技术方案。基于碰撞系统的感知实现简单高效,适合中小型项目;基于网格图的感知则更加精确和可配置,适合大型复杂项目。无论采用哪种方案,都需要考虑性能优化、真实性平衡和可扩展性等关键因素。
7.2 基于碰撞检测的视觉感知
视觉感知是智能体最重要的感官通道,它模拟了生物通过眼睛观察世界的能力。基于碰撞系统的视觉感知使用物理碰撞检测来模拟视线检测,这是Unity中最直接、最高效的实现方式之一。
7.2.1 视觉感知理论基础
基于碰撞的视觉感知核心原理是通过射线投射(Raycasting)来检测智能体与目标之间是否存在直接视线。这种方法的优势在于:
- 直接利用Unity的物理系统,性能高效
- 实现简单,调试直观
- 支持动态障碍物检测
- 可以精确计算视线遮挡
然而,这种方法也有局限性:
- 视线检测是离散的,可能错过快速移动的目标
- 大量射线投射可能影响性能
- 对于复杂形状的障碍物需要更精细的处理
7.2.2 Unity中的视觉感知实现
以下是一个完整的基于碰撞系统的视觉感知模块实现:
using UnityEngine;
using System.Collections.Generic;
namespace PerceptionSystem
{
// 视觉感知配置
[System.Serializable]
public class VisualPerceptionConfig
{
[Header("视野参数")]
public float sightRange = 20f; // 可视距离
public float fieldOfView = 90f; // 视野角度(度)
public float eyeHeight = 1.7f; // 眼睛高度
public float peripheralVisionAngle = 30f; // 周边视野角度
[Header("检测精度")]
public int rayCountPerFrame = 10; // 每帧射线数量
public float raySpreadAngle = 5f; // 射线扩散角度
public LayerMask detectionMask = -1; // 检测层
public LayerMask obstacleMask = -1; // 障碍物层
[Header("记忆与识别")]
public float memoryDuration = 10f; // 记忆持续时间
public float recognitionTime = 2f; // 完全识别所需时间
public float suspicionThreshold = 0.5f; // 怀疑阈值
}
// 视觉目标信息
public class VisualTarget
{
public GameObject targetObject;
public Vector3 lastKnownPosition;
public Vector3 lastKnownVelocity;
public float lastDetectionTime;
public float awarenessLevel; // 认知程度 0-1
public float confidence; // 置信度 0-1
public VisualTarget(GameObject target, Vector3 position)
{
targetObject = target;
lastKnownPosition = position;
lastDetectionTime = Time.time;
awarenessLevel = 0.1f; // 初始认知
confidence = 0.5f; // 中等置信
}
public void UpdateDetection(Vector3 position, Vector3 velocity, float detectionStrength)
{
lastKnownPosition = position;
lastKnownVelocity = velocity;
lastDetectionTime = Time.time;
// 更新认知程度
awarenessLevel = Mathf.Clamp01(awarenessLevel + detectionStrength * Time.deltaTime);
// 更新置信度(基于连续检测)
confidence = Mathf.Lerp(confidence, 1.0f, 0.1f);
}
public void UpdateMemory(float deltaTime)
{
// 随时间衰减认知程度和置信度
float decayRate = 0.1f;
awarenessLevel = Mathf.Clamp01(awarenessLevel - decayRate * deltaTime);
confidence = Mathf.Clamp01(confidence - decayRate * 0.5f * deltaTime);
}
public bool IsForgotten(float currentTime, float memoryDuration)
{
return currentTime - lastDetectionTime > memoryDuration;
}
}
// 基于碰撞的视觉感知组件
public class CollisionBasedVisualPerception : MonoBehaviour
{
[Header("配置")]
public VisualPerceptionConfig config;
[Header("调试")]
public bool drawDebugRays = true;
public Color fovColor = new Color(0, 1, 0, 0.2f);
public Color detectedColor = Color.red;
public Color suspectedColor = Color.yellow;
// 感知数据
private Dictionary<GameObject, VisualTarget> detectedTargets;
private Queue<RaycastCommand> raycastQueue;
private List<VisualTarget> activeTargets;
// 状态变量
private Vector3 eyePosition;
private float currentFovAngle;
private float detectionTimer;
// 缓存
private RaycastHit[] raycastHits;
private int frameRayCounter;
private void Awake()
{
InitializePerceptionSystem();
}
private void InitializePerceptionSystem()
{
detectedTargets = new Dictionary<GameObject, VisualTarget>();
raycastQueue = new Queue<RaycastCommand>();
activeTargets = new List<VisualTarget>();
raycastHits = new RaycastHit[10];
frameRayCounter = 0;
detectionTimer = 0f;
// 计算眼睛位置
eyePosition = transform.position + Vector3.up * config.eyeHeight;
Debug.Log("基于碰撞的视觉感知系统初始化完成");
}
private void Update()
{
UpdateEyePosition();
UpdatePerceptionCycle();
UpdateTargetMemory();
}
private void UpdateEyePosition()
{
eyePosition = transform.position + Vector3.up * config.eyeHeight;
}
private void UpdatePerceptionCycle()
{
// 每帧处理有限数量的射线以提高性能
frameRayCounter = 0;
// 主视线检测(正前方)
PerformPrimarySightCheck();
// 周边视野检测
PerformPeripheralVisionCheck();
// 处理射线队列
ProcessRaycastQueue();
// 更新活动目标列表
UpdateActiveTargets();
}
private void PerformPrimarySightCheck()
{
Vector3 forward = transform.forward;
// 中心射线(最精确)
PerformRaycast(forward, 1.0f);
// 在视野范围内发射额外射线
float angleStep = config.fieldOfView / (config.rayCountPerFrame - 1);
for (int i = 0; i < config.rayCountPerFrame; i++)
{
float angle = -config.fieldOfView / 2 + i * angleStep;
if (Mathf.Abs(angle) < 0.1f) // 跳过中心射线(已处理)
{
continue;
}
Quaternion rotation = Quaternion.Euler(0, angle, 0);
Vector3 direction = rotation * forward;
// 根据角度调整检测强度
float strengthMultiplier = CalculateAngleStrengthMultiplier(angle);
PerformRaycast(direction, strengthMultiplier);
}
}
private void PerformPeripheralVisionCheck()
{
// 周边视野(灵敏度较低但范围更广)
int peripheralRays = Mathf.Max(1, config.rayCountPerFrame / 3);
float peripheralAngleStep = config.peripheralVisionAngle * 2 / peripheralRays;
for (int i = 0; i < peripheralRays; i++)
{
float angle = -config.peripheralVisionAngle + i * peripheralAngleStep;
Quaternion rotation = Quaternion.Euler(0, angle, 0);
Vector3 direction = rotation * transform.forward;
// 周边视野检测强度较低
PerformRaycast(direction, 0.3f);
}
}
private void PerformRaycast(Vector3 direction, float strengthMultiplier)
{
if (frameRayCounter >= config.rayCountPerFrame)
{
return;
}
Ray ray = new Ray(eyePosition, direction);
float maxDistance = config.sightRange;
// 添加到队列异步处理(或在主线程立即执行)
RaycastCommand command = new RaycastCommand
{
ray = ray,
maxDistance = maxDistance,
layerMask = config.detectionMask | config.obstacleMask,
strengthMultiplier = strengthMultiplier
};
raycastQueue.Enqueue(command);
frameRayCounter++;
// 调试绘制
if (drawDebugRays)
{
Color rayColor = Color.Lerp(Color.green, Color.yellow, strengthMultiplier);
Debug.DrawRay(eyePosition, direction * maxDistance, rayColor, 0.1f);
}
}
private void ProcessRaycastQueue()
{
while (raycastQueue.Count > 0)
{
RaycastCommand command = raycastQueue.Dequeue();
// 执行射线检测
RaycastHit hit;
if (Physics.Raycast(command.ray, out hit, command.maxDistance, command.layerMask))
{
ProcessRaycastHit(hit, command.strengthMultiplier);
}
}
}
private void ProcessRaycastHit(RaycastHit hit, float strengthMultiplier)
{
GameObject hitObject = hit.collider.gameObject;
// 检查是否击中障碍物
if (((1 << hitObject.layer) & config.obstacleMask) != 0)
{
// 被障碍物遮挡,无法看到后面的目标
return;
}
// 检查是否是需要检测的目标
if (((1 << hitObject.layer) & config.detectionMask) == 0)
{
return;
}
// 检查目标是否有可感知组件
IPerceivable perceivable = hitObject.GetComponent<IPerceivable>();
if (perceivable == null)
{
return;
}
// 计算检测强度
float distanceFactor = 1.0f - (hit.distance / config.sightRange);
float angleFactor = CalculateAngleFactor(hit.point);
float visibilityFactor = perceivable.GetVisibility();
float detectionStrength = strengthMultiplier * distanceFactor * angleFactor * visibilityFactor;
// 更新目标信息
UpdateTargetDetection(hitObject, hit.point, detectionStrength);
}
private void UpdateTargetDetection(GameObject target, Vector3 position, float detectionStrength)
{
Vector3 velocity = Vector3.zero;
// 尝试获取目标速度
Rigidbody rb = target.GetComponent<Rigidbody>();
if (rb != null)
{
velocity = rb.velocity;
}
// 更新或创建目标记录
if (detectedTargets.ContainsKey(target))
{
VisualTarget visualTarget = detectedTargets[target];
visualTarget.UpdateDetection(position, velocity, detectionStrength);
}
else
{
VisualTarget newTarget = new VisualTarget(target, position);
newTarget.UpdateDetection(position, velocity, detectionStrength);
detectedTargets[target] = newTarget;
Debug.Log($"新目标检测到: {target.name}, 强度: {detectionStrength:F2}");
}
}
private float CalculateAngleStrengthMultiplier(float angle)
{
// 角度越小,检测强度越高
float normalizedAngle = Mathf.Abs(angle) / (config.fieldOfView / 2);
return Mathf.Clamp01(1.0f - normalizedAngle);
}
private float CalculateAngleFactor(Vector3 targetPosition)
{
Vector3 toTarget = targetPosition - eyePosition;
float angle = Vector3.Angle(transform.forward, toTarget);
// 在视野中心角度因子为1,边缘为0
float normalizedAngle = Mathf.Clamp01(angle / (config.fieldOfView / 2));
return 1.0f - normalizedAngle;
}
private void UpdateActiveTargets()
{
activeTargets.Clear();
foreach (VisualTarget target in detectedTargets.Values)
{
if (target.awarenessLevel > 0.01f) // 忽略几乎不可见的目标
{
activeTargets.Add(target);
}
}
// 按认知程度排序
activeTargets.Sort((a, b) => b.awarenessLevel.CompareTo(a.awarenessLevel));
}
private void UpdateTargetMemory()
{
float currentTime = Time.time;
List<GameObject> targetsToRemove = new List<GameObject>();
foreach (KeyValuePair<GameObject, VisualTarget> entry in detectedTargets)
{
VisualTarget target = entry.Value;
// 更新记忆衰减
target.UpdateMemory(Time.deltaTime);
// 检查是否应忘记该目标
if (target.IsForgotten(currentTime, config.memoryDuration))
{
targetsToRemove.Add(entry.Key);
}
}
// 移除被遗忘的目标
foreach (GameObject targetKey in targetsToRemove)
{
detectedTargets.Remove(targetKey);
Debug.Log($"目标已被遗忘: {targetKey.name}");
}
}
public List<VisualTarget> GetDetectedTargets()
{
return new List<VisualTarget>(activeTargets);
}
public VisualTarget GetMostSignificantTarget()
{
if (activeTargets.Count > 0)
{
return activeTargets[0];
}
return null;
}
public bool CanSeeTarget(GameObject target)
{
if (detectedTargets.ContainsKey(target))
{
VisualTarget visualTarget = detectedTargets[target];
return visualTarget.awarenessLevel > config.suspicionThreshold;
}
return false;
}
public float GetTargetAwareness(GameObject target)
{
if (detectedTargets.ContainsKey(target))
{
return detectedTargets[target].awarenessLevel;
}
return 0f;
}
public Vector3 GetTargetLastKnownPosition(GameObject target)
{
if (detectedTargets.ContainsKey(target))
{
return detectedTargets[target].lastKnownPosition;
}
return Vector3.zero;
}
private void OnDrawGizmosSelected()
{
if (!enabled)
{
return;
}
// 绘制视野锥体
DrawFieldOfViewGizmo();
// 绘制检测到的目标
DrawDetectedTargetsGizmo();
}
private void DrawFieldOfViewGizmo()
{
Gizmos.color = fovColor;
Vector3 forward = transform.forward;
Vector3 leftBoundary = Quaternion.Euler(0, -config.fieldOfView / 2, 0) * forward;
Vector3 rightBoundary = Quaternion.Euler(0, config.fieldOfView / 2, 0) * forward;
Vector3 eyePos = transform.position + Vector3.up * config.eyeHeight;
// 绘制视野锥体
Gizmos.DrawRay(eyePos, leftBoundary * config.sightRange);
Gizmos.DrawRay(eyePos, rightBoundary * config.sightRange);
// 绘制视野弧线
int segments = 20;
float angleStep = config.fieldOfView / segments;
Vector3 prevPoint = eyePos + leftBoundary * config.sightRange;
for (int i = 1; i <= segments; i++)
{
float angle = -config.fieldOfView / 2 + i * angleStep;
Vector3 direction = Quaternion.Euler(0, angle, 0) * forward;
Vector3 point = eyePos + direction * config.sightRange;
Gizmos.DrawLine(prevPoint, point);
prevPoint = point;
}
// 连接起点和终点形成闭合区域
Gizmos.DrawLine(eyePos + leftBoundary * config.sightRange,
eyePos + rightBoundary * config.sightRange);
}
private void DrawDetectedTargetsGizmo()
{
foreach (VisualTarget target in activeTargets)
{
if (target.targetObject == null)
{
continue;
}
// 根据认知程度着色
Color targetColor = Color.Lerp(suspectedColor, detectedColor, target.awarenessLevel);
Gizmos.color = targetColor;
// 绘制目标位置
Vector3 targetPos = target.lastKnownPosition;
Gizmos.DrawSphere(targetPos, 0.5f);
// 绘制连接线
Vector3 eyePos = transform.position + Vector3.up * config.eyeHeight;
Gizmos.DrawLine(eyePos, targetPos);
// 绘制认知程度指示器
Vector3 labelPos = targetPos + Vector3.up * 1.5f;
#if UNITY_EDITOR
UnityEditor.Handles.Label(labelPos,
$"Aware: {target.awarenessLevel:F2}\nConf: {target.confidence:F2}");
#endif
}
}
// 射线命令结构
private struct RaycastCommand
{
public Ray ray;
public float maxDistance;
public LayerMask layerMask;
public float strengthMultiplier;
}
}
// 可感知接口
public interface IPerceivable
{
float GetVisibility(); // 返回0-1的可见性值
float GetAudibility(); // 返回0-1的可听性值
Vector3 GetPerceptionPosition(); // 获取感知位置
GameObject GetGameObject(); // 获取游戏对象
}
// 基础可感知组件
public class BasicPerceivable : MonoBehaviour, IPerceivable
{
[Header("感知属性")]
public float baseVisibility = 1.0f; // 基础可见性
public float baseAudibility = 1.0f; // 基础可听性
public float stealthMultiplier = 1.0f; // 潜行乘数
public Vector3 perceptionOffset = Vector3.up * 0.5f; // 感知偏移
[Header("动态调整")]
public bool isCrouching = false;
public bool isInShadow = false;
public bool isMoving = false;
// 计算当前可见性
public float GetVisibility()
{
float visibility = baseVisibility;
// 应用状态修正
if (isCrouching)
{
visibility *= 0.7f;
}
if (isInShadow)
{
visibility *= 0.5f;
}
if (isMoving)
{
visibility *= 1.2f; // 移动时更容易被发现
}
return visibility * stealthMultiplier;
}
public float GetAudibility()
{
float audibility = baseAudibility;
// 应用状态修正
if (isCrouching)
{
audibility *= 0.5f;
}
if (isMoving)
{
audibility *= 2.0f; // 移动时发出更多声音
}
return audibility;
}
public Vector3 GetPerceptionPosition()
{
return transform.position + perceptionOffset;
}
public GameObject GetGameObject()
{
return gameObject;
}
public void SetStealthMultiplier(float multiplier)
{
stealthMultiplier = Mathf.Clamp01(multiplier);
}
}
}
7.2.3 视觉感知的性能优化
在商业项目中,视觉感知系统的性能至关重要。以下是一些优化策略的实现:
// 优化的视觉感知系统
public class OptimizedVisualPerception : CollisionBasedVisualPerception
{
[System.Serializable]
public class OptimizationSettings
{
public bool useSpatialPartitioning = true;
public float partitionCellSize = 10f;
public int maxTargetsPerFrame = 5;
public bool prioritizeVisibleTargets = true;
public float updateInterval = 0.1f; // 更新间隔(秒)
public bool useAsyncRaycasting = false;
}
public OptimizationSettings optimizationSettings;
// 空间分区
private Dictionary<Vector3Int, List<GameObject>> spatialPartition;
private Bounds partitionBounds;
private float lastUpdateTime;
private List<GameObject> potentialTargets;
protected override void InitializePerceptionSystem()
{
base.InitializePerceptionSystem();
spatialPartition = new Dictionary<Vector3Int, List<GameObject>>();
potentialTargets = new List<GameObject>();
lastUpdateTime = 0f;
// 初始化空间分区
InitializeSpatialPartition();
}
private void InitializeSpatialPartition()
{
// 创建初始分区边界
partitionBounds = new Bounds(transform.position,
Vector3.one * optimizationSettings.partitionCellSize * 10);
// 收集初始潜在目标
UpdatePotentialTargets();
}
private void UpdatePotentialTargets()
{
potentialTargets.Clear();
if (!optimizationSettings.useSpatialPartitioning)
{
// 简单方法:查找所有可感知对象
BasicPerceivable[] allPerceivables = FindObjectsOfType<BasicPerceivable>();
foreach (BasicPerceivable perceivable in allPerceivables)
{
if (perceivable.gameObject != gameObject)
{
potentialTargets.Add(perceivable.gameObject);
}
}
}
else
{
// 使用空间分区查找附近目标
Vector3Int currentCell = GetPartitionCell(transform.position);
Vector3Int[] neighborCells = GetNeighborCells(currentCell);
foreach (Vector3Int cell in neighborCells)
{
if (spatialPartition.ContainsKey(cell))
{
potentialTargets.AddRange(spatialPartition[cell]);
}
}
}
// 限制每帧处理的目标数量
if (potentialTargets.Count > optimizationSettings.maxTargetsPerFrame)
{
// 根据距离排序,优先处理近的目标
potentialTargets.Sort((a, b) =>
Vector3.Distance(transform.position, a.transform.position)
.CompareTo(Vector3.Distance(transform.position, b.transform.position)));
potentialTargets = potentialTargets.GetRange(0, optimizationSettings.maxTargetsPerFrame);
}
}
private Vector3Int GetPartitionCell(Vector3 position)
{
int x = Mathf.FloorToInt(position.x / optimizationSettings.partitionCellSize);
int y = Mathf.FloorToInt(position.y / optimizationSettings.partitionCellSize);
int z = Mathf.FloorToInt(position.z / optimizationSettings.partitionCellSize);
return new Vector3Int(x, y, z);
}
private Vector3Int[] GetNeighborCells(Vector3Int centerCell)
{
List<Vector3Int> neighbors = new List<Vector3Int>();
// 3x3x3的邻居区域
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
for (int z = -1; z <= 1; z++)
{
neighbors.Add(new Vector3Int(
centerCell.x + x,
centerCell.y + y,
centerCell.z + z
));
}
}
}
return neighbors.ToArray();
}
protected override void UpdatePerceptionCycle()
{
// 根据更新间隔控制检测频率
if (Time.time - lastUpdateTime < optimizationSettings.updateInterval)
{
return;
}
lastUpdateTime = Time.time;
// 更新潜在目标
UpdatePotentialTargets();
// 执行感知检测
base.UpdatePerceptionCycle();
}
protected override void PerformPrimarySightCheck()
{
if (optimizationSettings.prioritizeVisibleTargets)
{
// 优先检查已知目标
foreach (VisualTarget target in activeTargets)
{
if (target.targetObject != null && target.awarenessLevel > 0.3f)
{
Vector3 targetPos = target.targetObject.transform.position;
Vector3 direction = (targetPos - eyePosition).normalized;
float distance = Vector3.Distance(eyePosition, targetPos);
if (distance <= config.sightRange)
{
PerformRaycast(direction, 1.0f);
}
}
}
}
// 仍然执行常规视野检测(但使用更少的射线)
base.PerformPrimarySightCheck();
}
protected override void PerformRaycast(Vector3 direction, float strengthMultiplier)
{
if (optimizationSettings.useAsyncRaycasting)
{
// 异步射线检测(需要Unity的Job System)
PerformAsyncRaycast(direction, strengthMultiplier);
}
else
{
base.PerformRaycast(direction, strengthMultiplier);
}
}
private void PerformAsyncRaycast(Vector3 direction, float strengthMultiplier)
{
// 简化的异步检测实现
// 实际项目中应使用Unity的Job System进行优化
Ray ray = new Ray(eyePosition, direction);
// 这里使用协程模拟异步操作
StartCoroutine(AsyncRaycastCoroutine(ray, strengthMultiplier));
}
private System.Collections.IEnumerator AsyncRaycastCoroutine(Ray ray, float strengthMultiplier)
{
// 在实际项目中,这里应该使用Physics.SphereCastNonAlloc等非阻塞方法
RaycastHit hit;
if (Physics.Raycast(ray, out hit, config.sightRange, config.detectionMask | config.obstacleMask))
{
ProcessRaycastHit(hit, strengthMultiplier);
}
yield return null;
}
// 注册和注销可感知对象到空间分区
public void RegisterPerceivable(GameObject perceivable)
{
if (!optimizationSettings.useSpatialPartitioning)
{
return;
}
Vector3Int cell = GetPartitionCell(perceivable.transform.position);
if (!spatialPartition.ContainsKey(cell))
{
spatialPartition[cell] = new List<GameObject>();
}
if (!spatialPartition[cell].Contains(perceivable))
{
spatialPartition[cell].Add(perceivable);
}
}
public void UnregisterPerceivable(GameObject perceivable)
{
if (!optimizationSettings.useSpatialPartitioning)
{
return;
}
Vector3Int cell = GetPartitionCell(perceivable.transform.position);
if (spatialPartition.ContainsKey(cell))
{
spatialPartition[cell].Remove(perceivable);
// 清理空单元格
if (spatialPartition[cell].Count == 0)
{
spatialPartition.Remove(cell);
}
}
}
private void OnDrawGizmosSelected()
{
base.OnDrawGizmosSelected();
if (!optimizationSettings.useSpatialPartitioning)
{
return;
}
// 绘制空间分区
Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.1f);
Vector3Int currentCell = GetPartitionCell(transform.position);
Vector3 cellCenter = new Vector3(
currentCell.x * optimizationSettings.partitionCellSize + optimizationSettings.partitionCellSize / 2,
transform.position.y,
currentCell.z * optimizationSettings.partitionCellSize + optimizationSettings.partitionCellSize / 2
);
Vector3 cellSize = Vector3.one * optimizationSettings.partitionCellSize;
cellSize.y = 10f; // 高度扩展
Gizmos.DrawWireCube(cellCenter, cellSize);
// 绘制潜在目标
Gizmos.color = Color.blue;
foreach (GameObject target in potentialTargets)
{
if (target != null)
{
Gizmos.DrawSphere(target.transform.position, 0.3f);
Gizmos.DrawLine(transform.position, target.transform.position);
}
}
}
}
7.3 基于碰撞检测的听觉感知
听觉感知模拟了生物通过声音感知环境的能力。基于碰撞系统的听觉感知使用球形碰撞检测来模拟声音传播,能够检测到视线之外的目标。
7.3.1 听觉感知理论基础
听觉感知与视觉感知有几个关键区别:
- 全方位性:听觉是360度全方位的,没有方向限制
- 穿透性:声音可以穿透某些障碍物,但会衰减
- 衰减特性:声音强度随距离增加而减弱
- 频率敏感性:不同频率的声音传播特性不同
在游戏AI中,听觉感知通常用于:
- 检测玩家发出的声音(脚步声、枪声、对话)
- 响应环境声音(警报、爆炸、物体掉落)
- 提供线索引导(声音来源方向)
7.3.2 Unity中的听觉感知实现
// 基于碰撞的听觉感知组件
public class CollisionBasedAuditoryPerception : MonoBehaviour
{
[System.Serializable]
public class AuditoryPerceptionConfig
{
[Header("听觉范围")]
public float hearingRange = 30f; // 最大听觉范围
public float hearingThreshold = 0.1f; // 听觉阈值
public LayerMask soundMask = -1; // 声音检测层
public LayerMask soundObstacleMask = -1; // 声音障碍层
[Header("声音传播")]
public float soundAttenuationFactor = 1.0f; // 衰减因子
public float obstaclePenetration = 0.5f; // 障碍穿透度
public bool useFrequencyFiltering = true; // 使用频率过滤
[Header("记忆与定位")]
public float soundMemoryDuration = 5f; // 声音记忆时间
public float directionAccuracy = 0.8f; // 方向准确度
public float minSoundVolume = 0.05f; // 最小音量
}
// 声音事件
public class SoundEvent
{
public Vector3 position;
public float volume; // 原始音量
public float perceivedVolume; // 感知音量
public float frequency; // 频率(Hz)
public float timestamp;
public GameObject source;
public SoundType type;
public SoundEvent(Vector3 pos, float vol, float freq, GameObject src, SoundType t)
{
position = pos;
volume = vol;
perceivedVolume = vol;
frequency = freq;
timestamp = Time.time;
source = src;
type = t;
}
public bool IsExpired(float currentTime, float memoryDuration)
{
return currentTime - timestamp > memoryDuration;
}
}
public enum SoundType
{
Footstep,
Gunshot,
Explosion,
Voice,
Environment,
Other
}
[Header("配置")]
public AuditoryPerceptionConfig config;
[Header("调试")]
public bool drawSoundSpheres = true;
public Color soundSphereColor = new Color(1, 0.5f, 0, 0.1f);
public float debugSphereDuration = 2f;
// 听觉数据
private List<SoundEvent> soundEvents;
private Dictionary<SoundType, float> soundTypeSensitivity;
private SphereCollider hearingCollider;
// 频率过滤
private float[] frequencyBands;
private float[] bandSensitivity;
private void Awake()
{
InitializeAuditorySystem();
}
private void InitializeAuditorySystem()
{
soundEvents = new List<SoundEvent>();
// 添加听觉碰撞器
hearingCollider = gameObject.AddComponent<SphereCollider>();
hearingCollider.isTrigger = true;
hearingCollider.radius = config.hearingRange;
// 初始化声音类型敏感度
InitializeSoundSensitivity();
// 初始化频率带
InitializeFrequencyBands();
Debug.Log("基于碰撞的听觉感知系统初始化完成");
}
private void InitializeSoundSensitivity()
{
soundTypeSensitivity = new Dictionary<SoundType, float>
{
{ SoundType.Footstep, 0.8f },
{ SoundType.Gunshot, 1.0f },
{ SoundType.Explosion, 1.0f },
{ SoundType.Voice, 0.9f },
{ SoundType.Environment, 0.6f },
{ SoundType.Other, 0.5f }
};
}
private void InitializeFrequencyBands()
{
if (!config.useFrequencyFiltering)
{
return;
}
// 定义频率带(人类听觉范围:20Hz - 20kHz)
frequencyBands = new float[]
{
20f, // 低音
250f, // 中低音
1000f, // 中音
4000f, // 中高音
8000f, // 高音
20000f // 超高音
};
// 各频带敏感度(模拟人耳频率响应)
bandSensitivity = new float[]
{
0.7f, // 低音敏感度较低
1.0f, // 中低音最敏感
0.9f, // 中音
0.8f, // 中高音
0.6f, // 高音
0.3f // 超高音敏感度低
};
}
private void Update()
{
UpdateSoundEvents();
ProcessSoundMemory();
}
private void UpdateSoundEvents()
{
// 更新声音事件的感知音量(考虑衰减等)
for (int i = soundEvents.Count - 1; i >= 0; i--)
{
SoundEvent soundEvent = soundEvents[i];
if (soundEvent.source == null)
{
soundEvents.RemoveAt(i);
continue;
}
// 计算距离衰减
float distance = Vector3.Distance(transform.position, soundEvent.position);
float distanceAttenuation = CalculateDistanceAttenuation(distance);
// 计算障碍衰减
float obstacleAttenuation = CalculateObstacleAttenuation(soundEvent.position);
// 计算频率敏感度
float frequencySensitivity = CalculateFrequencySensitivity(soundEvent.frequency);
// 计算类型敏感度
float typeSensitivity = soundTypeSensitivity.ContainsKey(soundEvent.type) ?
soundTypeSensitivity[soundEvent.type] : 0.5f;
// 最终感知音量
soundEvent.perceivedVolume = soundEvent.volume *
distanceAttenuation *
obstacleAttenuation *
frequencySensitivity *
typeSensitivity;
// 如果音量低于阈值,移除该事件
if (soundEvent.perceivedVolume < config.hearingThreshold)
{
soundEvents.RemoveAt(i);
}
}
}
private float CalculateDistanceAttenuation(float distance)
{
// 使用反平方定律:音量与距离的平方成反比
if (distance <= 0.1f)
{
return 1.0f;
}
float maxRange = config.hearingRange;
float normalizedDistance = Mathf.Clamp01(distance / maxRange);
// 应用衰减因子
float attenuation = 1.0f / (1.0f + config.soundAttenuationFactor * normalizedDistance * normalizedDistance);
return attenuation;
}
private float CalculateObstacleAttenuation(Vector3 soundPosition)
{
// 检查声音路径上的障碍物
Vector3 direction = soundPosition - transform.position;
float distance = direction.magnitude;
if (distance <= 0.1f)
{
return 1.0f;
}
RaycastHit[] hits = Physics.RaycastAll(
transform.position,
direction.normalized,
distance,
config.soundObstacleMask
);
if (hits.Length == 0)
{
return 1.0f; // 无障碍物
}
// 计算障碍物总厚度
float totalObstacleThickness = 0f;
foreach (RaycastHit hit in hits)
{
// 估算障碍物厚度(简化处理)
float thickness = EstimateObstacleThickness(hit.collider);
totalObstacleThickness += thickness;
}
// 障碍物越多,衰减越大
float attenuation = Mathf.Exp(-config.obstaclePenetration * totalObstacleThickness);
return Mathf.Clamp01(attenuation);
}
private float EstimateObstacleThickness(Collider collider)
{
// 简化的厚度估计
if (collider is BoxCollider boxCollider)
{
return boxCollider.size.z; // 使用Z轴尺寸作为厚度
}
else if (collider is SphereCollider sphereCollider)
{
return sphereCollider.radius * 2f;
}
else if (collider is CapsuleCollider capsuleCollider)
{
return capsuleCollider.radius * 2f;
}
return 1.0f; // 默认厚度
}
private float CalculateFrequencySensitivity(float frequency)
{
if (!config.useFrequencyFiltering)
{
return 1.0f;
}
// 找到频率所在的频带
int bandIndex = 0;
for (int i = 0; i < frequencyBands.Length - 1; i++)
{
if (frequency >= frequencyBands[i] && frequency < frequencyBands[i + 1])
{
bandIndex = i;
break;
}
}
// 应用频带敏感度
if (bandIndex < bandSensitivity.Length)
{
return bandSensitivity[bandIndex];
}
return 0.5f; // 默认值
}
private void ProcessSoundMemory()
{
float currentTime = Time.time;
// 移除过期的声音事件
soundEvents.RemoveAll(sound =>
sound.IsExpired(currentTime, config.soundMemoryDuration));
}
// 声音检测触发器
private void OnTriggerStay(Collider other)
{
// 检测声音源
SoundEmitter soundEmitter = other.GetComponent<SoundEmitter>();
if (soundEmitter != null && soundEmitter.IsEmitting())
{
ProcessSoundEmission(soundEmitter);
}
}
private void ProcessSoundEmission(SoundEmitter emitter)
{
Vector3 soundPosition = emitter.transform.position;
float volume = emitter.GetCurrentVolume();
float frequency = emitter.GetFrequency();
SoundType type = emitter.GetSoundType();
// 检查是否已有该源的活跃声音事件
SoundEvent existingEvent = null;
foreach (SoundEvent soundEvent in soundEvents)
{
if (soundEvent.source == emitter.gameObject &&
soundEvent.type == type)
{
existingEvent = soundEvent;
break;
}
}
if (existingEvent != null)
{
// 更新现有事件
existingEvent.position = soundPosition;
existingEvent.volume = volume;
existingEvent.timestamp = Time.time;
}
else
{
// 创建新事件
SoundEvent newEvent = new SoundEvent(
soundPosition,
volume,
frequency,
emitter.gameObject,
type
);
soundEvents.Add(newEvent);
Debug.Log($"检测到声音: {type}, 音量: {volume:F2}, 频率: {frequency}Hz");
}
// 调试可视化
if (drawSoundSpheres)
{
StartCoroutine(DrawSoundSphere(soundPosition, volume));
}
}
private System.Collections.IEnumerator DrawSoundSphere(Vector3 position, float volume)
{
float sphereRadius = volume * 5f;
float duration = debugSphereDuration;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float alpha = 1.0f - (elapsed / duration);
Color sphereColor = soundSphereColor;
sphereColor.a = alpha * 0.3f;
DebugExtension.DrawSphere(position, sphereColor, sphereRadius);
yield return null;
}
}
public List<SoundEvent> GetAudibleSounds()
{
return new List<SoundEvent>(soundEvents);
}
public SoundEvent GetLoudestSound()
{
SoundEvent loudest = null;
float maxVolume = 0f;
foreach (SoundEvent soundEvent in soundEvents)
{
if (soundEvent.perceivedVolume > maxVolume)
{
maxVolume = soundEvent.perceivedVolume;
loudest = soundEvent;
}
}
return loudest;
}
public Vector3 EstimateSoundDirection(SoundEvent soundEvent)
{
if (soundEvent == null)
{
return Vector3.zero;
}
Vector3 direction = soundEvent.position - transform.position;
// 添加方向误差模拟
if (config.directionAccuracy < 1.0f)
{
float errorAngle = (1.0f - config.directionAccuracy) * 90f; // 最大90度误差
Quaternion errorRotation = Quaternion.Euler(
0,
Random.Range(-errorAngle, errorAngle),
0
);
direction = errorRotation * direction;
}
return direction.normalized;
}
public bool CanHearSound(SoundType type, float minVolume = 0.1f)
{
foreach (SoundEvent soundEvent in soundEvents)
{
if (soundEvent.type == type && soundEvent.perceivedVolume >= minVolume)
{
return true;
}
}
return false;
}
private void OnDrawGizmosSelected()
{
if (!enabled)
{
return;
}
// 绘制听觉范围
Gizmos.color = soundSphereColor;
Gizmos.DrawWireSphere(transform.position, config.hearingRange);
// 绘制声音事件
foreach (SoundEvent soundEvent in soundEvents)
{
if (soundEvent.source == null)
{
continue;
}
// 根据音量大小着色
Color eventColor = Color.Lerp(Color.yellow, Color.red, soundEvent.perceivedVolume);
Gizmos.color = eventColor;
// 绘制声音位置
Gizmos.DrawSphere(soundEvent.position, 0.3f);
// 绘制到听觉源的方向线
Gizmos.DrawLine(transform.position, soundEvent.position);
// 绘制音量指示器
float sphereRadius = soundEvent.perceivedVolume * 2f;
Gizmos.DrawWireSphere(soundEvent.position, sphereRadius);
}
}
}
// 声音发射器组件
public class SoundEmitter : MonoBehaviour
{
[System.Serializable]
public class SoundEmissionConfig
{
public SoundType soundType = SoundType.Other;
public float baseVolume = 1.0f;
public float frequency = 1000f; // Hz
public float emissionRadius = 10f;
public bool continuousEmission = false;
public float emissionDuration = 1.0f;
public AnimationCurve volumeCurve = AnimationCurve.Linear(0, 1, 1, 0);
}
public SoundEmissionConfig config;
// 发射状态
private bool isEmitting = false;
private float emissionStartTime;
private float currentVolume;
// 物理触发器
private SphereCollider emissionCollider;
private void Start()
{
// 添加发射碰撞器
emissionCollider = gameObject.AddComponent<SphereCollider>();
emissionCollider.isTrigger = true;
emissionCollider.radius = config.emissionRadius;
emissionCollider.enabled = false;
}
public void EmitSound(float volumeMultiplier = 1.0f)
{
if (isEmitting && config.continuousEmission)
{
return; // 已经在连续发射
}
isEmitting = true;
emissionStartTime = Time.time;
emissionCollider.enabled = true;
// 计算当前音量
currentVolume = config.baseVolume * volumeMultiplier;
if (!config.continuousEmission)
{
// 单次发射,设置定时关闭
StartCoroutine(StopEmissionAfterDelay(config.emissionDuration));
}
Debug.Log($"发射声音: {config.soundType}, 音量: {currentVolume:F2}");
}
public void StopEmission()
{
isEmitting = false;
emissionCollider.enabled = false;
currentVolume = 0f;
}
private System.Collections.IEnumerator StopEmissionAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
StopEmission();
}
private void Update()
{
if (!isEmitting)
{
return;
}
if (config.continuousEmission)
{
// 持续发射,音量不变
currentVolume = config.baseVolume;
}
else
{
// 根据曲线更新音量
float elapsed = Time.time - emissionStartTime;
float normalizedTime = Mathf.Clamp01(elapsed / config.emissionDuration);
currentVolume = config.baseVolume * config.volumeCurve.Evaluate(normalizedTime);
// 如果音量接近0,停止发射
if (currentVolume < 0.01f)
{
StopEmission();
}
}
}
public bool IsEmitting()
{
return isEmitting;
}
public float GetCurrentVolume()
{
return currentVolume;
}
public float GetFrequency()
{
return config.frequency;
}
public SoundType GetSoundType()
{
return config.soundType;
}
public float GetEmissionRadius()
{
return config.emissionRadius;
}
private void OnDrawGizmosSelected()
{
if (!isEmitting)
{
return;
}
// 绘制声音发射范围
Gizmos.color = new Color(1, 0.5f, 0, 0.2f);
Gizmos.DrawSphere(transform.position, config.emissionRadius);
// 绘制当前音量指示
Gizmos.color = Color.red;
float volumeSphereRadius = currentVolume;
Gizmos.DrawWireSphere(transform.position, volumeSphereRadius);
}
}
// 调试扩展类
public static class DebugExtension
{
public static void DrawSphere(Vector3 center, Color color, float radius)
{
// 绘制线框球体
int segments = 16;
float angleIncrement = 360f / segments;
// 水平圆
for (int i = 0; i < segments; i++)
{
float angle1 = i * angleIncrement * Mathf.Deg2Rad;
float angle2 = ((i + 1) % segments) * angleIncrement * Mathf.Deg2Rad;
Vector3 point1 = center + new Vector3(
Mathf.Cos(angle1) * radius,
0,
Mathf.Sin(angle1) * radius
);
Vector3 point2 = center + new Vector3(
Mathf.Cos(angle2) * radius,
0,
Mathf.Sin(angle2) * radius
);
Debug.DrawLine(point1, point2, color);
}
// 垂直圆
for (int i = 0; i < segments; i++)
{
float angle1 = i * angleIncrement * Mathf.Deg2Rad;
float angle2 = ((i + 1) % segments) * angleIncrement * Mathf.Deg2Rad;
Vector3 point1 = center + new Vector3(
Mathf.Cos(angle1) * radius,
Mathf.Sin(angle1) * radius,
0
);
Vector3 point2 = center + new Vector3(
Mathf.Cos(angle2) * radius,
Mathf.Sin(angle2) * radius,
0
);
Debug.DrawLine(point1, point2, color);
}
}
}
7.3.3 听觉感知的高级特性
在商业游戏中,听觉感知系统通常需要更复杂的功能,如声音掩蔽、方向辨别和模式识别:
// 高级听觉感知系统
public class AdvancedAuditoryPerception : CollisionBasedAuditoryPerception
{
[System.Serializable]
public class AdvancedAuditorySettings
{
public bool useSoundMasking = true; // 声音掩蔽
public float maskingThreshold = 0.7f; // 掩蔽阈值
public bool useDopplerEffect = true; // 多普勒效应
public bool useSpatialAudio = true; // 空间音频
public float minimumAudibleAngle = 1.0f; // 最小可辨角度(度)
public bool recognizeSoundPatterns = true; // 声音模式识别
}
public AdvancedAuditorySettings advancedSettings;
// 声音掩蔽数据
private Dictionary<SoundType, float> soundMaskingEffects;
private List<SoundPattern> knownSoundPatterns;
// 多普勒效应
private Vector3 lastPosition;
private Vector3 velocity;
// 空间音频
private AudioSource spatialAudioSource;
protected override void InitializeAuditorySystem()
{
base.InitializeAuditorySystem();
// 初始化声音掩蔽效果
InitializeSoundMasking();
// 初始化声音模式
InitializeSoundPatterns();
// 设置空间音频
if (advancedSettings.useSpatialAudio)
{
spatialAudioSource = gameObject.AddComponent<AudioSource>();
spatialAudioSource.spatialBlend = 1.0f; // 完全3D音效
spatialAudioSource.rolloffMode = AudioRolloffMode.Logarithmic;
}
lastPosition = transform.position;
}
private void InitializeSoundMasking()
{
soundMaskingEffects = new Dictionary<SoundType, float>
{
// 不同类型声音对其他声音的掩蔽效果
{ SoundType.Explosion, 0.9f }, // 爆炸声会掩蔽大部分其他声音
{ SoundType.Gunshot, 0.7f }, // 枪声有较强掩蔽效果
{ SoundType.Voice, 0.3f }, // 人声掩蔽效果较弱
{ SoundType.Footstep, 0.1f }, // 脚步声掩蔽效果很弱
{ SoundType.Environment, 0.2f } // 环境声掩蔽效果较弱
};
}
private void InitializeSoundPatterns()
{
if (!advancedSettings.recognizeSoundPatterns)
{
return;
}
knownSoundPatterns = new List<SoundPattern>();
// 预定义一些常见声音模式
knownSoundPatterns.Add(new SoundPattern(
"Walking",
new SoundType[] { SoundType.Footstep, SoundType.Footstep, SoundType.Footstep },
2.0f // 2秒内
));
knownSoundPatterns.Add(new SoundPattern(
"Running",
new SoundType[] { SoundType.Footstep, SoundType.Footstep, SoundType.Footstep, SoundType.Footstep },
1.5f // 1.5秒内
));
knownSoundPatterns.Add(new SoundPattern(
"Gunfight",
new SoundType[] { SoundType.Gunshot, SoundType.Gunshot, SoundType.Gunshot },
3.0f // 3秒内
));
}
private void Update()
{
base.Update();
// 计算速度(用于多普勒效应)
velocity = (transform.position - lastPosition) / Time.deltaTime;
lastPosition = transform.position;
// 应用声音掩蔽
if (advancedSettings.useSoundMasking)
{
ApplySoundMasking();
}
// 识别声音模式
if (advancedSettings.recognizeSoundPatterns)
{
RecognizeSoundPatterns();
}
}
private void ApplySoundMasking()
{
// 找出最响的声音
SoundEvent loudestEvent = GetLoudestSound();
if (loudestEvent == null)
{
return;
}
// 检查该声音是否足够响以产生掩蔽效果
if (loudestEvent.perceivedVolume < advancedSettings.maskingThreshold)
{
return;
}
// 获取该声音类型的掩蔽强度
float maskingStrength = 0f;
if (soundMaskingEffects.ContainsKey(loudestEvent.type))
{
maskingStrength = soundMaskingEffects[loudestEvent.type];
}
if (maskingStrength <= 0f)
{
return;
}
// 应用掩蔽效果到其他声音
foreach (SoundEvent soundEvent in soundEvents)
{
if (soundEvent == loudestEvent)
{
continue; // 不掩蔽自己
}
// 根据掩蔽强度降低其他声音的感知音量
float maskingFactor = 1.0f - maskingStrength *
(loudestEvent.perceivedVolume / 1.0f); // 标准化
soundEvent.perceivedVolume *= Mathf.Clamp01(maskingFactor);
}
}
private void RecognizeSoundPatterns()
{
if (knownSoundPatterns == null || soundEvents.Count < 2)
{
return;
}
float currentTime = Time.time;
float patternTimeWindow = 5.0f; // 分析最近5秒的声音
// 收集时间窗口内的声音事件
List<SoundEvent> recentEvents = new List<SoundEvent>();
foreach (SoundEvent soundEvent in soundEvents)
{
if (currentTime - soundEvent.timestamp <= patternTimeWindow)
{
recentEvents.Add(soundEvent);
}
}
// 按时间排序
recentEvents.Sort((a, b) => a.timestamp.CompareTo(b.timestamp));
// 尝试匹配已知模式
foreach (SoundPattern pattern in knownSoundPatterns)
{
if (pattern.Match(recentEvents))
{
Debug.Log($"识别到声音模式: {pattern.patternName}");
// 触发模式识别响应
OnSoundPatternRecognized(pattern);
}
}
}
protected override float CalculateDistanceAttenuation(float distance)
{
float baseAttenuation = base.CalculateDistanceAttenuation(distance);
if (advancedSettings.useDopplerEffect)
{
// 简化的多普勒效应计算
baseAttenuation *= CalculateDopplerEffect(distance);
}
return baseAttenuation;
}
private float CalculateDopplerEffect(float distance)
{
// 简化版本:根据相对运动调整感知音量
// 实际的多普勒效应会影响频率,这里简化为影响音量
// 计算与声音源的相对速度(需要知道声音源速度,这里简化)
float relativeSpeed = velocity.magnitude;
// 相对速度越大,多普勒效应越明显
float dopplerFactor = 1.0f + relativeSpeed * 0.1f;
return Mathf.Clamp(dopplerFactor, 0.8f, 1.2f);
}
public override Vector3 EstimateSoundDirection(SoundEvent soundEvent)
{
Vector3 baseDirection = base.EstimateSoundDirection(soundEvent);
if (!advancedSettings.useSpatialAudio || spatialAudioSource == null)
{
return baseDirection;
}
// 使用空间音频增强方向感
// 这里可以添加HRTF(头部相关传输函数)模拟等高级功能
// 简化实现:添加轻微的方向修正
// 计算双耳时间差和强度差
Vector3 earOffset = Vector3.right * 0.1f; // 假设耳朵间距20cm
// 左右耳的方向向量
Vector3 leftEarDirection = (soundEvent.position - (transform.position - earOffset)).normalized;
Vector3 rightEarDirection = (soundEvent.position - (transform.position + earOffset)).normalized;
// 计算时间差(简化)
float leftDistance = Vector3.Distance(transform.position - earOffset, soundEvent.position);
float rightDistance = Vector3.Distance(transform.position + earOffset, soundEvent.position);
float timeDifference = (rightDistance - leftDistance) / 343f; // 声速343m/s
// 应用方向修正(简化)
float correctionAngle = timeDifference * 100f; // 放大效果
Quaternion correction = Quaternion.Euler(0, correctionAngle, 0);
return correction * baseDirection;
}
public float GetDirectionAccuracy(Vector3 estimatedDirection, Vector3 actualDirection)
{
float angle = Vector3.Angle(estimatedDirection, actualDirection);
// 如果角度小于最小可辨角度,认为方向准确
if (angle <= advancedSettings.minimumAudibleAngle)
{
return 1.0f;
}
// 角度越大,准确度越低
float maxAngle = 180f;
float accuracy = 1.0f - (angle / maxAngle);
return Mathf.Clamp01(accuracy);
}
private void OnSoundPatternRecognized(SoundPattern pattern)
{
// 处理模式识别事件
// 例如:更新AI状态、触发警报、改变行为等
switch (pattern.patternName)
{
case "Walking":
// 检测到行走,提高警惕
Debug.Log("检测到行走模式,提高警惕等级");
break;
case "Running":
// 检测到奔跑,进入警戒状态
Debug.Log("检测到奔跑模式,进入警戒状态");
break;
case "Gunfight":
// 检测到枪战,进入战斗状态
Debug.Log("检测到枪战模式,进入战斗状态");
break;
}
}
// 声音模式类
public class SoundPattern
{
public string patternName;
public SoundType[] sequence;
public float timeWindow;
private List<SoundEvent> matchedEvents;
public SoundPattern(string name, SoundType[] seq, float window)
{
patternName = name;
sequence = seq;
timeWindow = window;
matchedEvents = new List<SoundEvent>();
}
public bool Match(List<SoundEvent> events)
{
if (events.Count < sequence.Length)
{
return false;
}
// 清空匹配事件
matchedEvents.Clear();
// 尝试匹配序列
int sequenceIndex = 0;
float firstEventTime = 0f;
foreach (SoundEvent soundEvent in events)
{
if (sequenceIndex >= sequence.Length)
{
break; // 已经匹配完整序列
}
if (soundEvent.type == sequence[sequenceIndex])
{
if (sequenceIndex == 0)
{
firstEventTime = soundEvent.timestamp;
}
matchedEvents.Add(soundEvent);
sequenceIndex++;
// 检查时间窗口
if (sequenceIndex == sequence.Length)
{
float totalTime = soundEvent.timestamp - firstEventTime;
return totalTime <= timeWindow;
}
}
}
return false;
}
public List<SoundEvent> GetMatchedEvents()
{
return new List<SoundEvent>(matchedEvents);
}
}
}
由于篇幅限制,本章剩余部分(嗅觉感知、基于网格图的感知系统以及潜行游戏应用)将在后续继续展开。本章已经详细介绍了基于碰撞系统的视觉和听觉感知实现,包括基础实现和高级优化,这些技术可以直接应用于商业游戏开发中,为AI角色提供真实可信的感知能力。
第7章 智能体感知系统构建(续)
7.6 基于网格图的听觉感知
基于网格图的听觉感知提供了一种更为精确和可控的声音传播模拟方式。与基于碰撞系统的方法不同,网格图听觉感知通过将环境划分为网格单元,在每个单元中计算声音强度,实现更真实的声学效果。
7.6.1 网格图听觉感知理论基础
网格图听觉感知的核心是声波传播模拟。声音在环境中传播时会受到多种因素影响:
- 距离衰减:声音强度随距离增加而减弱
- 障碍物吸收:声音穿过障碍物时能量被吸收
- 反射与折射:声音在表面反射,在介质边界折射
- 衍射:声音绕过障碍物边缘传播
- 混响:声音在封闭空间中的多次反射
在网格图实现中,我们使用波前传播算法来模拟声音在网格中的传播。每个网格单元存储声音强度和传播方向信息,通过迭代更新模拟声波传播过程。
7.6.2 Unity中的网格图听觉感知实现
// 基于网格图的听觉感知组件
public class GridBasedAuditoryPerception : MonoBehaviour
{
[System.Serializable]
public class GridAuditoryConfig
{
[Header("网格设置")]
public float gridCellSize = 1.0f; // 网格单元大小
public int gridRadius = 30; // 网格半径(单元数)
public LayerMask soundObstacleMask = -1; // 声音障碍物层
[Header("声学参数")]
public float soundSpeed = 343f; // 声速(米/秒)
public float airAbsorption = 0.01f; // 空气吸收系数
public float obstacleAbsorption = 0.3f; // 障碍物吸收系数
public float reflectionCoefficient = 0.5f; // 反射系数
public float diffractionCoefficient = 0.2f; // 衍射系数
[Header("处理设置")]
public float updateInterval = 0.1f; // 更新间隔
public int maxPropagationSteps = 100; // 最大传播步数
public bool useMultithreading = false; // 使用多线程
}
// 声学网格单元
public class AcousticGridCell
{
public Vector3 worldPosition;
public Vector2Int gridCoordinate;
public float soundIntensity; // 声音强度(0-1)
public Vector3 soundDirection; // 声音传播方向
public float lastUpdateTime;
public bool isObstacle; // 是否为障碍物
public float obstacleThickness; // 障碍物厚度
public float reflectionPotential; // 反射潜力
// 用于波前传播
public float wavefrontTime; // 波前到达时间
public bool wavefrontVisited; // 波前是否访问过
public AcousticGridCell(Vector3 pos, Vector2Int coord)
{
worldPosition = pos;
gridCoordinate = coord;
soundIntensity = 0f;
soundDirection = Vector3.zero;
lastUpdateTime = 0f;
isObstacle = false;
obstacleThickness = 0f;
reflectionPotential = 0f;
wavefrontTime = float.MaxValue;
wavefrontVisited = false;
}
public void ResetWavefront()
{
wavefrontTime = float.MaxValue;
wavefrontVisited = false;
}
public void UpdateSound(float intensity, Vector3 direction, float time)
{
// 使用指数平滑更新声音强度
float alpha = 0.3f;
soundIntensity = Mathf.Lerp(soundIntensity, intensity, alpha);
if (intensity > soundIntensity)
{
soundDirection = Vector3.Lerp(soundDirection, direction, alpha);
}
lastUpdateTime = time;
}
}
// 声音源
public class SoundSource
{
public GameObject sourceObject;
public Vector3 position;
public float intensity;
public float frequency;
public SoundType type;
public float startTime;
public SoundSource(GameObject obj, Vector3 pos, float intens, float freq, SoundType t)
{
sourceObject = obj;
position = pos;
intensity = intens;
frequency = freq;
type = t;
startTime = Time.time;
}
public bool IsActive(float currentTime, float maxDuration = 10f)
{
if (sourceObject == null)
{
return false;
}
// 简单持续时间检查
return currentTime - startTime < maxDuration;
}
}
[Header("配置")]
public GridAuditoryConfig config;
[Header("调试")]
public bool drawSoundGrid = false;
public bool drawSoundPropagation = true;
public Color soundColor = new Color(1, 0.5f, 0, 0.3f);
public Color obstacleColor = Color.gray;
// 网格数据
private Dictionary<Vector2Int, AcousticGridCell> gridCells;
private Vector2Int currentCenterCell;
// 声音源
private List<SoundSource> activeSoundSources;
private Dictionary<SoundType, float> soundTypeAttenuation;
// 波前传播
private PriorityQueue<WavefrontNode> wavefrontQueue;
private List<Vector2Int> propagationPath;
// 更新计时
private float lastUpdateTime;
private bool isProcessingWavefront;
private void Awake()
{
InitializeGridAuditorySystem();
}
private void InitializeGridAuditorySystem()
{
gridCells = new Dictionary<Vector2Int, AcousticGridCell>();
activeSoundSources = new List<SoundSource>();
wavefrontQueue = new PriorityQueue<WavefrontNode>();
propagationPath = new List<Vector2Int>();
// 初始化声音类型衰减
InitializeSoundTypeAttenuation();
// 初始化网格
InitializeAcousticGrid();
lastUpdateTime = Time.time;
isProcessingWavefront = false;
Debug.Log("基于网格图的听觉感知系统初始化完成");
}
private void InitializeSoundTypeAttenuation()
{
soundTypeAttenuation = new Dictionary<SoundType, float>
{
{ SoundType.Footstep, 0.8f },
{ SoundType.Gunshot, 0.3f }, // 枪声衰减较慢
{ SoundType.Explosion, 0.2f }, // 爆炸声衰减最慢
{ SoundType.Voice, 0.9f },
{ SoundType.Environment, 0.7f },
{ SoundType.Other, 0.6f }
};
}
private void InitializeAcousticGrid()
{
Vector3 center = transform.position;
currentCenterCell = WorldToGridCoordinate(center);
int gridDiameter = config.gridRadius * 2 + 1;
for (int x = -config.gridRadius; x <= config.gridRadius; x++)
{
for (int y = -config.gridRadius; y <= config.gridRadius; y++)
{
Vector2Int coord = new Vector2Int(
currentCenterCell.x + x,
currentCenterCell.y + y
);
Vector3 worldPos = GridToWorldPosition(coord);
worldPos.y = center.y;
AcousticGridCell cell = new AcousticGridCell(worldPos, coord);
// 检测障碍物
DetectObstacles(cell);
gridCells[coord] = cell;
}
}
Debug.Log($"声学网格初始化完成,单元数: {gridCells.Count}");
}
private void DetectObstacles(AcousticGridCell cell)
{
Vector3 checkPos = cell.worldPosition + Vector3.up * 0.5f;
float checkSize = config.gridCellSize * 0.45f;
// 检查障碍物
Collider[] obstacles = Physics.OverlapBox(
checkPos,
new Vector3(checkSize, 2f, checkSize),
Quaternion.identity,
config.soundObstacleMask
);
cell.isObstacle = obstacles.Length > 0;
if (cell.isObstacle)
{
// 估算障碍物厚度
cell.obstacleThickness = EstimateObstacleThickness(checkPos, obstacles);
// 计算反射潜力(基于表面法线)
cell.reflectionPotential = CalculateReflectionPotential(checkPos);
}
}
private float EstimateObstacleThickness(Vector3 position, Collider[] obstacles)
{
if (obstacles.Length == 0)
{
return 0f;
}
// 简单估算:使用第一个障碍物的边界大小
Collider obstacle = obstacles[0];
Bounds bounds = obstacle.bounds;
// 返回最小尺寸作为厚度估计
return Mathf.Min(bounds.size.x, bounds.size.y, bounds.size.z);
}
private float CalculateReflectionPotential(Vector3 position)
{
// 发射射线检测表面法线
RaycastHit hit;
if (Physics.Raycast(position + Vector3.up, Vector3.down, out hit, 2f, config.soundObstacleMask))
{
// 表面越平坦,反射潜力越高
float flatness = 1.0f - Mathf.Abs(Vector3.Dot(hit.normal, Vector3.up));
return flatness;
}
return 0.5f; // 默认值
}
private void Update()
{
UpdateGridCenter();
// 按间隔更新声学传播
if (Time.time - lastUpdateTime >= config.updateInterval)
{
ProcessSoundPropagation();
lastUpdateTime = Time.time;
}
UpdateActiveSoundSources();
}
private void UpdateGridCenter()
{
Vector3 currentPos = transform.position;
Vector2Int newCenterCell = WorldToGridCoordinate(currentPos);
if (newCenterCell != currentCenterCell)
{
// 更新网格中心
currentCenterCell = newCenterCell;
UpdateAcousticGridStructure();
}
}
private void UpdateAcousticGridStructure()
{
// 动态更新网格结构
List<Vector2Int> cellsToRemove = new List<Vector2Int>();
foreach (Vector2Int coord in gridCells.Keys)
{
int dx = Mathf.Abs(coord.x - currentCenterCell.x);
int dy = Mathf.Abs(coord.y - currentCenterCell.y);
if (dx > config.gridRadius || dy > config.gridRadius)
{
cellsToRemove.Add(coord);
}
}
// 移除远处的单元格
foreach (Vector2Int coord in cellsToRemove)
{
gridCells.Remove(coord);
}
// 添加新的单元格
for (int dx = -config.gridRadius; dx <= config.gridRadius; dx++)
{
for (int dy = -config.gridRadius; dy <= config.gridRadius; dy++)
{
Vector2Int coord = new Vector2Int(
currentCenterCell.x + dx,
currentCenterCell.y + dy
);
if (!gridCells.ContainsKey(coord))
{
Vector3 worldPos = GridToWorldPosition(coord);
worldPos.y = transform.position.y;
AcousticGridCell cell = new AcousticGridCell(worldPos, coord);
DetectObstacles(cell);
gridCells[coord] = cell;
}
}
}
}
private void ProcessSoundPropagation()
{
if (activeSoundSources.Count == 0)
{
return;
}
// 重置所有单元格的波前状态
foreach (AcousticGridCell cell in gridCells.Values)
{
cell.ResetWavefront();
}
// 为每个声音源执行波前传播
foreach (SoundSource source in activeSoundSources)
{
if (!source.IsActive(Time.time))
{
continue;
}
PropagateSoundWavefront(source);
}
// 清除强度衰减
AttenuateSoundIntensities();
}
private void PropagateSoundWavefront(SoundSource source)
{
// 使用Dijkstra算法进行波前传播
Vector2Int sourceCell = WorldToGridCoordinate(source.position);
if (!gridCells.ContainsKey(sourceCell))
{
return;
}
// 初始化波前队列
wavefrontQueue.Clear();
AcousticGridCell startCell = gridCells[sourceCell];
startCell.wavefrontTime = 0f;
startCell.wavefrontVisited = true;
wavefrontQueue.Enqueue(new WavefrontNode(sourceCell, 0f));
int processedCells = 0;
while (wavefrontQueue.Count > 0 && processedCells < config.maxPropagationSteps)
{
WavefrontNode currentNode = wavefrontQueue.Dequeue();
Vector2Int currentCoord = currentNode.coordinate;
if (!gridCells.ContainsKey(currentCoord))
{
continue;
}
AcousticGridCell currentCell = gridCells[currentCoord];
// 处理邻居单元格
ProcessNeighborCells(currentCoord, currentCell.wavefrontTime, source);
processedCells++;
}
// 清理
wavefrontQueue.Clear();
}
private void ProcessNeighborCells(Vector2Int centerCoord, float arrivalTime, SoundSource source)
{
// 8方向邻居
Vector2Int[] neighborOffsets = {
new Vector2Int(1, 0), // 右
new Vector2Int(-1, 0), // 左
new Vector2Int(0, 1), // 前
new Vector2Int(0, -1), // 后
new Vector2Int(1, 1), // 右前
new Vector2Int(1, -1), // 右后
new Vector2Int(-1, 1), // 左前
new Vector2Int(-1, -1) // 左后
};
foreach (Vector2Int offset in neighborOffsets)
{
Vector2Int neighborCoord = centerCoord + offset;
if (!gridCells.ContainsKey(neighborCoord))
{
continue;
}
AcousticGridCell neighborCell = gridCells[neighborCoord];
if (neighborCell.wavefrontVisited)
{
continue;
}
// 计算传播时间
float distance = Vector2Int.Distance(centerCoord, neighborCoord) * config.gridCellSize;
float propagationTime = distance / config.soundSpeed;
// 计算障碍物衰减
float obstacleAttenuation = CalculateObstacleAttenuationBetween(
gridCells[centerCoord], neighborCell, distance);
// 计算空气吸收
float airAttenuation = Mathf.Exp(-config.airAbsorption * distance);
// 计算总传播时间(包括障碍物延迟)
float totalTime = arrivalTime + propagationTime * (1f + obstacleAttenuation);
// 计算到达时的声音强度
float distanceFromSource = Vector3.Distance(
neighborCell.worldPosition, source.position);
float baseIntensity = source.intensity / (1f + distanceFromSource * distanceFromSource);
float attenuatedIntensity = baseIntensity * airAttenuation * (1f - obstacleAttenuation);
// 应用声音类型特定的衰减
float typeAttenuation = soundTypeAttenuation.ContainsKey(source.type) ?
soundTypeAttenuation[source.type] : 0.7f;
attenuatedIntensity *= typeAttenuation;
// 更新邻居单元格
if (attenuatedIntensity > neighborCell.soundIntensity)
{
Vector3 soundDirection = (neighborCell.worldPosition - source.position).normalized;
neighborCell.UpdateSound(attenuatedIntensity, soundDirection, Time.time);
}
// 标记为已访问并加入队列
neighborCell.wavefrontTime = totalTime;
neighborCell.wavefrontVisited = true;
wavefrontQueue.Enqueue(new WavefrontNode(neighborCoord, totalTime));
}
}
private float CalculateObstacleAttenuationBetween(AcousticGridCell fromCell, AcousticGridCell toCell, float distance)
{
if (!fromCell.isObstacle && !toCell.isObstacle)
{
return 0f; // 无障碍物
}
float attenuation = 0f;
// 如果任一单元格是障碍物,应用吸收
if (fromCell.isObstacle)
{
attenuation += config.obstacleAbsorption * fromCell.obstacleThickness;
}
if (toCell.isObstacle)
{
attenuation += config.obstacleAbsorption * toCell.obstacleThickness;
}
// 如果两个单元格都是障碍物,检查是否连续
if (fromCell.isObstacle && toCell.isObstacle)
{
// 检查视线是否被连续障碍物阻挡
Vector3 rayStart = fromCell.worldPosition + Vector3.up * 0.5f;
Vector3 rayEnd = toCell.worldPosition + Vector3.up * 0.5f;
RaycastHit hit;
if (Physics.Raycast(rayStart, (rayEnd - rayStart).normalized, out hit, distance))
{
if (((1 << hit.collider.gameObject.layer) & config.soundObstacleMask) != 0)
{
attenuation *= 2f; // 连续障碍物加倍衰减
}
}
}
return Mathf.Clamp01(attenuation);
}
private void AttenuateSoundIntensities()
{
// 随时间衰减所有单元格的声音强度
float attenuationRate = 0.1f; // 每帧衰减率
foreach (AcousticGridCell cell in gridCells.Values)
{
if (cell.soundIntensity > 0f)
{
cell.soundIntensity *= (1f - attenuationRate);
if (cell.soundIntensity < 0.01f)
{
cell.soundIntensity = 0f;
cell.soundDirection = Vector3.zero;
}
}
}
}
private void UpdateActiveSoundSources()
{
// 移除不活跃的声音源
activeSoundSources.RemoveAll(source => !source.IsActive(Time.time));
}
public void RegisterSoundSource(SoundSource source)
{
activeSoundSources.Add(source);
}
public void RegisterSoundEvent(GameObject sourceObject, Vector3 position,
float intensity, float frequency, SoundType type)
{
SoundSource source = new SoundSource(
sourceObject, position, intensity, frequency, type);
activeSoundSources.Add(source);
Debug.Log($"注册声音事件: {type}, 强度: {intensity:F2}, 位置: {position}");
}
public float GetSoundIntensityAtPosition(Vector3 position)
{
Vector2Int cellCoord = WorldToGridCoordinate(position);
if (gridCells.ContainsKey(cellCoord))
{
return gridCells[cellCoord].soundIntensity;
}
return 0f;
}
public Vector3 GetSoundDirectionAtPosition(Vector3 position)
{
Vector2Int cellCoord = WorldToGridCoordinate(position);
if (gridCells.ContainsKey(cellCoord))
{
return gridCells[cellCoord].soundDirection;
}
return Vector3.zero;
}
public List<Vector3> GetAudiblePositions(float threshold = 0.1f)
{
List<Vector3> audiblePositions = new List<Vector3>();
foreach (AcousticGridCell cell in gridCells.Values)
{
if (cell.soundIntensity >= threshold)
{
audiblePositions.Add(cell.worldPosition);
}
}
return audiblePositions;
}
public bool CanHearPosition(Vector3 position, float threshold = 0.05f)
{
Vector2Int cellCoord = WorldToGridCoordinate(position);
if (gridCells.ContainsKey(cellCoord))
{
return gridCells[cellCoord].soundIntensity >= threshold;
}
return false;
}
private Vector2Int WorldToGridCoordinate(Vector3 worldPosition)
{
int x = Mathf.FloorToInt(worldPosition.x / config.gridCellSize);
int z = Mathf.FloorToInt(worldPosition.z / config.gridCellSize);
return new Vector2Int(x, z);
}
private Vector3 GridToWorldPosition(Vector2Int gridCoordinate)
{
float x = gridCoordinate.x * config.gridCellSize + config.gridCellSize / 2;
float z = gridCoordinate.y * config.gridCellSize + config.gridCellSize / 2;
return new Vector3(x, 0, z);
}
private void OnDrawGizmosSelected()
{
if (!enabled)
{
return;
}
if (drawSoundGrid)
{
// 绘制声学网格
foreach (AcousticGridCell cell in gridCells.Values)
{
if (cell.isObstacle)
{
Gizmos.color = obstacleColor;
}
else
{
// 根据声音强度着色
float intensity = cell.soundIntensity;
Gizmos.color = Color.Lerp(Color.clear, soundColor, intensity);
}
Vector3 cellCenter = cell.worldPosition;
Vector3 cellSize = Vector3.one * config.gridCellSize * 0.9f;
cellSize.y = 0.1f;
Gizmos.DrawCube(cellCenter, cellSize);
// 绘制声音方向
if (cell.soundIntensity > 0.1f && cell.soundDirection != Vector3.zero)
{
Gizmos.color = Color.red;
Gizmos.DrawRay(cellCenter, cell.soundDirection * config.gridCellSize);
}
}
}
if (drawSoundPropagation)
{
// 绘制声音源
Gizmos.color = Color.yellow;
foreach (SoundSource source in activeSoundSources)
{
if (source.sourceObject != null)
{
Gizmos.DrawSphere(source.position, 0.3f);
// 绘制声音传播范围
float range = Mathf.Sqrt(source.intensity) * 5f;
Gizmos.DrawWireSphere(source.position, range);
}
}
// 绘制当前听到的声音
Vector3 listenerPos = transform.position;
float hearingThreshold = 0.05f;
foreach (AcousticGridCell cell in gridCells.Values)
{
if (cell.soundIntensity >= hearingThreshold)
{
float distance = Vector3.Distance(listenerPos, cell.worldPosition);
if (distance <= config.gridRadius * config.gridCellSize)
{
// 绘制声音强度指示器
float sphereRadius = cell.soundIntensity * 2f;
Color sphereColor = Color.Lerp(Color.yellow, Color.red, cell.soundIntensity);
sphereColor.a = 0.3f;
Gizmos.color = sphereColor;
Gizmos.DrawSphere(cell.worldPosition, sphereRadius);
// 绘制到监听者的连线
Gizmos.color = Color.white;
Gizmos.DrawLine(listenerPos, cell.worldPosition);
}
}
}
}
}
// 优先队列实现
private class PriorityQueue<T> where T : IComparable<T>
{
private List<T> data;
public PriorityQueue()
{
data = new List<T>();
}
public void Enqueue(T item)
{
data.Add(item);
int ci = data.Count - 1;
while (ci > 0)
{
int pi = (ci - 1) / 2;
if (data[ci].CompareTo(data[pi]) >= 0)
{
break;
}
T tmp = data[ci];
data[ci] = data[pi];
data[pi] = tmp;
ci = pi;
}
}
public T Dequeue()
{
if (data.Count == 0)
{
throw new InvalidOperationException("Queue is empty");
}
int li = data.Count - 1;
T frontItem = data[0];
data[0] = data[li];
data.RemoveAt(li);
li--;
int pi = 0;
while (true)
{
int ci = pi * 2 + 1;
if (ci > li)
{
break;
}
int rc = ci + 1;
if (rc <= li && data[rc].CompareTo(data[ci]) < 0)
{
ci = rc;
}
if (data[pi].CompareTo(data[ci]) <= 0)
{
break;
}
T tmp = data[pi];
data[pi] = data[ci];
data[ci] = tmp;
pi = ci;
}
return frontItem;
}
public int Count
{
get { return data.Count; }
}
public void Clear()
{
data.Clear();
}
}
// 波前节点
private class WavefrontNode : IComparable<WavefrontNode>
{
public Vector2Int coordinate;
public float arrivalTime;
public WavefrontNode(Vector2Int coord, float time)
{
coordinate = coord;
arrivalTime = time;
}
public int CompareTo(WavefrontNode other)
{
return arrivalTime.CompareTo(other.arrivalTime);
}
}
}
7.6.3 高级声学特性实现
在实际游戏中,听觉感知通常需要更复杂的效果。以下实现增加了回声、混响和空间定位等高级特性:
// 高级声学感知系统
public class AdvancedAcousticPerception : GridBasedAuditoryPerception
{
[System.Serializable]
public class AdvancedAcousticConfig
{
public bool simulateReverberation = true; // 模拟混响
public float reverbTime = 2.0f; // 混响时间(秒)
public float reverbDensity = 0.7f; // 混响密度
public bool simulateEchoes = true; // 模拟回声
public int maxEchoBounces = 3; // 最大回声反射次数
public float echoDelay = 0.5f; // 回声延迟(秒)
public bool useSpatialCues = true; // 使用空间线索
public float itdScale = 1.0f; // 双耳时间差缩放
public float ildScale = 1.0f; // 双耳强度差缩放
public bool useFrequencyDependentAttenuation = true; // 频率相关衰减
public AnimationCurve frequencyResponse = AnimationCurve.Linear(20, 0, 20000, 1);
}
public AdvancedAcousticConfig advancedConfig;
// 混响数据
private class ReverberationData
{
public Vector3 position;
public float intensity;
public float frequency;
public float startTime;
public List<Vector3> reflectionPoints;
public ReverberationData(Vector3 pos, float intens, float freq)
{
position = pos;
intensity = intens;
frequency = freq;
startTime = Time.time;
reflectionPoints = new List<Vector3>();
}
public float GetCurrentIntensity(float currentTime, float reverbTime)
{
float elapsed = currentTime - startTime;
float decay = Mathf.Exp(-elapsed / reverbTime);
return intensity * decay;
}
}
private List<ReverberationData> reverbSources;
private Dictionary<Vector2Int, float> reverbIntensityMap;
// 回声数据
private class EchoPath
{
public Vector3 source;
public Vector3 listener;
public List<Vector3> bouncePoints;
public float totalDistance;
public float intensity;
public EchoPath(Vector3 src, Vector3 lis, float intens)
{
source = src;
listener = lis;
bouncePoints = new List<Vector3>();
totalDistance = 0f;
intensity = intens;
}
public float GetEchoDelay(float soundSpeed)
{
return totalDistance / soundSpeed;
}
public float GetAttenuatedIntensity(int bounceCount, float reflectionLoss)
{
return intensity * Mathf.Pow(1f - reflectionLoss, bounceCount);
}
}
private List<EchoPath> echoPaths;
protected override void InitializeGridAuditorySystem()
{
base.InitializeGridAuditorySystem();
reverbSources = new List<ReverberationData>();
reverbIntensityMap = new Dictionary<Vector2Int, float>();
echoPaths = new List<EchoPath>();
}
protected override void ProcessSoundPropagation()
{
base.ProcessSoundPropagation();
if (advancedConfig.simulateReverberation)
{
SimulateReverberation();
}
if (advancedConfig.simulateEchoes)
{
SimulateEchoes();
}
if (advancedConfig.useSpatialCues)
{
ApplySpatialCues();
}
}
private void SimulateReverberation()
{
float currentTime = Time.time;
// 更新混响源
for (int i = reverbSources.Count - 1; i >= 0; i--)
{
ReverberationData reverb = reverbSources[i];
float currentIntensity = reverb.GetCurrentIntensity(currentTime, advancedConfig.reverbTime);
if (currentIntensity < 0.01f)
{
reverbSources.RemoveAt(i);
continue;
}
// 在网格中传播混响
PropagateReverberation(reverb, currentIntensity);
}
// 添加新的混响源(从强声音源)
foreach (SoundSource source in activeSoundSources)
{
if (source.intensity > 0.5f && !IsAlreadyReverbSource(source.position))
{
ReverberationData newReverb = new ReverberationData(
source.position,
source.intensity * 0.3f, // 混响强度较低
source.frequency
);
reverbSources.Add(newReverb);
}
}
// 合并混响强度到主网格
MergeReverbIntoGrid();
}
private bool IsAlreadyReverbSource(Vector3 position)
{
float checkRadius = config.gridCellSize * 2f;
foreach (ReverberationData reverb in reverbSources)
{
if (Vector3.Distance(reverb.position, position) <= checkRadius)
{
return true;
}
}
return false;
}
private void PropagateReverberation(ReverberationData reverb, float intensity)
{
Vector2Int sourceCell = WorldToGridCoordinate(reverb.position);
if (!gridCells.ContainsKey(sourceCell))
{
return;
}
// 清除旧的混响强度图
reverbIntensityMap.Clear();
// 使用简化传播(不考虑障碍物)
int spreadRadius = Mathf.FloorToInt(advancedConfig.reverbDensity * 10f);
for (int dx = -spreadRadius; dx <= spreadRadius; dx++)
{
for (int dy = -spreadRadius; dy <= spreadRadius; dy++)
{
Vector2Int cellCoord = new Vector2Int(
sourceCell.x + dx,
sourceCell.y + dy
);
if (!gridCells.ContainsKey(cellCoord))
{
continue;
}
// 计算距离衰减
float distance = Vector2Int.Distance(sourceCell, cellCoord) * config.gridCellSize;
float attenuation = Mathf.Exp(-distance / (advancedConfig.reverbDensity * 5f));
float reverbIntensity = intensity * attenuation;
if (reverbIntensity > 0.01f)
{
reverbIntensityMap[cellCoord] = reverbIntensity;
}
}
}
}
private void MergeReverbIntoGrid()
{
// 将混响强度合并到主网格
foreach (KeyValuePair<Vector2Int, float> entry in reverbIntensityMap)
{
if (gridCells.ContainsKey(entry.Key))
{
AcousticGridCell cell = gridCells[entry.Key];
// 混响增加环境声强度
float totalIntensity = cell.soundIntensity + entry.Value * 0.5f;
cell.UpdateSound(totalIntensity, cell.soundDirection, Time.time);
}
}
}
private void SimulateEchoes()
{
echoPaths.Clear();
// 为每个强声音源计算回声路径
foreach (SoundSource source in activeSoundSources)
{
if (source.intensity > 0.3f)
{
CalculateEchoPaths(source);
}
}
// 将回声效果应用到网格
ApplyEchoesToGrid();
}
private void CalculateEchoPaths(SoundSource source)
{
Vector3 listenerPos = transform.position;
// 寻找可能的反射表面
List<Vector3> reflectionSurfaces = FindReflectionSurfaces(source.position, listenerPos);
foreach (Vector3 surfacePoint in reflectionSurfaces)
{
// 计算一次反射路径
EchoPath path = new EchoPath(source.position, listenerPos, source.intensity * 0.2f);
path.bouncePoints.Add(surfacePoint);
// 计算路径距离
float distanceToSurface = Vector3.Distance(source.position, surfacePoint);
float distanceToListener = Vector3.Distance(surfacePoint, listenerPos);
path.totalDistance = distanceToSurface + distanceToListener;
echoPaths.Add(path);
// 如果允许多次反射,计算更复杂的路径
if (advancedConfig.maxEchoBounces > 1)
{
CalculateMultiBouncePaths(source, listenerPos, surfacePoint);
}
}
}
private List<Vector3> FindReflectionSurfaces(Vector3 sourcePos, Vector3 listenerPos)
{
List<Vector3> surfaces = new List<Vector3>();
// 采样多个方向寻找反射表面
int rayCount = 8;
float maxDistance = 20f;
for (int i = 0; i < rayCount; i++)
{
float angle = i * (360f / rayCount) * Mathf.Deg2Rad;
Vector3 direction = new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle));
RaycastHit hit;
if (Physics.Raycast(sourcePos, direction, out hit, maxDistance, config.soundObstacleMask))
{
// 检查反射点是否朝向听者
Vector3 toListener = listenerPos - hit.point;
float reflectAngle = Vector3.Angle(hit.normal, toListener);
if (reflectAngle < 90f) // 表面朝向听者
{
surfaces.Add(hit.point);
}
}
}
return surfaces;
}
private void CalculateMultiBouncePaths(SoundSource source, Vector3 listenerPos, Vector3 firstBounce)
{
// 计算二次反射路径
Vector3 incomingDir = (firstBounce - source.position).normalized;
Vector3 reflectDir = Vector3.Reflect(incomingDir, GetSurfaceNormal(firstBounce));
RaycastHit secondHit;
if (Physics.Raycast(firstBounce, reflectDir, out secondHit, 20f, config.soundObstacleMask))
{
Vector3 toListener = listenerPos - secondHit.point;
float angle = Vector3.Angle(secondHit.normal, toListener);
if (angle < 90f)
{
EchoPath path = new EchoPath(source.position, listenerPos, source.intensity * 0.1f);
path.bouncePoints.Add(firstBounce);
path.bouncePoints.Add(secondHit.point);
float dist1 = Vector3.Distance(source.position, firstBounce);
float dist2 = Vector3.Distance(firstBounce, secondHit.point);
float dist3 = Vector3.Distance(secondHit.point, listenerPos);
path.totalDistance = dist1 + dist2 + dist3;
echoPaths.Add(path);
}
}
}
private Vector3 GetSurfaceNormal(Vector3 point)
{
// 获取表面法线
RaycastHit hit;
if (Physics.Raycast(point + Vector3.up, Vector3.down, out hit, 2f, config.soundObstacleMask))
{
return hit.normal;
}
return Vector3.up;
}
private void ApplyEchoesToGrid()
{
foreach (EchoPath path in echoPaths)
{
float echoDelay = path.GetEchoDelay(config.soundSpeed);
// 只显示延迟明显的回声
if (echoDelay >= advancedConfig.echoDelay)
{
// 在回声路径的终点(听者附近)添加回声效果
Vector2Int listenerCell = WorldToGridCoordinate(transform.position);
if (gridCells.ContainsKey(listenerCell))
{
AcousticGridCell cell = gridCells[listenerCell];
// 回声强度随延迟增加而减弱
float echoIntensity = path.GetAttenuatedIntensity(
path.bouncePoints.Count,
config.obstacleAbsorption
) * (1f / echoDelay);
float totalIntensity = cell.soundIntensity + echoIntensity;
cell.UpdateSound(totalIntensity, cell.soundDirection, Time.time);
}
}
}
}
private void ApplySpatialCues()
{
Vector3 listenerPos = transform.position;
Vector3 listenerRight = transform.right;
foreach (AcousticGridCell cell in gridCells.Values)
{
if (cell.soundIntensity > 0.05f)
{
// 计算双耳时间差(ITD)
Vector3 toSound = cell.worldPosition - listenerPos;
float distance = toSound.magnitude;
if (distance > 0.1f)
{
Vector3 normalized = toSound.normalized;
// 计算声音在左右耳的相对位置
float rightEarDistance = distance - Vector3.Dot(normalized, listenerRight) * 0.1f;
float leftEarDistance = distance + Vector3.Dot(normalized, listenerRight) * 0.1f;
float itd = (rightEarDistance - leftEarDistance) / config.soundSpeed;
itd *= advancedConfig.itdScale;
// 计算双耳强度差(ILD)
float rightEarIntensity = cell.soundIntensity / (1f + Mathf.Abs(itd) * 10f);
float leftEarIntensity = cell.soundIntensity / (1f + Mathf.Abs(itd) * 10f);
float ild = (rightEarIntensity - leftEarIntensity) / cell.soundIntensity;
ild *= advancedConfig.ildScale;
// 应用空间线索(简化处理)
// 在实际应用中,这会影响声音的立体声平衡
float spatialFactor = 1f + Mathf.Abs(ild) * 0.5f;
cell.UpdateSound(cell.soundIntensity * spatialFactor, cell.soundDirection, Time.time);
}
}
}
}
protected override float CalculateObstacleAttenuationBetween(AcousticGridCell fromCell, AcousticGridCell toCell, float distance)
{
float baseAttenuation = base.CalculateObstacleAttenuationBetween(fromCell, toCell, distance);
if (advancedConfig.useFrequencyDependentAttenuation)
{
// 频率相关的衰减(高频衰减更快)
// 这里使用一个简化的频率响应曲线
float frequencyFactor = advancedConfig.frequencyResponse.Evaluate(1000f); // 以1kHz为参考
baseAttenuation *= (1f + (1f - frequencyFactor) * 2f);
}
return baseAttenuation;
}
public float GetReverbIntensityAtPosition(Vector3 position)
{
Vector2Int cellCoord = WorldToGridCoordinate(position);
if (reverbIntensityMap.ContainsKey(cellCoord))
{
return reverbIntensityMap[cellCoord];
}
return 0f;
}
public List<EchoPath> GetAudibleEchoes(float intensityThreshold = 0.05f)
{
List<EchoPath> audibleEchoes = new List<EchoPath>();
foreach (EchoPath path in echoPaths)
{
float echoIntensity = path.GetAttenuatedIntensity(
path.bouncePoints.Count,
config.obstacleAbsorption
);
if (echoIntensity >= intensityThreshold)
{
audibleEchoes.Add(path);
}
}
return audibleEchoes;
}
protected override void OnDrawGizmosSelected()
{
base.OnDrawGizmosSelected();
if (advancedConfig.simulateEchoes)
{
// 绘制回声路径
Gizmos.color = Color.cyan;
foreach (EchoPath path in echoPaths)
{
if (path.bouncePoints.Count > 0)
{
// 绘制源点到第一个反射点
Gizmos.DrawLine(path.source, path.bouncePoints[0]);
// 绘制反射点之间的路径
for (int i = 0; i < path.bouncePoints.Count - 1; i++)
{
Gizmos.DrawLine(path.bouncePoints[i], path.bouncePoints[i + 1]);
}
// 绘制最后一个反射点到听者
Gizmos.DrawLine(path.bouncePoints[path.bouncePoints.Count - 1], transform.position);
// 绘制反射点
foreach (Vector3 bouncePoint in path.bouncePoints)
{
Gizmos.DrawSphere(bouncePoint, 0.2f);
}
}
}
}
if (advancedConfig.simulateReverberation)
{
// 绘制混响源
Gizmos.color = new Color(1, 0, 1, 0.5f); // 紫色表示混响
foreach (ReverberationData reverb in reverbSources)
{
Gizmos.DrawSphere(reverb.position, 0.4f);
// 绘制混响传播范围
float reverbRadius = advancedConfig.reverbDensity * 10f;
Gizmos.DrawWireSphere(reverb.position, reverbRadius);
}
}
}
}
7.7 基于网格图的嗅觉感知
基于网格图的嗅觉感知模拟气味在环境中的扩散和传播。与听觉类似,气味传播也受到距离、障碍物和环境因素的影响,但通常扩散速度较慢,持续时间更长。
7.7.1 网格图嗅觉感知实现
// 基于网格图的嗅觉感知组件
public class GridBasedOlfactoryPerception : MonoBehaviour
{
[System.Serializable]
public class GridOlfactoryConfig
{
[Header("网格设置")]
public float gridCellSize = 2.0f; // 网格单元大小
public int gridRadius = 25; // 网格半径(单元数)
[Header("气味传播")]
public float diffusionRate = 0.1f; // 扩散速率
public float decayRate = 0.05f; // 衰减速率
public Vector3 windDirection = Vector3.forward; // 风向
public float windStrength = 0.5f; // 风力
public float obstaclePenetration = 0.2f; // 障碍物穿透
[Header("检测参数")]
public float detectionThreshold = 0.01f; // 检测阈值
public float trackingThreshold = 0.1f; // 追踪阈值
public float memoryDecayRate = 0.02f; // 记忆衰减率
}
// 气味网格单元
public class ScentGridCell
{
public Vector3 worldPosition;
public Vector2Int gridCoordinate;
public float scentConcentration; // 气味浓度(0-1)
public Vector3 scentGradient; // 气味梯度(浓度变化方向)
public Dictionary<ScentType, float> scentTypeConcentrations;
public float lastUpdateTime;
public bool isObstacle;
// 用于扩散计算
public float newConcentration;
public ScentGridCell(Vector3 pos, Vector2Int coord)
{
worldPosition = pos;
gridCoordinate = coord;
scentConcentration = 0f;
scentGradient = Vector3.zero;
scentTypeConcentrations = new Dictionary<ScentType, float>();
lastUpdateTime = Time.time;
isObstacle = false;
newConcentration = 0f;
}
public void AddScent(ScentType type, float amount)
{
if (!scentTypeConcentrations.ContainsKey(type))
{
scentTypeConcentrations[type] = 0f;
}
scentTypeConcentrations[type] += amount;
UpdateTotalConcentration();
lastUpdateTime = Time.time;
}
public void UpdateTotalConcentration()
{
scentConcentration = 0f;
foreach (float concentration in scentTypeConcentrations.Values)
{
scentConcentration = Mathf.Max(scentConcentration, concentration);
}
}
public float GetScentConcentration(ScentType type)
{
if (scentTypeConcentrations.ContainsKey(type))
{
return scentTypeConcentrations[type];
}
return 0f;
}
public ScentType GetDominantScentType()
{
ScentType dominantType = ScentType.Other;
float maxConcentration = 0f;
foreach (KeyValuePair<ScentType, float> entry in scentTypeConcentrations)
{
if (entry.Value > maxConcentration)
{
maxConcentration = entry.Value;
dominantType = entry.Key;
}
}
return dominantType;
}
}
// 气味源
public class ScentSource
{
public GameObject sourceObject;
public Vector3 position;
public ScentType scentType;
public float emissionRate;
public float startTime;
public bool isActive;
public ScentSource(GameObject obj, Vector3 pos, ScentType type, float rate)
{
sourceObject = obj;
position = pos;
scentType = type;
emissionRate = rate;
startTime = Time.time;
isActive = true;
}
public void UpdatePosition(Vector3 newPos)
{
position = newPos;
}
public float GetCurrentEmission(float timeSinceLastUpdate)
{
if (!isActive)
{
return 0f;
}
return emissionRate * timeSinceLastUpdate;
}
}
[Header("配置")]
public GridOlfactoryConfig config;
[Header("调试")]
public bool drawScentGrid = true;
public bool drawScentGradients = false;
public Color scentColor = new Color(0.5f, 0.25f, 0, 0.3f);
public float maxVisualizedConcentration = 0.5f;
// 网格数据
private Dictionary<Vector2Int, ScentGridCell> gridCells;
private Vector2Int currentCenterCell;
// 气味源
private List<ScentSource> activeScentSources;
private Dictionary<ScentType, Color> scentTypeColors;
// 扩散计算
private float lastDiffusionTime;
private bool isProcessingDiffusion;
// 记忆追踪
private Dictionary<ScentType, List<Vector3>> scentTrails;
private void Awake()
{
InitializeGridOlfactorySystem();
}
private void InitializeGridOlfactorySystem()
{
gridCells = new Dictionary<Vector2Int, ScentGridCell>();
activeScentSources = new List<ScentSource>();
scentTrails = new Dictionary<ScentType, List<Vector3>>();
// 初始化气味类型颜色
InitializeScentTypeColors();
// 初始化网格
InitializeScentGrid();
lastDiffusionTime = Time.time;
isProcessingDiffusion = false;
Debug.Log("基于网格图的嗅觉感知系统初始化完成");
}
private void InitializeScentTypeColors()
{
scentTypeColors = new Dictionary<ScentType, Color>
{
{ ScentType.Player, Color.red },
{ ScentType.Blood, new Color(0.8f, 0, 0, 1) },
{ ScentType.Food, Color.green },
{ ScentType.Enemy, Color.yellow },
{ ScentType.Other, Color.gray }
};
}
private void InitializeScentGrid()
{
Vector3 center = transform.position;
currentCenterCell = WorldToGridCoordinate(center);
int gridDiameter = config.gridRadius * 2 + 1;
for (int x = -config.gridRadius; x <= config.gridRadius; x++)
{
for (int y = -config.gridRadius; y <= config.gridRadius; y++)
{
Vector2Int coord = new Vector2Int(
currentCenterCell.x + x,
currentCenterCell.y + y
);
Vector3 worldPos = GridToWorldPosition(coord);
worldPos.y = center.y;
ScentGridCell cell = new ScentGridCell(worldPos, coord);
// 检测障碍物
DetectScentObstacles(cell);
gridCells[coord] = cell;
}
}
}
private void DetectScentObstacles(ScentGridCell cell)
{
// 简化的障碍物检测
Vector3 checkPos = cell.worldPosition + Vector3.up * 0.5f;
float checkRadius = config.gridCellSize * 0.4f;
Collider[] obstacles = Physics.OverlapSphere(checkPos, checkRadius);
cell.isObstacle = false;
foreach (Collider obstacle in obstacles)
{
// 检查是否为固体障碍物
if (obstacle.gameObject.layer == LayerMask.NameToLayer("Obstacle"))
{
cell.isObstacle = true;
break;
}
}
}
private void Update()
{
UpdateGridCenter();
// 处理气味扩散
if (Time.time - lastDiffusionTime >= 0.5f) // 每0.5秒更新一次扩散
{
ProcessScentDiffusion();
lastDiffusionTime = Time.time;
}
// 持续更新气味源
UpdateScentSources();
// 更新气味梯度
UpdateScentGradients();
// 衰减现有气味
DecayExistingScents();
// 更新记忆追踪
UpdateScentTrails();
}
private void UpdateGridCenter()
{
Vector3 currentPos = transform.position;
Vector2Int newCenterCell = WorldToGridCoordinate(currentPos);
if (newCenterCell != currentCenterCell)
{
currentCenterCell = newCenterCell;
UpdateScentGridStructure();
}
}
private void UpdateScentGridStructure()
{
// 动态更新网格(类似听觉网格的实现)
// 这里省略详细实现以节省篇幅
// 实际实现应与听觉网格类似
}
private void ProcessScentDiffusion()
{
if (isProcessingDiffusion)
{
return;
}
isProcessingDiffusion = true;
// 第一步:准备新浓度值
foreach (ScentGridCell cell in gridCells.Values)
{
cell.newConcentration = cell.scentConcentration;
}
// 第二步:应用扩散
foreach (KeyValuePair<Vector2Int, ScentGridCell> entry in gridCells)
{
Vector2Int coord = entry.Key;
ScentGridCell cell = entry.Value;
if (cell.isObstacle)
{
continue;
}
// 扩散到邻居单元格
DiffuseToNeighbors(coord, cell);
// 应用风效
ApplyWindEffect(coord, cell);
}
// 第三步:更新浓度值
foreach (ScentGridCell cell in gridCells.Values)
{
cell.scentConcentration = cell.newConcentration;
cell.UpdateTotalConcentration();
}
isProcessingDiffusion = false;
}
private void DiffuseToNeighbors(Vector2Int centerCoord, ScentGridCell centerCell)
{
Vector2Int[] neighborOffsets = {
new Vector2Int(1, 0),
new Vector2Int(-1, 0),
new Vector2Int(0, 1),
new Vector2Int(0, -1)
};
float diffusionAmount = centerCell.scentConcentration * config.diffusionRate * 0.25f;
foreach (Vector2Int offset in neighborOffsets)
{
Vector2Int neighborCoord = centerCoord + offset;
if (gridCells.ContainsKey(neighborCoord))
{
ScentGridCell neighborCell = gridCells[neighborCoord];
if (neighborCell.isObstacle)
{
// 障碍物穿透
diffusionAmount *= config.obstaclePenetration;
}
// 从中心单元格扩散到邻居
neighborCell.newConcentration += diffusionAmount;
centerCell.newConcentration -= diffusionAmount;
// 同时扩散各类型气味
foreach (ScentType type in centerCell.scentTypeConcentrations.Keys)
{
float typeConcentration = centerCell.GetScentConcentration(type);
float typeDiffusion = typeConcentration * config.diffusionRate * 0.25f;
neighborCell.AddScent(type, typeDiffusion);
// 从源中减去(简化处理)
centerCell.scentTypeConcentrations[type] -= typeDiffusion;
}
}
}
}
private void ApplyWindEffect(Vector2Int coord, ScentGridCell cell)
{
if (config.windStrength <= 0f)
{
return;
}
// 计算风的方向在网格上的投影
Vector3 windDir = config.windDirection.normalized;
Vector2Int windOffset = new Vector2Int(
Mathf.RoundToInt(windDir.x),
Mathf.RoundToInt(windDir.z)
);
Vector2Int downwindCoord = coord + windOffset;
if (gridCells.ContainsKey(downwindCoord))
{
ScentGridCell downwindCell = gridCells[downwindCoord];
if (!downwindCell.isObstacle)
{
// 风向传播
float windTransfer = cell.scentConcentration * config.windStrength * 0.3f;
downwindCell.newConcentration += windTransfer;
cell.newConcentration -= windTransfer;
}
}
}
private void UpdateScentSources()
{
float currentTime = Time.time;
foreach (ScentSource source in activeScentSources)
{
if (!source.isActive)
{
continue;
}
// 更新位置(如果源移动了)
if (source.sourceObject != null)
{
source.UpdatePosition(source.sourceObject.transform.position);
}
// 计算自上次更新以来的排放量
float timeSinceLastUpdate = currentTime - source.startTime;
float emission = source.GetCurrentEmission(timeSinceLastUpdate);
if (emission > 0f)
{
// 在源位置添加气味
Vector2Int sourceCell = WorldToGridCoordinate(source.position);
if (gridCells.ContainsKey(sourceCell))
{
ScentGridCell cell = gridCells[sourceCell];
cell.AddScent(source.scentType, emission);
// 记录气味踪迹
RecordScentTrail(source.scentType, source.position);
}
source.startTime = currentTime;
}
}
}
private void UpdateScentGradients()
{
// 计算每个单元格的气味梯度(浓度变化最快的方向)
foreach (KeyValuePair<Vector2Int, ScentGridCell> entry in gridCells)
{
Vector2Int coord = entry.Key;
ScentGridCell cell = entry.Value;
if (cell.scentConcentration < config.detectionThreshold)
{
cell.scentGradient = Vector3.zero;
continue;
}
// 检查四个主要方向的浓度差
Vector3 gradient = Vector3.zero;
float maxIncrease = 0f;
Vector2Int[] neighborOffsets = {
new Vector2Int(1, 0),
new Vector2Int(-1, 0),
new Vector2Int(0, 1),
new Vector2Int(0, -1)
};
Vector3[] neighborDirections = {
Vector3.right,
Vector3.left,
Vector3.forward,
Vector3.back
};
for (int i = 0; i < neighborOffsets.Length; i++)
{
Vector2Int neighborCoord = coord + neighborOffsets[i];
if (gridCells.ContainsKey(neighborCoord))
{
ScentGridCell neighbor = gridCells[neighborCoord];
if (!neighbor.isObstacle)
{
float concentrationDiff = neighbor.scentConcentration - cell.scentConcentration;
if (concentrationDiff > maxIncrease)
{
maxIncrease = concentrationDiff;
gradient = neighborDirections[i] * concentrationDiff;
}
}
}
}
cell.scentGradient = gradient.normalized;
}
}
private void DecayExistingScents()
{
// 所有气味随时间衰减
float decayAmount = config.decayRate * Time.deltaTime;
foreach (ScentGridCell cell in gridCells.Values)
{
if (cell.scentConcentration > 0f)
{
cell.scentConcentration = Mathf.Max(0f, cell.scentConcentration - decayAmount);
// 衰减各类型气味
List<ScentType> typesToRemove = new List<ScentType>();
foreach (KeyValuePair<ScentType, float> entry in cell.scentTypeConcentrations)
{
float newConcentration = Mathf.Max(0f, entry.Value - decayAmount);
if (newConcentration <= 0.001f)
{
typesToRemove.Add(entry.Key);
}
else
{
cell.scentTypeConcentrations[entry.Key] = newConcentration;
}
}
// 移除完全衰减的气味类型
foreach (ScentType type in typesToRemove)
{
cell.scentTypeConcentrations.Remove(type);
}
cell.UpdateTotalConcentration();
}
}
}
private void RecordScentTrail(ScentType type, Vector3 position)
{
if (!scentTrails.ContainsKey(type))
{
scentTrails[type] = new List<Vector3>();
}
List<Vector3> trail = scentTrails[type];
// 检查是否与最后一个点足够远
if (trail.Count == 0 || Vector3.Distance(trail[trail.Count - 1], position) > config.gridCellSize * 2f)
{
trail.Add(position);
// 限制踪迹长度
if (trail.Count > 50)
{
trail.RemoveAt(0);
}
}
}
private void UpdateScentTrails()
{
// 随时间衰减记忆中的气味踪迹
float memoryDecay = config.memoryDecayRate * Time.deltaTime;
foreach (ScentType type in scentTrails.Keys)
{
// 这里可以添加更复杂的记忆衰减逻辑
// 目前只是简单的时间衰减
}
}
public void RegisterScentSource(ScentSource source)
{
activeScentSources.Add(source);
}
public void RegisterScentEmission(GameObject sourceObject, Vector3 position,
ScentType type, float intensity)
{
// 查找或创建气味源
ScentSource existingSource = null;
foreach (ScentSource source in activeScentSources)
{
if (source.sourceObject == sourceObject && source.scentType == type)
{
existingSource = source;
break;
}
}
if (existingSource != null)
{
existingSource.UpdatePosition(position);
existingSource.emissionRate = intensity;
existingSource.isActive = true;
existingSource.startTime = Time.time;
}
else
{
ScentSource newSource = new ScentSource(sourceObject, position, type, intensity);
activeScentSources.Add(newSource);
}
}
public void StopScentEmission(GameObject sourceObject, ScentType type)
{
foreach (ScentSource source in activeScentSources)
{
if (source.sourceObject == sourceObject && source.scentType == type)
{
source.isActive = false;
break;
}
}
}
public float GetScentConcentrationAtPosition(Vector3 position, ScentType type)
{
Vector2Int cellCoord = WorldToGridCoordinate(position);
if (gridCells.ContainsKey(cellCoord))
{
return gridCells[cellCoord].GetScentConcentration(type);
}
return 0f;
}
public Vector3 GetScentGradientAtPosition(Vector3 position)
{
Vector2Int cellCoord = WorldToGridCoordinate(position);
if (gridCells.ContainsKey(cellCoord))
{
return gridCells[cellCoord].scentGradient;
}
return Vector3.zero;
}
public ScentType DetectScentAtPosition(Vector3 position, float threshold = 0.05f)
{
Vector2Int cellCoord = WorldToGridCoordinate(position);
if (gridCells.ContainsKey(cellCoord))
{
ScentGridCell cell = gridCells[cellCoord];
if (cell.scentConcentration >= threshold)
{
return cell.GetDominantScentType();
}
}
return ScentType.Other;
}
public bool CanTrackScent(ScentType type, Vector3 fromPosition, out Vector3 trackingDirection)
{
trackingDirection = Vector3.zero;
Vector2Int startCell = WorldToGridCoordinate(fromPosition);
if (!gridCells.ContainsKey(startCell))
{
return false;
}
// 寻找最近的强气味源
Vector2Int strongestCell = startCell;
float strongestConcentration = 0f;
float maxSearchDistance = config.gridRadius * config.gridCellSize;
foreach (KeyValuePair<Vector2Int, ScentGridCell> entry in gridCells)
{
ScentGridCell cell = entry.Value;
float concentration = cell.GetScentConcentration(type);
if (concentration > strongestConcentration)
{
float distance = Vector3.Distance(fromPosition, cell.worldPosition);
if (distance <= maxSearchDistance)
{
strongestConcentration = concentration;
strongestCell = entry.Key;
}
}
}
if (strongestConcentration >= config.trackingThreshold)
{
trackingDirection = (gridCells[strongestCell].worldPosition - fromPosition).normalized;
return true;
}
return false;
}
public List<Vector3> GetScentTrail(ScentType type)
{
if (scentTrails.ContainsKey(type))
{
return new List<Vector3>(scentTrails[type]);
}
return new List<Vector3>();
}
private Vector2Int WorldToGridCoordinate(Vector3 worldPosition)
{
int x = Mathf.FloorToInt(worldPosition.x / config.gridCellSize);
int z = Mathf.FloorToInt(worldPosition.z / config.gridCellSize);
return new Vector2Int(x, z);
}
private Vector3 GridToWorldPosition(Vector2Int gridCoordinate)
{
float x = gridCoordinate.x * config.gridCellSize + config.gridCellSize / 2;
float z = gridCoordinate.y * config.gridCellSize + config.gridCellSize / 2;
return new Vector3(x, 0, z);
}
private void OnDrawGizmosSelected()
{
if (!enabled)
{
return;
}
if (drawScentGrid)
{
// 绘制气味网格
foreach (ScentGridCell cell in gridCells.Values)
{
if (cell.scentConcentration > 0.01f)
{
// 根据浓度和类型着色
ScentType dominantType = cell.GetDominantScentType();
Color baseColor = scentTypeColors.ContainsKey(dominantType) ?
scentTypeColors[dominantType] : Color.gray;
float concentrationFactor = Mathf.Clamp01(cell.scentConcentration / maxVisualizedConcentration);
Color cellColor = Color.Lerp(Color.clear, baseColor, concentrationFactor);
cellColor.a = concentrationFactor * 0.5f;
Gizmos.color = cellColor;
Vector3 cellCenter = cell.worldPosition;
Vector3 cellSize = Vector3.one * config.gridCellSize * 0.8f;
cellSize.y = 0.1f;
Gizmos.DrawCube(cellCenter, cellSize);
// 如果是障碍物,特殊标记
if (cell.isObstacle)
{
Gizmos.color = Color.black;
Gizmos.DrawWireCube(cellCenter, cellSize);
}
}
}
// 绘制气味源
Gizmos.color = Color.red;
foreach (ScentSource source in activeScentSources)
{
if (source.isActive)
{
Gizmos.DrawSphere(source.position, 0.3f);
// 绘制排放范围
float emissionRadius = Mathf.Sqrt(source.emissionRate) * 2f;
Gizmos.DrawWireSphere(source.position, emissionRadius);
}
}
}
if (drawScentGradients)
{
// 绘制气味梯度
Gizmos.color = Color.green;
foreach (ScentGridCell cell in gridCells.Values)
{
if (cell.scentConcentration > config.detectionThreshold && cell.scentGradient != Vector3.zero)
{
Vector3 start = cell.worldPosition;
Vector3 end = start + cell.scentGradient * config.gridCellSize * 2f;
Gizmos.DrawLine(start, end);
Gizmos.DrawSphere(end, 0.1f);
}
}
// 绘制气味踪迹
foreach (KeyValuePair<ScentType, List<Vector3>> entry in scentTrails)
{
Color trailColor = scentTypeColors.ContainsKey(entry.Key) ?
scentTypeColors[entry.Key] : Color.gray;
Gizmos.color = trailColor;
List<Vector3> trail = entry.Value;
for (int i = 0; i < trail.Count - 1; i++)
{
Gizmos.DrawLine(trail[i], trail[i + 1]);
Gizmos.DrawSphere(trail[i], 0.1f);
}
if (trail.Count > 0)
{
Gizmos.DrawSphere(trail[trail.Count - 1], 0.2f);
}
}
}
// 绘制当前检测状态
Vector3 listenerPos = transform.position;
ScentType detectedScent = DetectScentAtPosition(listenerPos);
if (detectedScent != ScentType.Other)
{
Color detectionColor = scentTypeColors.ContainsKey(detectedScent) ?
scentTypeColors[detectedScent] : Color.white;
Gizmos.color = detectionColor;
float concentration = GetScentConcentrationAtPosition(listenerPos, detectedScent);
float sphereRadius = concentration * 3f;
Gizmos.DrawWireSphere(listenerPos, sphereRadius);
// 绘制追踪方向(如果有)
Vector3 trackingDirection;
if (CanTrackScent(detectedScent, listenerPos, out trackingDirection))
{
Gizmos.DrawLine(listenerPos, listenerPos + trackingDirection * 5f);
Gizmos.DrawSphere(listenerPos + trackingDirection * 5f, 0.5f);
}
}
}
}
7.8 潜行游戏中的感知系统应用
在潜行类游戏中,感知系统是游戏体验的核心。玩家需要利用环境、声音和视觉限制来躲避敌人的检测,而敌人则需要使用多种感官来发现玩家。本节将展示如何将前面介绍的感知系统集成到一个完整的潜行游戏AI中。
7.8.1 潜行AI感知控制器
// 潜行AI感知控制器
public class StealthAIPerceptionController : MonoBehaviour
{
[System.Serializable]
public class StealthPerceptionProfile
{
[Header("视觉设置")]
public float visionRange = 20f;
public float fovAngle = 90f;
public float peripheralVisionAngle = 120f;
public float visionUpdateRate = 0.2f;
[Header("听觉设置")]
public float hearingRange = 15f;
public float hearingSensitivity = 1.0f;
public bool canHearThroughWalls = false;
[Header("嗅觉设置")]
public float smellRange = 10f;
public bool useSmell = true;
public float smellSensitivity = 0.5f;
[Header("警觉系统")]
public float maxAlertness = 100f;
public float alertDecayRate = 5f; // 每秒衰减
public float suspicionThreshold = 30f;
public float alertThreshold = 70f;
public float detectionThreshold = 90f;
[Header("行为参数")]
public float investigationRadius = 10f;
public float maxInvestigationTime = 10f;
public float patrolSpeed = 2f;
public float chaseSpeed = 5f;
}
public enum AIState
{
Idle,
Patrol,
Suspicious,
Alert,
Investigating,
Chasing,
Searching,
Returning
}
public enum AlertnessLevel
{
Calm, // 0-30
Suspicious, // 30-70
Alert, // 70-90
Detected // 90-100
}
[Header("配置文件")]
public StealthPerceptionProfile profile;
[Header("感知组件")]
public CollisionBasedVisualPerception visualPerception;
public CollisionBasedAuditoryPerception auditoryPerception;
public CollisionBasedOlfactoryPerception olfactoryPerception;
[Header("调试")]
public bool showPerceptionDebug = true;
public bool showAlertnessDebug = true;
// AI状态
private AIState currentState;
private AlertnessLevel currentAlertnessLevel;
private float currentAlertness;
// 目标追踪
private GameObject currentTarget;
private Vector3 lastKnownTargetPosition;
private float lastDetectionTime;
private Vector3 investigationPosition;
private float investigationStartTime;
// 路径点
private List<Vector3> patrolWaypoints;
private int currentWaypointIndex;
private NavMeshAgent navAgent;
// 记忆系统
private class PerceptionMemory
{
public Vector3 position;
public float timestamp;
public PerceptionType type;
public float confidence;
public PerceptionMemory(Vector3 pos, PerceptionType t, float conf)
{
position = pos;
timestamp = Time.time;
type = t;
confidence = conf;
}
public bool IsRelevant(float currentTime, float memoryDuration)
{
return currentTime - timestamp <= memoryDuration;
}
}
public enum PerceptionType
{
Visual,
Auditory,
Olfactory
}
private List<PerceptionMemory> perceptionMemories;
private float memoryDuration = 30f;
private void Awake()
{
InitializeStealthAI();
}
private void InitializeStealthAI()
{
// 获取或添加感知组件
if (visualPerception == null)
{
visualPerception = gameObject.AddComponent<CollisionBasedVisualPerception>();
ConfigureVisualPerception();
}
if (auditoryPerception == null)
{
auditoryPerception = gameObject.AddComponent<CollisionBasedAuditoryPerception>();
ConfigureAuditoryPerception();
}
if (olfactoryPerception == null && profile.useSmell)
{
olfactoryPerception = gameObject.AddComponent<CollisionBasedOlfactoryPerception>();
ConfigureOlfactoryPerception();
}
// 初始化导航代理
navAgent = GetComponent<NavMeshAgent>();
if (navAgent == null)
{
navAgent = gameObject.AddComponent<NavMeshAgent>();
}
// 初始化状态
currentState = AIState.Patrol;
currentAlertness = 0f;
currentAlertnessLevel = AlertnessLevel.Calm;
// 初始化记忆系统
perceptionMemories = new List<PerceptionMemory>();
// 初始化巡逻路径点
InitializePatrolWaypoints();
Debug.Log("潜行AI感知控制器初始化完成");
}
private void ConfigureVisualPerception()
{
visualPerception.config.sightRange = profile.visionRange;
visualPerception.config.fieldOfView = profile.fovAngle;
visualPerception.config.peripheralVisionAngle = profile.peripheralVisionAngle;
visualPerception.config.rayCountPerFrame = 20;
}
private void ConfigureAuditoryPerception()
{
auditoryPerception.config.hearingRange = profile.hearingRange;
auditoryPerception.config.soundAttenuationFactor = 1.0f / profile.hearingSensitivity;
}
private void ConfigureOlfactoryPerception()
{
olfactoryPerception.config.smellRange = profile.smellRange;
olfactoryPerception.config.smellThreshold = 0.01f * (1.0f / profile.smellSensitivity);
}
private void InitializePatrolWaypoints()
{
patrolWaypoints = new List<Vector3>();
// 生成简单的方形巡逻路径
Vector3 startPos = transform.position;
float patrolRadius = 10f;
patrolWaypoints.Add(startPos + new Vector3(patrolRadius, 0, 0));
patrolWaypoints.Add(startPos + new Vector3(patrolRadius, 0, patrolRadius));
patrolWaypoints.Add(startPos + new Vector3(0, 0, patrolRadius));
patrolWaypoints.Add(startPos + new Vector3(-patrolRadius, 0, 0));
patrolWaypoints.Add(startPos + new Vector3(0, 0, -patrolRadius));
currentWaypointIndex = 0;
}
private void Update()
{
UpdatePerceptions();
UpdateAlertness();
UpdateAIState();
ExecuteStateBehavior();
UpdatePerceptionMemory();
// 调试显示
if (showPerceptionDebug)
{
DebugDrawPerceptionInfo();
}
}
private void UpdatePerceptions()
{
// 视觉检测
List<VisualTarget> visibleTargets = visualPerception.GetDetectedTargets();
foreach (VisualTarget target in visibleTargets)
{
if (target.targetObject != null && target.targetObject.CompareTag("Player"))
{
ProcessVisualDetection(target);
}
}
// 听觉检测
List<SoundEvent> audibleSounds = auditoryPerception.GetAudibleSounds();
foreach (SoundEvent sound in audibleSounds)
{
if (sound.source != null && sound.source.CompareTag("Player"))
{
ProcessAuditoryDetection(sound);
}
}
// 嗅觉检测
if (profile.useSmell && olfactoryPerception != null)
{
List<ScentInfo> detectedScents = olfactoryPerception.GetDetectedScents();
foreach (ScentInfo scent in detectedScents)
{
if (scent.source != null && scent.source.CompareTag("Player"))
{
ProcessOlfactoryDetection(scent);
}
}
}
}
private void ProcessVisualDetection(VisualTarget target)
{
float detectionStrength = target.awarenessLevel;
if (detectionStrength > 0.3f) // 有意义的检测
{
// 增加警觉值
float alertnessIncrease = detectionStrength * 20f;
currentAlertness = Mathf.Min(profile.maxAlertness, currentAlertness + alertnessIncrease);
// 更新目标信息
currentTarget = target.targetObject;
lastKnownTargetPosition = target.lastKnownPosition;
lastDetectionTime = Time.time;
// 记录记忆
AddPerceptionMemory(lastKnownTargetPosition, PerceptionType.Visual, detectionStrength);
Debug.Log($"视觉检测到玩家,警觉值: {currentAlertness:F1}");
}
}
private void ProcessAuditoryDetection(SoundEvent sound)
{
float detectionStrength = sound.perceivedVolume;
if (detectionStrength > auditoryPerception.config.hearingThreshold)
{
// 增加警觉值
float alertnessIncrease = detectionStrength * 15f;
currentAlertness = Mathf.Min(profile.maxAlertness, currentAlertness + alertnessIncrease);
// 记录声音位置
lastKnownTargetPosition = sound.position;
lastDetectionTime = Time.time;
// 记录记忆
AddPerceptionMemory(sound.position, PerceptionType.Auditory, detectionStrength);
Debug.Log($"听觉检测到玩家,强度: {detectionStrength:F2}, 警觉值: {currentAlertness:F1}");
}
}
private void ProcessOlfactoryDetection(ScentInfo scent)
{
float detectionStrength = scent.perceivedStrength;
if (detectionStrength > olfactoryPerception.config.smellThreshold)
{
// 增加警觉值
float alertnessIncrease = detectionStrength * 10f;
currentAlertness = Mathf.Min(profile.maxAlertness, currentAlertness + alertnessIncrease);
// 记录气味位置
lastKnownTargetPosition = scent.position;
lastDetectionTime = Time.time;
// 记录记忆
AddPerceptionMemory(scent.position, PerceptionType.Olfactory, detectionStrength);
Debug.Log($"嗅觉检测到玩家,强度: {detectionStrength:F2}, 警觉值: {currentAlertness:F1}");
}
}
private void UpdateAlertness()
{
// 随时间衰减警觉值
float decayAmount = profile.alertDecayRate * Time.deltaTime;
currentAlertness = Mathf.Max(0f, currentAlertness - decayAmount);
// 更新警觉等级
UpdateAlertnessLevel();
}
private void UpdateAlertnessLevel()
{
AlertnessLevel newLevel;
if (currentAlertness >= profile.detectionThreshold)
{
newLevel = AlertnessLevel.Detected;
}
else if (currentAlertness >= profile.alertThreshold)
{
newLevel = AlertnessLevel.Alert;
}
else if (currentAlertness >= profile.suspicionThreshold)
{
newLevel = AlertnessLevel.Suspicious;
}
else
{
newLevel = AlertnessLevel.Calm;
}
// 检查等级变化
if (newLevel != currentAlertnessLevel)
{
currentAlertnessLevel = newLevel;
OnAlertnessLevelChanged(newLevel);
}
}
private void OnAlertnessLevelChanged(AlertnessLevel newLevel)
{
Debug.Log($"警觉等级变化: {currentAlertnessLevel} -> {newLevel}");
switch (newLevel)
{
case AlertnessLevel.Calm:
// 恢复平静状态
if (currentState == AIState.Investigating || currentState == AIState.Searching)
{
TransitionToState(AIState.Returning);
}
break;
case AlertnessLevel.Suspicious:
// 变得可疑
if (currentState == AIState.Patrol || currentState == AIState.Idle)
{
TransitionToState(AIState.Suspicious);
}
break;
case AlertnessLevel.Alert:
// 进入警戒状态
TransitionToState(AIState.Alert);
break;
case AlertnessLevel.Detected:
// 检测到玩家
TransitionToState(AIState.Chasing);
break;
}
}
private void UpdateAIState()
{
// 根据警觉等级和当前状态更新AI状态
switch (currentState)
{
case AIState.Patrol:
// 巡逻状态逻辑
if (currentAlertnessLevel >= AlertnessLevel.Suspicious)
{
// 找到最近的可疑位置
Vector3 suspiciousPosition = FindMostSuspiciousPosition();
if (suspiciousPosition != Vector3.zero)
{
investigationPosition = suspiciousPosition;
TransitionToState(AIState.Investigating);
}
}
break;
case AIState.Investigating:
// 调查状态逻辑
if (currentAlertnessLevel >= AlertnessLevel.Alert)
{
TransitionToState(AIState.Alert);
}
else if (currentAlertnessLevel == AlertnessLevel.Calm)
{
TransitionToState(AIState.Returning);
}
else if (Time.time - investigationStartTime > profile.maxInvestigationTime)
{
// 调查超时
TransitionToState(AIState.Searching);
}
break;
case AIState.Chasing:
// 追逐状态逻辑
if (currentAlertnessLevel <= AlertnessLevel.Suspicious)
{
// 丢失目标
TransitionToState(AIState.Searching);
}
break;
case AIState.Searching:
// 搜索状态逻辑
if (currentAlertnessLevel == AlertnessLevel.Calm)
{
TransitionToState(AIState.Returning);
}
else if (currentAlertnessLevel >= AlertnessLevel.Alert)
{
// 重新发现线索
TransitionToState(AIState.Investigating);
}
break;
}
}
private void ExecuteStateBehavior()
{
switch (currentState)
{
case AIState.Patrol:
ExecutePatrolBehavior();
break;
case AIState.Suspicious:
ExecuteSuspiciousBehavior();
break;
case AIState.Investigating:
ExecuteInvestigatingBehavior();
break;
case AIState.Alert:
ExecuteAlertBehavior();
break;
case AIState.Chasing:
ExecuteChasingBehavior();
break;
case AIState.Searching:
ExecuteSearchingBehavior();
break;
case AIState.Returning:
ExecuteReturningBehavior();
break;
}
}
private void ExecutePatrolBehavior()
{
// 设置巡逻速度
navAgent.speed = profile.patrolSpeed;
// 移动到下一个路径点
if (patrolWaypoints.Count > 0)
{
Vector3 currentWaypoint = patrolWaypoints[currentWaypointIndex];
navAgent.SetDestination(currentWaypoint);
// 检查是否到达路径点
float distanceToWaypoint = Vector3.Distance(transform.position, currentWaypoint);
if (distanceToWaypoint < 1f)
{
currentWaypointIndex = (currentWaypointIndex + 1) % patrolWaypoints.Count;
}
}
// 随机观看方向变化
if (Random.value < 0.01f) // 每帧1%概率
{
LookAtRandomDirection();
}
}
private void ExecuteSuspiciousBehavior()
{
// 减慢速度,四处查看
navAgent.speed = profile.patrolSpeed * 0.7f;
// 寻找可疑位置
Vector3 suspiciousPosition = FindMostSuspiciousPosition();
if (suspiciousPosition != Vector3.zero)
{
navAgent.SetDestination(suspiciousPosition);
}
else
{
// 没有明确目标,原地警戒
StopMoving();
LookAround();
}
}
private void ExecuteInvestigatingBehavior()
{
navAgent.speed = profile.patrolSpeed * 0.8f;
if (investigationPosition != Vector3.zero)
{
// 前往调查位置
navAgent.SetDestination(investigationPosition);
float distance = Vector3.Distance(transform.position, investigationPosition);
if (distance < 2f)
{
// 到达调查位置,开始搜索
StopMoving();
SearchArea(investigationPosition, profile.investigationRadius);
// 检查是否发现新线索
CheckForNewClues();
}
}
}
private void ExecuteAlertBehavior()
{
navAgent.speed = profile.patrolSpeed * 1.2f;
// 快速移动到最后一个已知位置
if (lastKnownTargetPosition != Vector3.zero)
{
navAgent.SetDestination(lastKnownTargetPosition);
float distance = Vector3.Distance(transform.position, lastKnownTargetPosition);
if (distance < 3f)
{
// 到达位置,开始详细搜索
TransitionToState(AIState.Searching);
}
}
}
private void ExecuteChasingBehavior()
{
navAgent.speed = profile.chaseSpeed;
if (currentTarget != null)
{
// 追逐当前目标
Vector3 targetPosition = currentTarget.transform.position;
navAgent.SetDestination(targetPosition);
// 检查是否抓住目标
float distance = Vector3.Distance(transform.position, targetPosition);
if (distance < 2f)
{
Debug.Log("抓住玩家!");
// 这里可以触发游戏结束或其他事件
}
}
else if (lastKnownTargetPosition != Vector3.zero)
{
// 追逐最后已知位置
navAgent.SetDestination(lastKnownTargetPosition);
}
}
private void ExecuteSearchingBehavior()
{
navAgent.speed = profile.patrolSpeed * 0.9f;
// 在区域中搜索
if (lastKnownTargetPosition != Vector3.zero)
{
SearchArea(lastKnownTargetPosition, profile.investigationRadius * 1.5f);
}
}
private void ExecuteReturningBehavior()
{
navAgent.speed = profile.patrolSpeed;
// 返回巡逻路线
Vector3 nearestWaypoint = FindNearestPatrolWaypoint();
navAgent.SetDestination(nearestWaypoint);
float distance = Vector3.Distance(transform.position, nearestWaypoint);
if (distance < 1f)
{
TransitionToState(AIState.Patrol);
}
}
private void LookAtRandomDirection()
{
float randomAngle = Random.Range(-45f, 45f);
Quaternion rotation = Quaternion.Euler(0, randomAngle, 0);
transform.rotation = Quaternion.Slerp(transform.rotation, rotation * transform.rotation, 0.1f);
}
private void LookAround()
{
// 缓慢环顾四周
float lookAngle = Mathf.Sin(Time.time * 0.5f) * 60f;
Quaternion targetRotation = Quaternion.Euler(0, lookAngle, 0);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime);
}
private void StopMoving()
{
navAgent.isStopped = true;
}
private void ResumeMoving()
{
navAgent.isStopped = false;
}
private void SearchArea(Vector3 center, float radius)
{
// 在区域内随机移动点
if (Random.value < 0.02f) // 每帧2%概率
{
Vector2 randomCircle = Random.insideUnitCircle * radius;
Vector3 searchPoint = center + new Vector3(randomCircle.x, 0, randomCircle.y);
// 确保点在NavMesh上
NavMeshHit hit;
if (NavMesh.SamplePosition(searchPoint, out hit, radius, NavMesh.AllAreas))
{
navAgent.SetDestination(hit.position);
}
}
}
private void CheckForNewClues()
{
// 检查感知记忆中的新线索
float currentTime = Time.time;
float recentTimeWindow = 5f; // 最近5秒
foreach (PerceptionMemory memory in perceptionMemories)
{
if (memory.IsRelevant(currentTime, recentTimeWindow))
{
float distance = Vector3.Distance(transform.position, memory.position);
if (distance < profile.investigationRadius)
{
// 发现新线索,更新调查位置
investigationPosition = memory.position;
investigationStartTime = Time.time;
break;
}
}
}
}
private Vector3 FindMostSuspiciousPosition()
{
// 从感知记忆中找到最可疑的位置
float maxSuspicion = 0f;
Vector3 mostSuspiciousPosition = Vector3.zero;
float currentTime = Time.time;
foreach (PerceptionMemory memory in perceptionMemories)
{
if (memory.IsRelevant(currentTime, memoryDuration))
{
// 计算可疑度(基于置信度和时间)
float timeFactor = 1.0f - ((currentTime - memory.timestamp) / memoryDuration);
float suspicion = memory.confidence * timeFactor;
if (suspicion > maxSuspicion)
{
maxSuspicion = suspicion;
mostSuspiciousPosition = memory.position;
}
}
}
return mostSuspiciousPosition;
}
private Vector3 FindNearestPatrolWaypoint()
{
Vector3 nearest = patrolWaypoints[0];
float nearestDistance = Vector3.Distance(transform.position, nearest);
for (int i = 1; i < patrolWaypoints.Count; i++)
{
float distance = Vector3.Distance(transform.position, patrolWaypoints[i]);
if (distance < nearestDistance)
{
nearestDistance = distance;
nearest = patrolWaypoints[i];
}
}
return nearest;
}
private void AddPerceptionMemory(Vector3 position, PerceptionType type, float confidence)
{
PerceptionMemory memory = new PerceptionMemory(position, type, confidence);
perceptionMemories.Add(memory);
// 限制记忆数量
if (perceptionMemories.Count > 20)
{
perceptionMemories.RemoveAt(0);
}
}
private void UpdatePerceptionMemory()
{
// 移除过时的记忆
float currentTime = Time.time;
perceptionMemories.RemoveAll(memory => !memory.IsRelevant(currentTime, memoryDuration));
}
private void TransitionToState(AIState newState)
{
Debug.Log($"AI状态变化: {currentState} -> {newState}");
AIState previousState = currentState;
currentState = newState;
// 状态进入处理
switch (newState)
{
case AIState.Investigating:
investigationStartTime = Time.time;
break;
case AIState.Chasing:
// 开始追逐时可能会有特殊行为
break;
}
// 状态退出处理
switch (previousState)
{
case AIState.Chasing:
// 停止追逐时重置一些变量
break;
}
}
private void DebugDrawPerceptionInfo()
{
// 绘制视野
if (visualPerception != null)
{
Vector3 eyePos = transform.position + Vector3.up * visualPerception.config.eyeHeight;
Vector3 forward = transform.forward;
Vector3 leftBoundary = Quaternion.Euler(0, -visualPerception.config.fieldOfView / 2, 0) * forward;
Vector3 rightBoundary = Quaternion.Euler(0, visualPerception.config.fieldOfView / 2, 0) * forward;
Debug.DrawRay(eyePos, leftBoundary * visualPerception.config.sightRange, Color.green);
Debug.DrawRay(eyePos, rightBoundary * visualPerception.config.sightRange, Color.green);
}
// 绘制当前目标
if (currentTarget != null)
{
Debug.DrawLine(transform.position, currentTarget.transform.position, Color.red);
}
// 绘制最后已知位置
if (lastKnownTargetPosition != Vector3.zero)
{
DebugExtension.DrawCircle(lastKnownTargetPosition, Color.yellow, 1f);
Debug.DrawLine(transform.position, lastKnownTargetPosition, Color.yellow);
}
// 绘制调查位置
if (investigationPosition != Vector3.zero)
{
DebugExtension.DrawCircle(investigationPosition, Color.blue, 0.5f);
Debug.DrawLine(transform.position, investigationPosition, Color.blue);
}
}
private void OnDrawGizmosSelected()
{
if (!showAlertnessDebug)
{
return;
}
// 绘制警觉值指示器
Vector3 position = transform.position + Vector3.up * 2f;
// 绘制警觉条
float alertnessBarWidth = 2f;
float alertnessBarHeight = 0.2f;
float currentWidth = (currentAlertness / profile.maxAlertness) * alertnessBarWidth;
// 背景条(灰色)
Gizmos.color = Color.gray;
Gizmos.DrawCube(position + new Vector3(alertnessBarWidth / 2, 0, 0),
new Vector3(alertnessBarWidth, alertnessBarHeight, 0.1f));
// 当前警觉值(颜色根据等级变化)
Color alertnessColor;
switch (currentAlertnessLevel)
{
case AlertnessLevel.Calm:
alertnessColor = Color.green;
break;
case AlertnessLevel.Suspicious:
alertnessColor = Color.yellow;
break;
case AlertnessLevel.Alert:
alertnessColor = new Color(1, 0.5f, 0); // 橙色
break;
case AlertnessLevel.Detected:
alertnessColor = Color.red;
break;
default:
alertnessColor = Color.white;
break;
}
Gizmos.color = alertnessColor;
Gizmos.DrawCube(position + new Vector3(currentWidth / 2, 0, 0),
new Vector3(currentWidth, alertnessBarHeight, 0.1f));
// 绘制阈值标记
DrawThresholdMarker(position, profile.suspicionThreshold / profile.maxAlertness,
alertnessBarWidth, "Suspicion", Color.yellow);
DrawThresholdMarker(position, profile.alertThreshold / profile.maxAlertness,
alertnessBarWidth, "Alert", new Color(1, 0.5f, 0));
DrawThresholdMarker(position, profile.detectionThreshold / profile.maxAlertness,
alertnessBarWidth, "Detected", Color.red);
// 绘制当前状态标签
#if UNITY_EDITOR
UnityEditor.Handles.Label(position + Vector3.up * 0.3f,
$"State: {currentState}\nAlertness: {currentAlertness:F1}/{profile.maxAlertness}");
#endif
// 绘制巡逻路径点
if (patrolWaypoints != null && patrolWaypoints.Count > 0)
{
Gizmos.color = Color.cyan;
for (int i = 0; i < patrolWaypoints.Count; i++)
{
Gizmos.DrawSphere(patrolWaypoints[i], 0.3f);
int nextIndex = (i + 1) % patrolWaypoints.Count;
Gizmos.DrawLine(patrolWaypoints[i], patrolWaypoints[nextIndex]);
#if UNITY_EDITOR
UnityEditor.Handles.Label(patrolWaypoints[i] + Vector3.up * 0.2f,
$"WP {i}");
#endif
}
}
// 绘制感知记忆点
Gizmos.color = new Color(1, 0, 1, 0.5f); // 紫色半透明
foreach (PerceptionMemory memory in perceptionMemories)
{
Gizmos.DrawSphere(memory.position, 0.2f);
// 根据记忆类型绘制不同标记
switch (memory.type)
{
case PerceptionType.Visual:
Gizmos.color = Color.red;
break;
case PerceptionType.Auditory:
Gizmos.color = Color.yellow;
break;
case PerceptionType.Olfactory:
Gizmos.color = Color.green;
break;
}
Gizmos.DrawWireSphere(memory.position, 0.3f);
Gizmos.color = new Color(1, 0, 1, 0.5f); // 恢复默认颜色
}
}
private void DrawThresholdMarker(Vector3 basePosition, float normalizedThreshold,
float totalWidth, string label, Color color)
{
float xPos = normalizedThreshold * totalWidth;
Vector3 markerPos = basePosition + new Vector3(xPos, 0.15f, 0);
Gizmos.color = color;
Gizmos.DrawLine(markerPos + Vector3.up * 0.05f, markerPos - Vector3.up * 0.05f);
#if UNITY_EDITOR
UnityEditor.Handles.Label(markerPos + Vector3.up * 0.1f, label);
#endif
}
}
// 调试扩展类(续)
public static class DebugExtension
{
public static void DrawCircle(Vector3 center, Color color, float radius)
{
int segments = 20;
float angleIncrement = 360f / segments;
Vector3 prevPoint = center + new Vector3(radius, 0, 0);
for (int i = 1; i <= segments; i++)
{
float angle = i * angleIncrement * Mathf.Deg2Rad;
Vector3 point = center + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius);
Debug.DrawLine(prevPoint, point, color);
prevPoint = point;
}
}
}
7.8.2 玩家潜行控制器
为了让潜行游戏完整,我们还需要一个玩家控制器,用于管理玩家产生的感知信号:
// 玩家潜行控制器
public class PlayerStealthController : MonoBehaviour, IPerceivable
{
[System.Serializable]
public class StealthAttributes
{
[Header("移动参数")]
public float walkSpeed = 3f;
public float runSpeed = 6f;
public float crouchSpeed = 1.5f;
public float jumpHeight = 1f;
[Header("潜行参数")]
public float baseVisibility = 1.0f;
public float baseAudibility = 1.0f;
public float crouchVisibilityMultiplier = 0.6f;
public float crouchAudibilityMultiplier = 0.4f;
public float runVisibilityMultiplier = 1.2f;
public float runAudibilityMultiplier = 2.0f;
public float inShadowVisibilityMultiplier = 0.3f;
[Header("声音发射")]
public float footstepVolume = 0.3f;
public float jumpVolume = 0.7f;
public float landVolume = 0.5f;
public float voiceVolume = 0.8f;
}
public StealthAttributes attributes;
[Header("组件")]
public CharacterController characterController;
public SoundEmitter footstepEmitter;
public SoundEmitter voiceEmitter;
public ScentEmitter playerScentEmitter;
[Header("状态")]
public bool isCrouching = false;
public bool isRunning = false;
public bool isGrounded = true;
public bool isInShadow = false;
// 移动变量
private Vector3 moveDirection;
private float verticalVelocity;
private float gravity = -9.81f;
// 感知接口实现
private float currentVisibility;
private float currentAudibility;
// 输入缓存
private float horizontalInput;
private float verticalInput;
private bool jumpInput;
private bool crouchInput;
private bool runInput;
private void Start()
{
// 获取或添加组件
if (characterController == null)
{
characterController = GetComponent<CharacterController>();
}
InitializePerceptionEmitters();
UpdatePerceptionAttributes();
}
private void InitializePerceptionEmitters()
{
// 创建脚步声发射器
if (footstepEmitter == null)
{
GameObject footstepObj = new GameObject("FootstepEmitter");
footstepObj.transform.parent = transform;
footstepObj.transform.localPosition = Vector3.zero;
footstepEmitter = footstepObj.AddComponent<SoundEmitter>();
footstepEmitter.config.soundType = SoundType.Footstep;
footstepEmitter.config.baseVolume = attributes.footstepVolume;
footstepEmitter.config.emissionRadius = 10f;
footstepEmitter.config.continuousEmission = false;
}
// 创建语音发射器
if (voiceEmitter == null)
{
GameObject voiceObj = new GameObject("VoiceEmitter");
voiceObj.transform.parent = transform;
voiceObj.transform.localPosition = Vector3.up * 1.5f;
voiceEmitter = voiceObj.AddComponent<SoundEmitter>();
voiceEmitter.config.soundType = SoundType.Voice;
voiceEmitter.config.baseVolume = attributes.voiceVolume;
voiceEmitter.config.emissionRadius = 15f;
}
// 创建气味发射器
if (playerScentEmitter == null)
{
GameObject scentObj = new GameObject("ScentEmitter");
scentObj.transform.parent = transform;
scentObj.transform.localPosition = Vector3.zero;
playerScentEmitter = scentObj.AddComponent<ScentEmitter>();
playerScentEmitter.config.scentType = ScentType.Player;
playerScentEmitter.config.baseStrength = 0.5f;
playerScentEmitter.config.emissionRadius = 8f;
playerScentEmitter.config.continuousEmission = true;
playerScentEmitter.EmitSound(1.0f);
}
}
private void Update()
{
GetPlayerInput();
UpdateMovement();
UpdatePerceptionAttributes();
UpdatePerceptionEmitters();
CheckEnvironmentalFactors();
}
private void GetPlayerInput()
{
horizontalInput = Input.GetAxis("Horizontal");
verticalInput = Input.GetAxis("Vertical");
jumpInput = Input.GetButtonDown("Jump");
crouchInput = Input.GetKey(KeyCode.LeftControl);
runInput = Input.GetKey(KeyCode.LeftShift);
}
private void UpdateMovement()
{
isGrounded = characterController.isGrounded;
// 处理蹲伏
isCrouching = crouchInput;
characterController.height = isCrouching ? 1f : 2f;
// 处理奔跑
isRunning = runInput && !isCrouching && verticalInput > 0;
// 计算移动速度
float currentSpeed;
if (isCrouching)
{
currentSpeed = attributes.crouchSpeed;
}
else if (isRunning)
{
currentSpeed = attributes.runSpeed;
}
else
{
currentSpeed = attributes.walkSpeed;
}
// 计算移动方向
Vector3 forward = Camera.main.transform.forward;
Vector3 right = Camera.main.transform.right;
forward.y = 0;
right.y = 0;
forward.Normalize();
right.Normalize();
moveDirection = forward * verticalInput + right * horizontalInput;
if (moveDirection.magnitude > 1f)
{
moveDirection.Normalize();
}
// 应用移动
characterController.Move(moveDirection * currentSpeed * Time.deltaTime);
// 处理跳跃
if (isGrounded)
{
verticalVelocity = -0.5f; // 小的向下力确保接地
if (jumpInput && !isCrouching)
{
verticalVelocity = Mathf.Sqrt(attributes.jumpHeight * -2f * gravity);
EmitJumpSound();
}
}
else
{
verticalVelocity += gravity * Time.deltaTime;
}
// 应用垂直速度
characterController.Move(new Vector3(0, verticalVelocity * Time.deltaTime, 0));
// 旋转角色面向移动方向
if (moveDirection.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 10f);
}
}
private void UpdatePerceptionAttributes()
{
// 计算当前可见性
currentVisibility = attributes.baseVisibility;
if (isCrouching)
{
currentVisibility *= attributes.crouchVisibilityMultiplier;
}
if (isRunning)
{
currentVisibility *= attributes.runVisibilityMultiplier;
}
if (isInShadow)
{
currentVisibility *= attributes.inShadowVisibilityMultiplier;
}
// 计算当前可听性
currentAudibility = attributes.baseAudibility;
if (isCrouching)
{
currentAudibility *= attributes.crouchAudibilityMultiplier;
}
if (isRunning)
{
currentAudibility *= attributes.runAudibilityMultiplier;
}
// 移动时增加可听性
float moveAmount = moveDirection.magnitude;
currentAudibility *= (1f + moveAmount * 0.5f);
}
private void UpdatePerceptionEmitters()
{
// 更新脚步声
if (isGrounded && moveDirection.magnitude > 0.1f)
{
float footstepRate = isRunning ? 0.3f : (isCrouching ? 0.6f : 0.4f);
if (Time.time % footstepRate < Time.deltaTime)
{
float volumeMultiplier = isRunning ? 1.5f : (isCrouching ? 0.5f : 1f);
footstepEmitter.EmitSound(volumeMultiplier);
}
}
// 更新气味发射
if (playerScentEmitter != null)
{
float scentStrength = isRunning ? 1.5f : (isCrouching ? 0.3f : 1f);
playerScentEmitter.config.baseStrength = scentStrength * 0.5f;
}
}
private void CheckEnvironmentalFactors()
{
// 检查是否在阴影中(简化实现)
RaycastHit hit;
Vector3 rayStart = transform.position + Vector3.up * 2f;
if (Physics.Raycast(rayStart, Vector3.up, out hit, 10f))
{
// 被物体遮挡,可能在阴影中
isInShadow = hit.distance < 5f;
}
else
{
isInShadow = false;
}
// 检查地面材质(影响脚步声)
CheckGroundMaterial();
}
private void CheckGroundMaterial()
{
RaycastHit groundHit;
if (Physics.Raycast(transform.position + Vector3.up * 0.1f, Vector3.down, out groundHit, 0.2f))
{
// 可以根据地面材质调整
更多推荐
所有评论(0)