第5章 Unity引擎中智能体的环境感知系统构建与实践
Unity引擎提供了智能体环境感知的两种基础机制:主动轮询检测和事件驱动响应。轮询方式通过定时检查环境实现简单感知,适合实时性要求不高的场景,但存在性能开销;事件驱动则通过监听特定事件实现高效感知,减少了不必要计算,适用于大量AI实体的感知需求。开发者可根据游戏场景需求选择合适机制,或组合使用两者,以构建灵活高效的智能体感知系统。
第5章 Unity引擎中智能体的环境感知系统构建与实践
5.1 智能体感知环境信息的基础机制
在游戏人工智能开发领域,环境感知是智能体决策与行为的基石。一个智能的角色必须能够感知其周围的世界,才能做出符合情境的合理反应。Unity引擎为实现这一目标提供了灵活的框架和工具集,开发者可以通过多种技术手段模拟生物或机械智能体的感知能力。
5.1.1 主动轮询检测机制
轮询方式是最直接的环境感知实现方法。在这种模式下,AI角色会以固定的时间间隔主动检查周围环境的状态。这种方法类似于现实世界中生物有意识地观察周围环境的行为。
轮询机制的核心在于定时器或更新循环。在Unity中,通常通过MonoBehaviour的Update()或FixedUpdate()方法来实现。虽然实现简单直观,但轮询方式存在明显的性能缺陷——无论环境是否发生变化,系统都会持续进行检查,这可能导致不必要的计算开销。
在商业游戏项目中,轮询通常用于对实时性要求不高或感知目标有限的场景。例如,一个策略游戏中资源采集单位的资源点检测,或者角色对自身状态(如生命值、能量值)的监控。
下面是轮询感知的基础实现框架:
using UnityEngine;
public class PollingSensor : MonoBehaviour
{
[SerializeField]
private float detectionRadius = 10.0f;
[SerializeField]
private float pollingInterval = 0.5f;
private float timeSinceLastPoll;
private void Start()
{
timeSinceLastPoll = 0.0f;
}
private void Update()
{
timeSinceLastPoll += Time.deltaTime;
if (timeSinceLastPoll >= pollingInterval)
{
PerformEnvironmentCheck();
timeSinceLastPoll = 0.0f;
}
}
private void PerformEnvironmentCheck()
{
Collider[] hitColliders = Physics.OverlapSphere(
transform.position,
detectionRadius
);
foreach (Collider collider in hitColliders)
{
// 检测逻辑:识别玩家、敌人、物品等
if (collider.CompareTag("Player"))
{
Debug.Log("玩家进入感知范围");
// 触发相应的AI行为
}
}
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, detectionRadius);
}
}
5.1.2 事件驱动的响应机制
事件驱动方式是一种更高效、更模块化的感知实现方法。在这种模式下,AI角色不主动检查环境,而是注册对特定事件的监听,当这些事件发生时接收通知。这类似于现实世界中生物对突发声响或光线变化的反应。
事件驱动系统的优势在于其高效性和解耦性。感知系统只在相关事件发生时被激活,大大减少了不必要的计算。同时,事件生产者和消费者之间是松耦合的,便于系统的扩展和维护。
在Unity商业项目中,事件驱动常用于处理大量AI实体的感知需求,如大型多人在线游戏中NPC对玩家行为的反应,或开放世界游戏中环境变化对生物行为的影响。
下面是事件驱动感知系统的基础结构:
using UnityEngine;
using System;
using System.Collections.Generic;
// 游戏事件定义
public enum GameEventType
{
PlayerMoved,
WeaponFired,
DoorOpened,
ItemCollected
}
// 事件数据封装
public class GameEventData
{
public GameEventType EventType;
public Vector3 EventPosition;
public GameObject EventSource;
public float EventIntensity;
public DateTime Timestamp;
}
// 事件管理器(单例模式)
public class GameEventManager : MonoBehaviour
{
private static GameEventManager instance;
private Dictionary<GameEventType, List<Action<GameEventData>>> eventListeners;
public static GameEventManager Instance
{
get
{
if (instance == null)
{
GameObject managerObject = new GameObject("GameEventManager");
instance = managerObject.AddComponent<GameEventManager>();
instance.Initialize();
DontDestroyOnLoad(managerObject);
}
return instance;
}
}
private void Initialize()
{
eventListeners = new Dictionary<GameEventType, List<Action<GameEventData>>>();
foreach (GameEventType eventType in Enum.GetValues(typeof(GameEventType)))
{
eventListeners[eventType] = new List<Action<GameEventData>>();
}
}
public void RegisterListener(GameEventType eventType, Action<GameEventData> listener)
{
if (eventListeners.ContainsKey(eventType))
{
eventListeners[eventType].Add(listener);
}
}
public void UnregisterListener(GameEventType eventType, Action<GameEventData> listener)
{
if (eventListeners.ContainsKey(eventType) && eventListeners[eventType].Contains(listener))
{
eventListeners[eventType].Remove(listener);
}
}
public void TriggerEvent(GameEventData eventData)
{
if (eventListeners.ContainsKey(eventData.EventType))
{
foreach (Action<GameEventData> listener in eventListeners[eventData.EventType])
{
listener(eventData);
}
}
}
}
// 基于事件的感知器
public class EventDrivenSensor : MonoBehaviour
{
[SerializeField]
private float hearingRange = 20.0f;
private void OnEnable()
{
GameEventManager.Instance.RegisterListener(GameEventType.WeaponFired, OnWeaponFired);
}
private void OnDisable()
{
GameEventManager.Instance.UnregisterListener(GameEventType.WeaponFired, OnWeaponFired);
}
private void OnWeaponFired(GameEventData eventData)
{
float distanceToEvent = Vector3.Distance(transform.position, eventData.EventPosition);
if (distanceToEvent <= hearingRange)
{
Debug.Log($"感知到枪声,距离:{distanceToEvent},强度:{eventData.EventIntensity}");
ProcessSoundEvent(eventData);
}
}
private void ProcessSoundEvent(GameEventData eventData)
{
// 根据声音事件做出AI决策
// 例如:转向声源方向、进入警戒状态、前往调查等
}
}
5.1.3 触发器交互机制
触发器是Unity引擎提供的专门用于检测物体进入、停留和离开特定区域的组件。它基于物理引擎的碰撞检测系统,但设置为"Trigger"模式的碰撞体不会产生物理交互,只用于触发事件。
触发器机制特别适合实现区域性感知,如安全区域、陷阱区域、任务触发区域等。在商业游戏中,触发器常用于实现NPC的对话触发、场景切换、任务进度更新等功能。
下面是触发器感知的典型实现:
using UnityEngine;
public class AreaTriggerSensor : MonoBehaviour
{
[SerializeField]
private string targetTag = "Player";
[SerializeField]
private bool drawDebugGizmos = true;
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag(targetTag))
{
Debug.Log($"{targetTag}进入感知区域");
OnTargetEntered(other.gameObject);
}
}
private void OnTriggerStay(Collider other)
{
if (other.CompareTag(targetTag))
{
OnTargetInArea(other.gameObject);
}
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag(targetTag))
{
Debug.Log($"{targetTag}离开感知区域");
OnTargetExited(other.gameObject);
}
}
protected virtual void OnTargetEntered(GameObject target)
{
// 子类可重写此方法实现特定行为
}
protected virtual void OnTargetInArea(GameObject target)
{
// 目标在区域内持续触发的逻辑
}
protected virtual void OnTargetExited(GameObject target)
{
// 子类可重写此方法实现特定行为
}
private void OnDrawGizmos()
{
if (!drawDebugGizmos)
{
return;
}
Collider collider = GetComponent<Collider>();
if (collider == null)
{
return;
}
Gizmos.color = new Color(0, 1, 0, 0.3f);
if (collider is BoxCollider boxCollider)
{
Gizmos.DrawCube(
transform.position + boxCollider.center,
Vector3.Scale(boxCollider.size, transform.lossyScale)
);
}
else if (collider is SphereCollider sphereCollider)
{
Gizmos.DrawSphere(
transform.position + sphereCollider.center,
sphereCollider.radius * Mathf.Max(
transform.lossyScale.x,
transform.lossyScale.y,
transform.lossyScale.z
)
);
}
}
}
5.2 各类感知系统的具体实践方案
5.2.1 触发器系统的基础抽象设计
在构建复杂的AI感知系统时,创建一个可扩展的触发器基类是至关重要的。这个基类定义了所有触发器的共同接口和行为模式,使得不同类型的触发器可以统一管理。
using UnityEngine;
using System;
public abstract class TriggerBase : MonoBehaviour
{
public string triggerId;
public bool isActive = true;
public float cooldownTime = 1.0f;
private float lastTriggerTime;
public event Action<GameObject> OnTriggerActivated;
public event Action<GameObject> OnTriggerDeactivated;
protected virtual void Start()
{
lastTriggerTime = -cooldownTime;
}
protected bool CanTrigger()
{
if (!isActive)
{
return false;
}
return Time.time >= lastTriggerTime + cooldownTime;
}
protected void RecordTriggerTime()
{
lastTriggerTime = Time.time;
}
protected virtual void ActivateTrigger(GameObject triggerTarget)
{
if (!CanTrigger())
{
return;
}
RecordTriggerTime();
OnTriggerActivated?.Invoke(triggerTarget);
ExecuteTriggerBehavior(triggerTarget);
}
protected virtual void DeactivateTrigger(GameObject triggerTarget)
{
OnTriggerDeactivated?.Invoke(triggerTarget);
ExecuteDeactivationBehavior(triggerTarget);
}
protected abstract void ExecuteTriggerBehavior(GameObject triggerTarget);
protected abstract void ExecuteDeactivationBehavior(GameObject triggerTarget);
public virtual void EnableTrigger()
{
isActive = true;
}
public virtual void DisableTrigger()
{
isActive = false;
}
public virtual void ToggleTrigger()
{
isActive = !isActive;
}
}
// 具体触发器实现: proximity trigger
public class ProximityTrigger : TriggerBase
{
[SerializeField]
private float activationDistance = 5.0f;
[SerializeField]
private Transform targetTransform;
[SerializeField]
private bool usePlayerAsDefaultTarget = true;
private void Start()
{
base.Start();
if (usePlayerAsDefaultTarget && targetTransform == null)
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
targetTransform = player.transform;
}
}
}
private void Update()
{
if (targetTransform == null)
{
return;
}
float distance = Vector3.Distance(transform.position, targetTransform.position);
if (distance <= activationDistance)
{
ActivateTrigger(targetTransform.gameObject);
}
else
{
DeactivateTrigger(targetTransform.gameObject);
}
}
protected override void ExecuteTriggerBehavior(GameObject triggerTarget)
{
Debug.Log($"接近触发器激活:{triggerTarget.name} 进入范围");
// 实际游戏中的触发逻辑,如开启机关、播放音效等
}
protected override void ExecuteDeactivationBehavior(GameObject triggerTarget)
{
Debug.Log($"接近触发器失活:{triggerTarget.name} 离开范围");
// 离开范围后的清理逻辑
}
private void OnDrawGizmosSelected()
{
if (!isActive)
{
return;
}
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, activationDistance);
}
}
5.2.2 感知器系统的抽象基础架构
感知器是AI角色感知环境的核心组件。一个良好的感知器基类设计应该支持多种感知类型,并提供统一的接口来处理感知数据。
using UnityEngine;
using System.Collections.Generic;
public abstract class SensorBase : MonoBehaviour
{
[SerializeField]
protected float updateInterval = 0.1f;
[SerializeField]
protected LayerMask detectionLayerMask = ~0;
[SerializeField]
protected LayerMask occlusionLayerMask;
protected float lastUpdateTime;
protected List<PerceptionData> currentPerceptions;
public List<PerceptionData> CurrentPerceptions
{
get { return new List<PerceptionData>(currentPerceptions); }
}
protected virtual void Awake()
{
currentPerceptions = new List<PerceptionData>();
lastUpdateTime = -updateInterval;
}
protected virtual void Update()
{
if (Time.time >= lastUpdateTime + updateInterval)
{
UpdatePerceptions();
lastUpdateTime = Time.time;
}
}
protected abstract void UpdatePerceptions();
protected virtual bool CheckOcclusion(Vector3 fromPosition, Vector3 toPosition, GameObject target)
{
if (occlusionLayerMask.value == 0)
{
return false;
}
Vector3 direction = toPosition - fromPosition;
float distance = direction.magnitude;
RaycastHit hit;
if (Physics.Raycast(
fromPosition,
direction.normalized,
out hit,
distance,
occlusionLayerMask
))
{
// 检查是否击中了目标本身
if (hit.collider.gameObject != target)
{
return true;
}
}
return false;
}
public virtual void ClearPerceptions()
{
currentPerceptions.Clear();
}
public virtual bool HasPerceptionOfTag(string tag)
{
foreach (PerceptionData perception in currentPerceptions)
{
if (perception.perceivedObject != null && perception.perceivedObject.CompareTag(tag))
{
return true;
}
}
return false;
}
public virtual List<GameObject> GetPerceivedObjectsByTag(string tag)
{
List<GameObject> result = new List<GameObject>();
foreach (PerceptionData perception in currentPerceptions)
{
if (perception.perceivedObject != null && perception.perceivedObject.CompareTag(tag))
{
result.Add(perception.perceivedObject);
}
}
return result;
}
}
// 感知数据容器
[System.Serializable]
public class PerceptionData
{
public GameObject perceivedObject;
public Vector3 lastKnownPosition;
public Vector3 lastKnownVelocity;
public float confidence; // 感知可信度(0-1)
public float lastPerceptionTime;
public PerceptionType perceptionType;
public PerceptionData(
GameObject obj,
Vector3 position,
PerceptionType type
)
{
perceivedObject = obj;
lastKnownPosition = position;
perceptionType = type;
confidence = 1.0f;
lastPerceptionTime = Time.time;
}
public void UpdatePosition(Vector3 newPosition, float newConfidence)
{
lastKnownPosition = newPosition;
confidence = newConfidence;
lastPerceptionTime = Time.time;
}
public float GetAge()
{
return Time.time - lastPerceptionTime;
}
}
public enum PerceptionType
{
Visual,
Auditory,
Tactile,
Memory,
Item
}
5.2.3 事件管理系统的优化实现
高效的事件管理系统是大型游戏项目不可或缺的组成部分。下面是一个优化的事件管理器实现,支持优先级和事件过滤。
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;
public class EnhancedEventManager : MonoBehaviour
{
private static EnhancedEventManager instance;
[System.Serializable]
private class EventListener
{
public Action<GameEventData> callback;
public GameObject listenerObject;
public int priority;
public float maxDistance; // 0表示无限距离
}
private Dictionary<GameEventType, List<EventListener>> eventListeners;
private Queue<GameEventData> eventQueue;
[SerializeField]
private int maxEventsPerFrame = 10;
public static EnhancedEventManager Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<EnhancedEventManager>();
if (instance == null)
{
GameObject go = new GameObject("EnhancedEventManager");
instance = go.AddComponent<EnhancedEventManager>();
}
}
return instance;
}
}
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
Initialize();
}
private void Initialize()
{
eventListeners = new Dictionary<GameEventType, List<EventListener>>();
eventQueue = new Queue<GameEventData>();
foreach (GameEventType eventType in Enum.GetValues(typeof(GameEventType)))
{
eventListeners[eventType] = new List<EventListener>();
}
}
private void Update()
{
ProcessEventQueue();
}
public void RegisterListener(
GameEventType eventType,
Action<GameEventData> callback,
GameObject listenerObject = null,
int priority = 0,
float maxDistance = 0
)
{
if (!eventListeners.ContainsKey(eventType))
{
eventListeners[eventType] = new List<EventListener>();
}
EventListener newListener = new EventListener
{
callback = callback,
listenerObject = listenerObject,
priority = priority,
maxDistance = maxDistance
};
eventListeners[eventType].Add(newListener);
// 按优先级排序
eventListeners[eventType] = eventListeners[eventType]
.OrderByDescending(l => l.priority)
.ToList();
}
public void UnregisterListener(GameEventType eventType, Action<GameEventData> callback)
{
if (!eventListeners.ContainsKey(eventType))
{
return;
}
eventListeners[eventType].RemoveAll(l => l.callback == callback);
}
public void UnregisterAllListeners(GameObject listenerObject)
{
foreach (var eventType in eventListeners.Keys)
{
eventListeners[eventType].RemoveAll(l => l.listenerObject == listenerObject);
}
}
public void TriggerEvent(GameEventData eventData, bool immediate = false)
{
if (immediate)
{
ProcessEventImmediately(eventData);
}
else
{
eventQueue.Enqueue(eventData);
}
}
private void ProcessEventQueue()
{
int processedCount = 0;
while (eventQueue.Count > 0 && processedCount < maxEventsPerFrame)
{
GameEventData eventData = eventQueue.Dequeue();
ProcessEventImmediately(eventData);
processedCount++;
}
}
private void ProcessEventImmediately(GameEventData eventData)
{
if (!eventListeners.ContainsKey(eventData.EventType))
{
return;
}
List<EventListener> listeners = eventListeners[eventData.EventType];
for (int i = listeners.Count - 1; i >= 0; i--)
{
EventListener listener = listeners[i];
// 检查监听器是否仍然有效
if (listener.callback == null)
{
listeners.RemoveAt(i);
continue;
}
// 检查距离限制
if (listener.maxDistance > 0 && listener.listenerObject != null)
{
float distance = Vector3.Distance(
listener.listenerObject.transform.position,
eventData.EventPosition
);
if (distance > listener.maxDistance)
{
continue;
}
}
try
{
listener.callback(eventData);
}
catch (Exception e)
{
Debug.LogError($"事件处理错误:{e.Message}");
}
}
}
public void ClearEventQueue()
{
eventQueue.Clear();
}
public int GetListenerCount(GameEventType eventType)
{
if (!eventListeners.ContainsKey(eventType))
{
return 0;
}
return eventListeners[eventType].Count;
}
}
5.2.4 视觉感知系统的实现细节
视觉感知是游戏AI中最复杂也最常用的感知类型。一个完整的视觉系统需要考虑视线检测、视锥范围、遮挡检测、目标识别等多个方面。
using UnityEngine;
using System.Collections.Generic;
public class VisualPerceptionSystem : SensorBase
{
[Header("视觉参数")]
[SerializeField]
private float sightRange = 20.0f;
[SerializeField]
[Range(0, 360)]
private float fieldOfView = 90.0f;
[SerializeField]
private int rayCount = 10;
[SerializeField]
private bool useRaycastScanning = true;
[SerializeField]
private float peripheralVisionMultiplier = 1.5f;
[Header("目标过滤")]
[SerializeField]
private string[] detectableTags = { "Player", "Enemy", "NPC" };
[SerializeField]
private bool ignoreInvisibleObjects = true;
private HashSet<GameObject> previouslyVisibleObjects;
protected override void Awake()
{
base.Awake();
previouslyVisibleObjects = new HashSet<GameObject>();
}
protected override void UpdatePerceptions()
{
List<PerceptionData> newPerceptions = new List<PerceptionData>();
HashSet<GameObject> currentlyVisibleObjects = new HashSet<GameObject>();
if (useRaycastScanning)
{
PerformRaycastScan(newPerceptions, currentlyVisibleObjects);
}
else
{
PerformOverlapScan(newPerceptions, currentlyVisibleObjects);
}
// 处理不再可见的对象
HandleNoLongerVisibleObjects(currentlyVisibleObjects);
// 更新当前感知
currentPerceptions = newPerceptions;
previouslyVisibleObjects = currentlyVisibleObjects;
}
private void PerformRaycastScan(List<PerceptionData> perceptions, HashSet<GameObject> visibleObjects)
{
float angleStep = fieldOfView / (rayCount - 1);
float startAngle = -fieldOfView / 2;
for (int i = 0; i < rayCount; i++)
{
float currentAngle = startAngle + (angleStep * i);
Vector3 direction = Quaternion.Euler(0, currentAngle, 0) * transform.forward;
RaycastHit[] hits = Physics.RaycastAll(
transform.position,
direction,
sightRange,
detectionLayerMask
);
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
foreach (RaycastHit hit in hits)
{
GameObject hitObject = hit.collider.gameObject;
if (hitObject == gameObject)
{
continue;
}
if (!IsObjectDetectable(hitObject))
{
continue;
}
if (CheckOcclusion(transform.position, hit.point, hitObject))
{
continue;
}
// 计算感知可信度(基于距离和角度)
float distanceFactor = 1.0f - Mathf.Clamp01(hit.distance / sightRange);
float angleFactor = 1.0f - Mathf.Abs(currentAngle) / (fieldOfView / 2);
float confidence = distanceFactor * angleFactor;
// 检查是否已在感知列表中
PerceptionData existingPerception = perceptions.Find(
p => p.perceivedObject == hitObject
);
if (existingPerception != null)
{
// 更新已有感知(取最高可信度)
if (confidence > existingPerception.confidence)
{
existingPerception.UpdatePosition(hit.point, confidence);
}
}
else
{
// 创建新感知
PerceptionData newPerception = new PerceptionData(
hitObject,
hit.point,
PerceptionType.Visual
)
{
confidence = confidence
};
perceptions.Add(newPerception);
}
visibleObjects.Add(hitObject);
break; // 只处理最近的可见对象
}
}
}
private void PerformOverlapScan(List<PerceptionData> perceptions, HashSet<GameObject> visibleObjects)
{
Collider[] hitColliders = Physics.OverlapSphere(
transform.position,
sightRange,
detectionLayerMask
);
foreach (Collider collider in hitColliders)
{
GameObject hitObject = collider.gameObject;
if (hitObject == gameObject)
{
continue;
}
if (!IsObjectDetectable(hitObject))
{
continue;
}
Vector3 directionToTarget = (hitObject.transform.position - transform.position).normalized;
float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
// 检查是否在视野内
if (angleToTarget > fieldOfView / 2)
{
// 检查是否在周边视野内(可信度较低)
if (angleToTarget > fieldOfView * peripheralVisionMultiplier / 2)
{
continue;
}
}
// 检查遮挡
if (CheckOcclusion(transform.position, hitObject.transform.position, hitObject))
{
continue;
}
float distance = Vector3.Distance(transform.position, hitObject.transform.position);
float distanceFactor = 1.0f - Mathf.Clamp01(distance / sightRange);
float angleFactor = 1.0f - Mathf.Clamp01(angleToTarget / (fieldOfView / 2));
float confidence = distanceFactor * angleFactor * 0.5f; // 重叠扫描可信度较低
PerceptionData perception = new PerceptionData(
hitObject,
hitObject.transform.position,
PerceptionType.Visual
)
{
confidence = confidence
};
perceptions.Add(perception);
visibleObjects.Add(hitObject);
}
}
private bool IsObjectDetectable(GameObject obj)
{
// 检查标签
bool tagMatch = false;
foreach (string tag in detectableTags)
{
if (obj.CompareTag(tag))
{
tagMatch = true;
break;
}
}
if (!tagMatch)
{
return false;
}
// 检查可见性
if (ignoreInvisibleObjects)
{
Renderer renderer = obj.GetComponent<Renderer>();
if (renderer != null && !renderer.isVisible)
{
return false;
}
}
return true;
}
private void HandleNoLongerVisibleObjects(HashSet<GameObject> currentlyVisibleObjects)
{
foreach (GameObject previouslyVisibleObject in previouslyVisibleObjects)
{
if (!currentlyVisibleObjects.Contains(previouslyVisibleObject))
{
Debug.Log($"{previouslyVisibleObject.name} 离开了视野");
// 可以在这里触发"失去目标"的事件或行为
}
}
}
public GameObject GetMostThreateningTarget()
{
if (currentPerceptions.Count == 0)
{
return null;
}
GameObject mostThreatening = null;
float highestThreatLevel = 0;
foreach (PerceptionData perception in currentPerceptions)
{
if (perception.perceivedObject == null)
{
continue;
}
// 简单的威胁评估:基于距离和面向角度
float distance = Vector3.Distance(
transform.position,
perception.lastKnownPosition
);
Vector3 directionToTarget = (perception.lastKnownPosition - transform.position).normalized;
float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
float distanceScore = 1.0f - Mathf.Clamp01(distance / sightRange);
float angleScore = 1.0f - Mathf.Clamp01(angleToTarget / 180.0f);
float threatLevel = distanceScore * 0.7f + angleScore * 0.3f;
threatLevel *= perception.confidence;
if (threatLevel > highestThreatLevel)
{
highestThreatLevel = threatLevel;
mostThreatening = perception.perceivedObject;
}
}
return mostThreatening;
}
private void OnDrawGizmos()
{
if (!Application.isPlaying)
{
return;
}
// 绘制视野范围
Gizmos.color = new Color(0, 1, 0, 0.1f);
Vector3 forward = transform.forward * sightRange;
Vector3 left = Quaternion.Euler(0, -fieldOfView / 2, 0) * forward;
Vector3 right = Quaternion.Euler(0, fieldOfView / 2, 0) * forward;
Vector3[] vertices = new Vector3[4];
vertices[0] = transform.position;
vertices[1] = transform.position + left;
vertices[2] = transform.position + forward;
vertices[3] = transform.position + right;
Gizmos.DrawLine(vertices[0], vertices[1]);
Gizmos.DrawLine(vertices[0], vertices[3]);
Gizmos.DrawLine(vertices[1], vertices[2]);
Gizmos.DrawLine(vertices[2], vertices[3]);
// 绘制感知到的目标
foreach (PerceptionData perception in currentPerceptions)
{
if (perception.perceivedObject == null)
{
continue;
}
Gizmos.color = Color.Lerp(Color.yellow, Color.red, perception.confidence);
Gizmos.DrawLine(transform.position, perception.lastKnownPosition);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(perception.lastKnownPosition, 0.5f * perception.confidence);
}
}
}
5.2.5 听觉感知系统的完整实现
听觉感知使AI角色能够对声音事件做出反应,如枪声、脚步声、爆炸声等。下面是完整的听觉系统实现。
using UnityEngine;
using System.Collections.Generic;
public class AuditoryPerceptionSystem : SensorBase
{
[Header("听觉参数")]
[SerializeField]
private float hearingRange = 30.0f;
[SerializeField]
private float soundDecayRate = 0.1f; // 声音衰减率(每米)
[SerializeField]
private float minimumAudibleVolume = 0.1f;
[Header("声音类型敏感度")]
[SerializeField]
private float gunshotSensitivity = 1.0f;
[SerializeField]
private float footstepsSensitivity = 0.3f;
[SerializeField]
private float explosionSensitivity = 1.5f;
[SerializeField]
private float voiceSensitivity = 0.5f;
private Dictionary<GameObject, SoundPerceptionData> soundPerceptions;
[System.Serializable]
private class SoundPerceptionData
{
public Vector3 soundPosition;
public float volume;
public SoundType soundType;
public float timestamp;
public float confidence;
public SoundPerceptionData(
Vector3 position,
float vol,
SoundType type,
float time
)
{
soundPosition = position;
volume = vol;
soundType = type;
timestamp = time;
confidence = Mathf.Clamp01(vol);
}
public float GetAge()
{
return Time.time - timestamp;
}
}
public enum SoundType
{
Gunshot,
Footsteps,
Explosion,
Voice,
Environment,
Other
}
protected override void Awake()
{
base.Awake();
soundPerceptions = new Dictionary<GameObject, SoundPerceptionData>();
// 注册事件监听器
EnhancedEventManager.Instance.RegisterListener(
GameEventType.WeaponFired,
OnSoundEvent,
gameObject,
0,
hearingRange
);
}
protected override void OnDestroy()
{
base.OnDestroy();
if (EnhancedEventManager.Instance != null)
{
EnhancedEventManager.Instance.UnregisterAllListeners(gameObject);
}
}
private void OnSoundEvent(GameEventData eventData)
{
float distance = Vector3.Distance(transform.position, eventData.EventPosition);
if (distance > hearingRange)
{
return;
}
// 计算声音衰减
float volume = eventData.EventIntensity * GetSensitivityForEventType(eventData.EventType);
float decayFactor = Mathf.Exp(-soundDecayRate * distance);
float finalVolume = volume * decayFactor;
if (finalVolume < minimumAudibleVolume)
{
return;
}
// 检查遮挡(声音穿透性)
float occlusionFactor = CheckSoundOcclusion(eventData.EventPosition);
finalVolume *= occlusionFactor;
if (finalVolume < minimumAudibleVolume)
{
return;
}
SoundType soundType = ConvertEventToSoundType(eventData.EventType);
SoundPerceptionData perceptionData = new SoundPerceptionData(
eventData.EventPosition,
finalVolume,
soundType,
Time.time
);
// 存储或更新声音感知
if (eventData.EventSource != null)
{
soundPerceptions[eventData.EventSource] = perceptionData;
}
else
{
// 没有源对象的声音(如环境音)
GameObject tempKey = new GameObject("TempSoundKey");
tempKey.transform.position = eventData.EventPosition;
soundPerceptions[tempKey] = perceptionData;
Destroy(tempKey, 5.0f); // 5秒后清理
}
Debug.Log($"听到声音:{soundType},音量:{finalVolume:F2},位置:{eventData.EventPosition}");
// 触发行为反应
ReactToSound(perceptionData);
}
private float GetSensitivityForEventType(GameEventType eventType)
{
switch (eventType)
{
case GameEventType.WeaponFired:
return gunshotSensitivity;
case GameEventType.PlayerMoved:
return footstepsSensitivity;
default:
return 1.0f;
}
}
private SoundType ConvertEventToSoundType(GameEventType eventType)
{
switch (eventType)
{
case GameEventType.WeaponFired:
return SoundType.Gunshot;
case GameEventType.PlayerMoved:
return SoundType.Footsteps;
case GameEventType.DoorOpened:
return SoundType.Environment;
default:
return SoundType.Other;
}
}
private float CheckSoundOcclusion(Vector3 soundPosition)
{
Vector3 direction = soundPosition - transform.position;
float distance = direction.magnitude;
RaycastHit[] hits = Physics.RaycastAll(
transform.position,
direction.normalized,
distance,
occlusionLayerMask
);
float occlusionFactor = 1.0f;
foreach (RaycastHit hit in hits)
{
// 检查材质的声音穿透性
SoundOccluder occluder = hit.collider.GetComponent<SoundOccluder>();
if (occluder != null)
{
occlusionFactor *= occluder.soundPenetration;
}
else
{
// 默认穿透性
occlusionFactor *= 0.5f;
}
}
return occlusionFactor;
}
protected override void UpdatePerceptions()
{
// 清理过时的声音感知
CleanupOldPerceptions();
// 将声音感知转换为通用感知数据
currentPerceptions.Clear();
foreach (var kvp in soundPerceptions)
{
if (kvp.Key == null || kvp.Value.GetAge() > 10.0f)
{
continue;
}
PerceptionData perception = new PerceptionData(
kvp.Key,
kvp.Value.soundPosition,
PerceptionType.Auditory
)
{
confidence = kvp.Value.confidence,
lastPerceptionTime = kvp.Value.timestamp
};
currentPerceptions.Add(perception);
}
}
private void CleanupOldPerceptions()
{
List<GameObject> toRemove = new List<GameObject>();
foreach (var kvp in soundPerceptions)
{
if (kvp.Key == null || kvp.Value.GetAge() > 30.0f)
{
toRemove.Add(kvp.Key);
}
}
foreach (GameObject key in toRemove)
{
soundPerceptions.Remove(key);
}
}
private void ReactToSound(SoundPerceptionData soundData)
{
// 根据声音类型和音量决定反应
switch (soundData.soundType)
{
case SoundType.Gunshot:
if (soundData.volume > 0.5f)
{
Debug.Log("听到枪声!进入警戒状态");
// 触发警戒行为
}
break;
case SoundType.Footsteps:
if (soundData.volume > 0.3f)
{
Debug.Log("听到脚步声,提高警觉");
// 触发调查行为
}
break;
case SoundType.Explosion:
Debug.Log("听到爆炸声!寻找掩体");
// 触发躲避行为
break;
}
}
public Vector3? GetLatestSoundPosition(SoundType typeFilter = SoundType.Other)
{
Vector3? latestPosition = null;
float latestTime = 0;
foreach (var kvp in soundPerceptions)
{
if (typeFilter != SoundType.Other && kvp.Value.soundType != typeFilter)
{
continue;
}
if (kvp.Value.timestamp > latestTime)
{
latestTime = kvp.Value.timestamp;
latestPosition = kvp.Value.soundPosition;
}
}
return latestPosition;
}
private void OnDrawGizmosSelected()
{
Gizmos.color = new Color(0, 0, 1, 0.1f);
Gizmos.DrawWireSphere(transform.position, hearingRange);
// 绘制听到的声音位置
Gizmos.color = Color.blue;
foreach (var kvp in soundPerceptions)
{
if (kvp.Key == null)
{
continue;
}
float age = kvp.Value.GetAge();
if (age < 5.0f)
{
float alpha = 1.0f - (age / 5.0f);
Gizmos.color = new Color(0, 0, 1, alpha * 0.5f);
Gizmos.DrawSphere(kvp.Value.soundPosition, 0.5f * kvp.Value.volume);
Gizmos.color = new Color(0, 0, 1, alpha);
Gizmos.DrawLine(transform.position, kvp.Value.soundPosition);
}
}
}
}
// 声音遮挡器组件
public class SoundOccluder : MonoBehaviour
{
[Range(0, 1)]
public float soundPenetration = 0.5f;
[SerializeField]
private bool affectsAllSounds = true;
[SerializeField]
private AuditoryPerceptionSystem.SoundType[] affectedSoundTypes;
}
5.2.6 触觉感知系统的实现方案
触觉感知使AI角色能够感知物理接触,如碰撞、攻击命中、环境交互等。
using UnityEngine;
using System.Collections.Generic;
public class TactilePerceptionSystem : SensorBase
{
[Header("触觉参数")]
[SerializeField]
private float touchMemoryDuration = 5.0f;
[SerializeField]
private bool detectCollisions = true;
[SerializeField]
private bool detectTriggers = true;
[SerializeField]
private float damageThreshold = 10.0f; // 伤害阈值,超过此值触发特殊反应
private List<TactilePerceptionData> tactileMemory;
[System.Serializable]
private class TactilePerceptionData
{
public GameObject contactObject;
public Vector3 contactPoint;
public Vector3 contactNormal;
public float contactForce;
public float contactTime;
public ContactType contactType;
public TactilePerceptionData(
GameObject obj,
Vector3 point,
Vector3 normal,
float force,
ContactType type
)
{
contactObject = obj;
contactPoint = point;
contactNormal = normal;
contactForce = force;
contactType = type;
contactTime = Time.time;
}
public float GetAge()
{
return Time.time - contactTime;
}
}
public enum ContactType
{
Collision,
Trigger,
Damage,
Environmental
}
protected override void Awake()
{
base.Awake();
tactileMemory = new List<TactilePerceptionData>();
}
protected override void UpdatePerceptions()
{
// 清理过时的触觉记忆
CleanupOldTactileMemory();
// 更新当前感知
currentPerceptions.Clear();
foreach (TactilePerceptionData tactileData in tactileMemory)
{
if (tactileData.contactObject == null)
{
continue;
}
PerceptionData perception = new PerceptionData(
tactileData.contactObject,
tactileData.contactPoint,
PerceptionType.Tactile
)
{
confidence = Mathf.Clamp01(tactileData.contactForce / damageThreshold),
lastPerceptionTime = tactileData.contactTime
};
currentPerceptions.Add(perception);
}
}
private void CleanupOldTactileMemory()
{
tactileMemory.RemoveAll(data =>
data.contactObject == null ||
data.GetAge() > touchMemoryDuration
);
}
private void OnCollisionEnter(Collision collision)
{
if (!detectCollisions)
{
return;
}
ProcessCollision(collision, ContactType.Collision);
}
private void OnTriggerEnter(Collider other)
{
if (!detectTriggers)
{
return;
}
ProcessTrigger(other, ContactType.Trigger);
}
private void ProcessCollision(Collision collision, ContactType contactType)
{
ContactPoint contactPoint = collision.contacts[0];
float impactForce = collision.impulse.magnitude;
TactilePerceptionData tactileData = new TactilePerceptionData(
collision.gameObject,
contactPoint.point,
contactPoint.normal,
impactForce,
contactType
);
tactileMemory.Add(tactileData);
Debug.Log($"碰撞感知:与 {collision.gameObject.name} 碰撞,力度:{impactForce:F2}");
// 根据碰撞力度做出反应
if (impactForce > damageThreshold)
{
ReactToDamage(collision.gameObject, impactForce, contactPoint.point);
}
else
{
ReactToTouch(collision.gameObject, impactForce);
}
}
private void ProcessTrigger(Collider other, ContactType contactType)
{
// 估算接触力(对于触发器,我们使用一个默认值)
float estimatedForce = 1.0f;
TactilePerceptionData tactileData = new TactilePerceptionData(
other.gameObject,
other.ClosestPoint(transform.position),
(transform.position - other.transform.position).normalized,
estimatedForce,
contactType
);
tactileMemory.Add(tactileData);
Debug.Log($"触发感知:进入 {other.gameObject.name} 的触发区域");
ReactToTrigger(other.gameObject);
}
// 外部调用的伤害感知方法
public void RegisterDamage(
GameObject damageSource,
float damageAmount,
Vector3 hitPoint,
Vector3 hitNormal
)
{
TactilePerceptionData damageData = new TactilePerceptionData(
damageSource,
hitPoint,
hitNormal,
damageAmount,
ContactType.Damage
);
tactileMemory.Add(damageData);
Debug.Log($"伤害感知:受到 {damageSource.name} 的伤害,值:{damageAmount:F2}");
ReactToDamage(damageSource, damageAmount, hitPoint);
}
private void ReactToTouch(GameObject touchObject, float force)
{
// 基础触觉反应
// 在实际游戏中,这里可能触发动画、音效或AI状态变化
string objectTag = touchObject.tag;
switch (objectTag)
{
case "Obstacle":
Debug.Log("碰到障碍物,尝试绕行");
break;
case "Player":
Debug.Log("接触到玩家");
break;
default:
Debug.Log($"接触到物体:{touchObject.name}");
break;
}
}
private void ReactToTrigger(GameObject triggerObject)
{
// 触发器反应逻辑
// 例如:进入特定区域触发特定行为
AreaTrigger areaTrigger = triggerObject.GetComponent<AreaTrigger>();
if (areaTrigger != null)
{
areaTrigger.OnEntityEnter(gameObject);
}
}
private void ReactToDamage(GameObject damageSource, float damageAmount, Vector3 hitPoint)
{
// 伤害反应逻辑
// 在实际游戏中,这里可能触发受伤动画、痛苦音效、AI战斗状态等
Debug.Log($"受到攻击!来源:{damageSource.name},伤害值:{damageAmount}");
// 通知AI控制器
AIStateController stateController = GetComponent<AIStateController>();
if (stateController != null)
{
stateController.OnDamageTaken(damageSource, damageAmount, hitPoint);
}
// 触发伤害事件
GameEventData damageEvent = new GameEventData
{
EventType = GameEventType.WeaponFired,
EventPosition = hitPoint,
EventSource = damageSource,
EventIntensity = damageAmount,
Timestamp = System.DateTime.Now
};
EnhancedEventManager.Instance.TriggerEvent(damageEvent);
}
public bool HasRecentContactWithTag(string tag, float maxAge = 2.0f)
{
foreach (TactilePerceptionData data in tactileMemory)
{
if (data.contactObject == null)
{
continue;
}
if (data.contactObject.CompareTag(tag) && data.GetAge() <= maxAge)
{
return true;
}
}
return false;
}
public Vector3? GetLastContactPointWithTag(string tag)
{
float latestTime = 0;
Vector3? latestPoint = null;
foreach (TactilePerceptionData data in tactileMemory)
{
if (data.contactObject == null || !data.contactObject.CompareTag(tag))
{
continue;
}
if (data.contactTime > latestTime)
{
latestTime = data.contactTime;
latestPoint = data.contactPoint;
}
}
return latestPoint;
}
private void OnDrawGizmos()
{
if (!Application.isPlaying)
{
return;
}
// 绘制最近的触觉接触点
Gizmos.color = Color.magenta;
foreach (TactilePerceptionData data in tactileMemory)
{
if (data.contactObject == null || data.GetAge() > 2.0f)
{
continue;
}
float alpha = 1.0f - (data.GetAge() / 2.0f);
Gizmos.color = new Color(1, 0, 1, alpha);
Gizmos.DrawSphere(data.contactPoint, 0.2f * alpha);
Gizmos.DrawLine(transform.position, data.contactPoint);
// 绘制接触法线
Gizmos.color = new Color(0, 1, 1, alpha);
Gizmos.DrawRay(data.contactPoint, data.contactNormal);
}
}
}
5.2.7 记忆感知系统的构建
记忆感知使AI角色能够记住过去感知到的信息,并在这些信息的基础上做出决策。
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class MemoryPerceptionSystem : MonoBehaviour
{
[System.Serializable]
private class MemoryRecord
{
public GameObject memorizedObject;
public Vector3 lastKnownPosition;
public Quaternion lastKnownRotation;
public float lastMemoryUpdateTime;
public float confidence; // 记忆可信度(随时间衰减)
public MemoryType memoryType;
public Dictionary<string, object> additionalData;
public MemoryRecord(
GameObject obj,
Vector3 position,
MemoryType type
)
{
memorizedObject = obj;
lastKnownPosition = position;
lastKnownRotation = obj != null ? obj.transform.rotation : Quaternion.identity;
lastMemoryUpdateTime = Time.time;
confidence = 1.0f;
memoryType = type;
additionalData = new Dictionary<string, object>();
}
public void UpdateMemory(
Vector3 newPosition,
Quaternion newRotation,
float newConfidence
)
{
lastKnownPosition = newPosition;
lastKnownRotation = newRotation;
lastMemoryUpdateTime = Time.time;
confidence = Mathf.Clamp01(newConfidence);
}
public float GetMemoryAge()
{
return Time.time - lastMemoryUpdateTime;
}
public bool IsValid()
{
return memorizedObject != null && confidence > 0.1f;
}
}
public enum MemoryType
{
VisualMemory,
AuditoryMemory,
TactileMemory,
LocationMemory,
EventMemory
}
[Header("记忆参数")]
[SerializeField]
private float defaultMemoryDecayRate = 0.1f; // 每秒衰减量
[SerializeField]
private float maxMemoryAge = 60.0f; // 最长记忆时间(秒)
[SerializeField]
private int maxMemoryRecords = 100;
[Header("记忆类型特定设置")]
[SerializeField]
private float visualMemoryDecayRate = 0.05f;
[SerializeField]
private float auditoryMemoryDecayRate = 0.2f;
[SerializeField]
private float importantEventMemoryDecayRate = 0.01f;
private Dictionary<GameObject, MemoryRecord> memoryRecords;
private List<MemoryRecord> locationMemories; // 位置记忆(无关联对象)
private void Awake()
{
memoryRecords = new Dictionary<GameObject, MemoryRecord>();
locationMemories = new List<MemoryRecord>();
}
private void Update()
{
UpdateMemoryDecay();
CleanupOldMemories();
}
public void RecordVisualMemory(GameObject target, Vector3 position, float confidence)
{
RecordMemory(target, position, MemoryType.VisualMemory, confidence);
}
public void RecordAuditoryMemory(GameObject source, Vector3 soundPosition, float confidence)
{
RecordMemory(source, soundPosition, MemoryType.AuditoryMemory, confidence);
}
public void RecordLocationMemory(Vector3 location, string locationName, float importance = 0.5f)
{
// 创建虚拟对象来表示位置
GameObject locationObject = new GameObject($"LocationMemory_{locationName}");
locationObject.transform.position = location;
MemoryRecord memory = new MemoryRecord(
locationObject,
location,
MemoryType.LocationMemory
)
{
confidence = importance
};
memory.additionalData["LocationName"] = locationName;
locationMemories.Add(memory);
// 限制位置记忆数量
if (locationMemories.Count > maxMemoryRecords / 2)
{
locationMemories = locationMemories
.OrderByDescending(m => m.confidence)
.Take(maxMemoryRecords / 2)
.ToList();
}
}
public void RecordEventMemory(
string eventDescription,
Vector3 eventLocation,
float importance
)
{
GameObject eventObject = new GameObject($"EventMemory_{Time.time}");
eventObject.transform.position = eventLocation;
MemoryRecord memory = new MemoryRecord(
eventObject,
eventLocation,
MemoryType.EventMemory
)
{
confidence = importance
};
memory.additionalData["EventDescription"] = eventDescription;
memory.additionalData["Importance"] = importance;
memoryRecords[eventObject] = memory;
}
private void RecordMemory(
GameObject target,
Vector3 position,
MemoryType type,
float confidence
)
{
if (target == null)
{
return;
}
if (memoryRecords.ContainsKey(target))
{
// 更新现有记忆
MemoryRecord existingRecord = memoryRecords[target];
// 如果新信息可信度更高,则更新记忆
if (confidence > existingRecord.confidence * 0.8f)
{
existingRecord.UpdateMemory(
position,
target.transform.rotation,
confidence
);
existingRecord.memoryType = type;
}
}
else
{
// 创建新记忆
MemoryRecord newRecord = new MemoryRecord(target, position, type)
{
confidence = confidence
};
memoryRecords[target] = newRecord;
// 限制记忆数量
if (memoryRecords.Count > maxMemoryRecords)
{
RemoveLeastImportantMemories();
}
}
}
private void UpdateMemoryDecay()
{
// 更新对象记忆的衰减
List<GameObject> keysToUpdate = new List<GameObject>(memoryRecords.Keys);
foreach (GameObject key in keysToUpdate)
{
if (memoryRecords[key].memorizedObject == null)
{
continue;
}
float decayRate = GetDecayRateForMemoryType(memoryRecords[key].memoryType);
memoryRecords[key].confidence -= decayRate * Time.deltaTime;
}
// 更新位置记忆的衰减
for (int i = locationMemories.Count - 1; i >= 0; i--)
{
locationMemories[i].confidence -= defaultMemoryDecayRate * Time.deltaTime;
}
}
private float GetDecayRateForMemoryType(MemoryType type)
{
switch (type)
{
case MemoryType.VisualMemory:
return visualMemoryDecayRate;
case MemoryType.AuditoryMemory:
return auditoryMemoryDecayRate;
case MemoryType.EventMemory:
return importantEventMemoryDecayRate;
default:
return defaultMemoryDecayRate;
}
}
private void CleanupOldMemories()
{
// 清理过时的对象记忆
List<GameObject> keysToRemove = new List<GameObject>();
foreach (var kvp in memoryRecords)
{
if (!kvp.Value.IsValid() || kvp.Value.GetMemoryAge() > maxMemoryAge)
{
keysToRemove.Add(kvp.Key);
}
}
foreach (GameObject key in keysToRemove)
{
if (memoryRecords[key].memorizedObject != null &&
memoryRecords[key].memorizedObject.name.StartsWith("EventMemory_"))
{
Destroy(memoryRecords[key].memorizedObject);
}
memoryRecords.Remove(key);
}
// 清理过时的位置记忆
locationMemories.RemoveAll(memory =>
!memory.IsValid() || memory.GetMemoryAge() > maxMemoryAge
);
}
private void RemoveLeastImportantMemories()
{
// 按可信度排序,移除最低的
var sortedMemories = memoryRecords
.OrderBy(kvp => kvp.Value.confidence)
.ThenBy(kvp => kvp.Value.GetMemoryAge())
.ToList();
int memoriesToRemove = memoryRecords.Count - maxMemoryRecords;
for (int i = 0; i < memoriesToRemove; i++)
{
GameObject key = sortedMemories[i].Key;
if (memoryRecords[key].memorizedObject != null &&
memoryRecords[key].memorizedObject.name.StartsWith("EventMemory_"))
{
Destroy(memoryRecords[key].memorizedObject);
}
memoryRecords.Remove(key);
}
}
public bool HasMemoryOfObject(GameObject obj)
{
return memoryRecords.ContainsKey(obj) && memoryRecords[obj].IsValid();
}
public Vector3? GetLastKnownPosition(GameObject obj)
{
if (HasMemoryOfObject(obj))
{
return memoryRecords[obj].lastKnownPosition;
}
return null;
}
public float GetMemoryConfidence(GameObject obj)
{
if (HasMemoryOfObject(obj))
{
return memoryRecords[obj].confidence;
}
return 0f;
}
public List<GameObject> GetRememberedObjectsByType(MemoryType type, float minConfidence = 0.3f)
{
List<GameObject> result = new List<GameObject>();
foreach (var kvp in memoryRecords)
{
if (kvp.Value.memoryType == type &&
kvp.Value.confidence >= minConfidence &&
kvp.Value.IsValid())
{
result.Add(kvp.Key);
}
}
return result;
}
public List<Vector3> GetRememberedLocations(float minConfidence = 0.3f)
{
return locationMemories
.Where(memory => memory.confidence >= minConfidence)
.Select(memory => memory.lastKnownPosition)
.ToList();
}
public void StrengthenMemory(GameObject obj, float strengthAmount)
{
if (memoryRecords.ContainsKey(obj))
{
memoryRecords[obj].confidence = Mathf.Clamp01(
memoryRecords[obj].confidence + strengthAmount
);
memoryRecords[obj].lastMemoryUpdateTime = Time.time;
}
}
public void WeakenMemory(GameObject obj, float weakenAmount)
{
if (memoryRecords.ContainsKey(obj))
{
memoryRecords[obj].confidence = Mathf.Clamp01(
memoryRecords[obj].confidence - weakenAmount
);
}
}
public void ClearMemoriesOfType(MemoryType type)
{
List<GameObject> keysToRemove = new List<GameObject>();
foreach (var kvp in memoryRecords)
{
if (kvp.Value.memoryType == type)
{
keysToRemove.Add(kvp.Key);
}
}
foreach (GameObject key in keysToRemove)
{
memoryRecords.Remove(key);
}
}
public void DebugLogMemories()
{
Debug.Log($"=== 记忆系统状态 ===");
Debug.Log($"对象记忆数量:{memoryRecords.Count}");
Debug.Log($"位置记忆数量:{locationMemories.Count}");
foreach (var kvp in memoryRecords)
{
if (kvp.Value.IsValid())
{
Debug.Log($"记忆对象:{kvp.Key.name},类型:{kvp.Value.memoryType}," +
$"可信度:{kvp.Value.confidence:F2}," +
$"年龄:{kvp.Value.GetMemoryAge():F1}秒");
}
}
}
private void OnDrawGizmosSelected()
{
if (!Application.isPlaying)
{
return;
}
// 绘制记忆位置
Gizmos.color = new Color(1, 0.5f, 0, 0.3f); // 橙色
foreach (var kvp in memoryRecords)
{
if (!kvp.Value.IsValid())
{
continue;
}
float alpha = kvp.Value.confidence * 0.5f;
Gizmos.color = new Color(1, 0.5f, 0, alpha);
Gizmos.DrawSphere(kvp.Value.lastKnownPosition, 0.3f * kvp.Value.confidence);
// 绘制到当前位置的线(如果对象已移动)
if (kvp.Value.memorizedObject != null)
{
Vector3 currentPosition = kvp.Value.memorizedObject.transform.position;
if (Vector3.Distance(currentPosition, kvp.Value.lastKnownPosition) > 0.5f)
{
Gizmos.DrawLine(currentPosition, kvp.Value.lastKnownPosition);
}
}
}
// 绘制位置记忆
Gizmos.color = new Color(0, 1, 1, 0.3f); // 青色
foreach (MemoryRecord locationMemory in locationMemories)
{
if (!locationMemory.IsValid())
{
continue;
}
float alpha = locationMemory.confidence * 0.3f;
Gizmos.color = new Color(0, 1, 1, alpha);
Gizmos.DrawWireSphere(locationMemory.lastKnownPosition, 0.5f);
if (locationMemory.additionalData.ContainsKey("LocationName"))
{
// 在Unity编辑器中显示位置名称
#if UNITY_EDITOR
UnityEditor.Handles.Label(
locationMemory.lastKnownPosition + Vector3.up,
locationMemory.additionalData["LocationName"] as string
);
#endif
}
}
}
}
5.2.8 物品感知系统的实现
物品感知使AI角色能够感知游戏世界中的物品,如血包、弹药、宝物、钥匙等。
using UnityEngine;
using System.Collections.Generic;
public class ItemPerceptionSystem : SensorBase
{
[System.Serializable]
private class ItemTypeSettings
{
public string itemTag;
public float detectionRange = 10.0f;
public float importance = 1.0f;
public bool isAlwaysDetected = false;
public Color debugColor = Color.white;
}
[Header("物品感知设置")]
[SerializeField]
private ItemTypeSettings[] itemTypeSettings;
[SerializeField]
private LayerMask itemLayerMask;
[SerializeField]
private float itemScanInterval = 0.5f;
[Header("记忆设置")]
[SerializeField]
private bool rememberItemLocations = true;
[SerializeField]
private float itemMemoryDuration = 30.0f;
private Dictionary<string, ItemTypeSettings> itemSettingsLookup;
private Dictionary<GameObject, float> itemMemory; // 物品记忆:物品 -> 最后感知时间
private float lastItemScanTime;
protected override void Awake()
{
base.Awake();
itemSettingsLookup = new Dictionary<string, ItemTypeSettings>();
itemMemory = new Dictionary<GameObject, float>();
// 初始化物品设置查找表
foreach (ItemTypeSettings settings in itemTypeSettings)
{
if (!string.IsNullOrEmpty(settings.itemTag))
{
itemSettingsLookup[settings.itemTag] = settings;
}
}
lastItemScanTime = -itemScanInterval;
}
protected override void Update()
{
base.Update();
// 单独的物品扫描(可以有不同的更新频率)
if (Time.time >= lastItemScanTime + itemScanInterval)
{
ScanForItems();
lastItemScanTime = Time.time;
}
// 清理过时的物品记忆
CleanupItemMemory();
}
protected override void UpdatePerceptions()
{
// 物品感知数据已经通过ScanForItems方法更新到currentPerceptions
// 这里主要处理感知数据的后处理
// 按重要性排序感知
currentPerceptions.Sort((a, b) =>
{
float importanceA = GetItemImportance(a.perceivedObject);
float importanceB = GetItemImportance(b.perceivedObject);
return importanceB.CompareTo(importanceA); // 降序排序
});
}
private void ScanForItems()
{
List<PerceptionData> itemPerceptions = new List<PerceptionData>();
// 扫描所有配置的物品类型
foreach (var kvp in itemSettingsLookup)
{
string itemTag = kvp.Key;
ItemTypeSettings settings = kvp.Value;
// 如果设置为始终检测,跳过范围检查
if (settings.isAlwaysDetected)
{
GameObject[] items = GameObject.FindGameObjectsWithTag(itemTag);
foreach (GameObject item in items)
{
AddItemPerception(itemPerceptions, item, settings);
}
}
else
{
// 范围检测
Collider[] hitColliders = Physics.OverlapSphere(
transform.position,
settings.detectionRange,
itemLayerMask
);
foreach (Collider collider in hitColliders)
{
if (collider.CompareTag(itemTag))
{
AddItemPerception(itemPerceptions, collider.gameObject, settings);
}
}
}
}
// 更新当前感知
currentPerceptions = itemPerceptions;
// 更新物品记忆
foreach (PerceptionData perception in itemPerceptions)
{
if (perception.perceivedObject != null)
{
itemMemory[perception.perceivedObject] = Time.time;
}
}
}
private void AddItemPerception(
List<PerceptionData> perceptions,
GameObject item,
ItemTypeSettings settings
)
{
// 检查遮挡
if (CheckOcclusion(transform.position, item.transform.position, item))
{
return;
}
// 计算感知可信度(基于距离)
float distance = Vector3.Distance(transform.position, item.transform.position);
float distanceFactor = 1.0f - Mathf.Clamp01(distance / settings.detectionRange);
float confidence = distanceFactor * settings.importance;
PerceptionData perception = new PerceptionData(
item,
item.transform.position,
PerceptionType.Item
)
{
confidence = confidence
};
perceptions.Add(perception);
}
private float GetItemImportance(GameObject item)
{
if (item == null)
{
return 0f;
}
foreach (var kvp in itemSettingsLookup)
{
if (item.CompareTag(kvp.Key))
{
return kvp.Value.importance;
}
}
return 0f;
}
private void CleanupItemMemory()
{
List<GameObject> itemsToRemove = new List<GameObject>();
foreach (var kvp in itemMemory)
{
if (kvp.Key == null || Time.time - kvp.Value > itemMemoryDuration)
{
itemsToRemove.Add(kvp.Key);
}
}
foreach (GameObject item in itemsToRemove)
{
itemMemory.Remove(item);
}
}
public GameObject GetMostImportantItem()
{
if (currentPerceptions.Count == 0)
{
// 检查记忆中是否有物品
return GetMostImportantRememberedItem();
}
// 返回当前感知中最重要的物品
if (currentPerceptions[0].perceivedObject != null)
{
return currentPerceptions[0].perceivedObject;
}
return null;
}
private GameObject GetMostImportantRememberedItem()
{
if (!rememberItemLocations || itemMemory.Count == 0)
{
return null;
}
GameObject mostImportantItem = null;
float highestImportance = 0;
foreach (var kvp in itemMemory)
{
if (kvp.Key == null)
{
continue;
}
float importance = GetItemImportance(kvp.Key);
float memoryFreshness = 1.0f - Mathf.Clamp01(
(Time.time - kvp.Value) / itemMemoryDuration
);
float totalScore = importance * memoryFreshness;
if (totalScore > highestImportance)
{
highestImportance = totalScore;
mostImportantItem = kvp.Key;
}
}
return mostImportantItem;
}
public bool HasItemInMemory(GameObject item)
{
return itemMemory.ContainsKey(item);
}
public void ClearItemMemory(GameObject item)
{
if (itemMemory.ContainsKey(item))
{
itemMemory.Remove(item);
}
}
public void ClearAllItemMemory()
{
itemMemory.Clear();
}
public List<GameObject> GetItemsOfType(string itemTag, bool includeRemembered = true)
{
List<GameObject> items = new List<GameObject>();
// 当前感知的物品
foreach (PerceptionData perception in currentPerceptions)
{
if (perception.perceivedObject != null &&
perception.perceivedObject.CompareTag(itemTag))
{
items.Add(perception.perceivedObject);
}
}
// 记忆中的物品
if (includeRemembered && rememberItemLocations)
{
foreach (var kvp in itemMemory)
{
if (kvp.Key != null &&
kvp.Key.CompareTag(itemTag) &&
!items.Contains(kvp.Key))
{
items.Add(kvp.Key);
}
}
}
return items;
}
public void OnItemCollected(GameObject item)
{
// 物品被收集,从感知和记忆中移除
currentPerceptions.RemoveAll(p => p.perceivedObject == item);
if (itemMemory.ContainsKey(item))
{
itemMemory.Remove(item);
}
Debug.Log($"物品 {item.name} 已被收集,从感知系统中移除");
}
private void OnDrawGizmosSelected()
{
if (!Application.isPlaying)
{
return;
}
// 绘制不同物品类型的检测范围
foreach (var kvp in itemSettingsLookup)
{
ItemTypeSettings settings = kvp.Value;
if (settings.detectionRange <= 0 || settings.isAlwaysDetected)
{
continue;
}
Gizmos.color = settings.debugColor;
Gizmos.DrawWireSphere(transform.position, settings.detectionRange);
}
// 绘制感知到的物品
foreach (PerceptionData perception in currentPerceptions)
{
if (perception.perceivedObject == null)
{
continue;
}
string itemTag = perception.perceivedObject.tag;
if (itemSettingsLookup.ContainsKey(itemTag))
{
Color itemColor = itemSettingsLookup[itemTag].debugColor;
float alpha = perception.confidence * 0.8f;
Gizmos.color = new Color(itemColor.r, itemColor.g, itemColor.b, alpha);
Gizmos.DrawLine(transform.position, perception.lastKnownPosition);
Gizmos.color = itemColor;
Gizmos.DrawWireSphere(perception.lastKnownPosition, 0.3f * perception.confidence);
}
}
// 绘制记忆中的物品
if (rememberItemLocations)
{
Gizmos.color = new Color(1, 1, 0, 0.3f); // 半透明黄色
foreach (var kvp in itemMemory)
{
if (kvp.Key == null)
{
continue;
}
float memoryAge = Time.time - kvp.Value;
float memoryFreshness = 1.0f - Mathf.Clamp01(memoryAge / itemMemoryDuration);
if (memoryFreshness > 0.1f)
{
float alpha = memoryFreshness * 0.3f;
Gizmos.color = new Color(1, 1, 0, alpha);
Gizmos.DrawWireSphere(kvp.Key.transform.position, 0.5f * memoryFreshness);
// 绘制虚线连接到当前位置
DrawDottedLine(transform.position, kvp.Key.transform.position, alpha);
}
}
}
}
private void DrawDottedLine(Vector3 start, Vector3 end, float alpha)
{
// 简单实现虚线绘制
int segments = 10;
for (int i = 0; i < segments; i++)
{
float t1 = i / (float)segments;
float t2 = (i + 0.5f) / (float)segments;
if (t2 > 1.0f)
{
break;
}
Vector3 point1 = Vector3.Lerp(start, end, t1);
Vector3 point2 = Vector3.Lerp(start, end, t2);
Gizmos.DrawLine(point1, point2);
}
}
}
5.3 综合感知系统整合实践
5.3.1 游戏场景环境搭建
在Unity中创建一个测试场景,用于演示综合感知系统:
- 创建地形或平面作为地面
- 添加光源(Directional Light)
- 创建几个立方体作为障碍物,设置Layer为"Obstacle"
- 创建一些球体作为可收集物品,分别标记为"HealthPack"、“Ammo”、“Treasure”
- 设置适当的碰撞体和触发器
5.3.2 创建AI士兵角色预制体
创建一个完整的AI士兵角色,整合所有感知系统:
using UnityEngine;
using System.Collections.Generic;
[RequireComponent(typeof(CharacterController))]
public class AISoldier : MonoBehaviour
{
[Header("移动设置")]
[SerializeField]
private float moveSpeed = 5.0f;
[SerializeField]
private float rotationSpeed = 10.0f;
[Header("感知系统")]
[SerializeField]
private VisualPerceptionSystem visualPerception;
[SerializeField]
private AuditoryPerceptionSystem auditoryPerception;
[SerializeField]
private TactilePerceptionSystem tactilePerception;
[SerializeField]
private MemoryPerceptionSystem memoryPerception;
[SerializeField]
private ItemPerceptionSystem itemPerception;
[Header("AI状态")]
[SerializeField]
private AIState currentState = AIState.Patrol;
private CharacterController characterController;
private Vector3 moveDirection;
private Vector3 targetPosition;
private GameObject currentTarget;
private GameObject lastKnownTargetPosition;
private float stateTimer;
private float patrolPointStayTime = 3.0f;
private List<Vector3> patrolPoints;
private int currentPatrolIndex;
public enum AIState
{
Patrol,
Investigate,
Combat,
Search,
Retreat,
CollectItem
}
private void Awake()
{
characterController = GetComponent<CharacterController>();
// 初始化巡逻点
patrolPoints = new List<Vector3>();
GeneratePatrolPoints(5, 10.0f);
currentPatrolIndex = 0;
targetPosition = patrolPoints[currentPatrolIndex];
// 初始化感知系统
if (visualPerception == null)
{
visualPerception = GetComponent<VisualPerceptionSystem>();
}
if (auditoryPerception == null)
{
auditoryPerception = GetComponent<AuditoryPerceptionSystem>();
}
if (tactilePerception == null)
{
tactilePerception = GetComponent<TactilePerceptionSystem>();
}
if (memoryPerception == null)
{
memoryPerception = GetComponent<MemoryPerceptionSystem>();
}
if (itemPerception == null)
{
itemPerception = GetComponent<ItemPerceptionSystem>();
}
}
private void Start()
{
// 记录初始位置作为记忆
memoryPerception.RecordLocationMemory(
transform.position,
"起始点",
0.8f
);
// 记录巡逻点
for (int i = 0; i < patrolPoints.Count; i++)
{
memoryPerception.RecordLocationMemory(
patrolPoints[i],
$"巡逻点{i}",
0.6f
);
}
}
private void Update()
{
UpdatePerception();
UpdateState();
MoveAI();
// 调试信息
if (Input.GetKeyDown(KeyCode.M))
{
memoryPerception.DebugLogMemories();
}
}
private void UpdatePerception()
{
// 检查视觉感知的威胁
GameObject visualTarget = visualPerception.GetMostThreateningTarget();
if (visualTarget != null && visualTarget.CompareTag("Player"))
{
OnPlayerSpotted(visualTarget);
}
// 检查听觉感知的最新声音
Vector3? latestSound = auditoryPerception.GetLatestSoundPosition();
if (latestSound.HasValue && currentState != AIState.Combat)
{
OnSoundHeard(latestSound.Value);
}
// 检查触觉感知的伤害
if (tactilePerception.HasRecentContactWithTag("Bullet", 1.0f))
{
Vector3? lastHitPoint = tactilePerception.GetLastContactPointWithTag("Bullet");
if (lastHitPoint.HasValue)
{
OnDamageTaken(lastHitPoint.Value);
}
}
// 检查物品感知
GameObject importantItem = itemPerception.GetMostImportantItem();
if (importantItem != null && currentState == AIState.Patrol)
{
// 如果物品足够重要,去收集
float importance = itemPerception.GetItemImportance(importantItem);
if (importance > 0.7f)
{
OnItemDetected(importantItem);
}
}
}
private void UpdateState()
{
stateTimer += Time.deltaTime;
switch (currentState)
{
case AIState.Patrol:
UpdatePatrolState();
break;
case AIState.Investigate:
UpdateInvestigateState();
break;
case AIState.Combat:
UpdateCombatState();
break;
case AIState.Search:
UpdateSearchState();
break;
case AIState.CollectItem:
UpdateCollectItemState();
break;
}
}
private void UpdatePatrolState()
{
// 到达巡逻点
if (Vector3.Distance(transform.position, targetPosition) < 1.0f)
{
if (stateTimer >= patrolPointStayTime)
{
// 前往下一个巡逻点
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Count;
targetPosition = patrolPoints[currentPatrolIndex];
stateTimer = 0;
Debug.Log($"前往下一个巡逻点:{currentPatrolIndex}");
}
}
}
private void UpdateInvestigateState()
{
// 到达调查点
if (Vector3.Distance(transform.position, targetPosition) < 1.0f)
{
// 环顾四周
transform.Rotate(0, 45 * Time.deltaTime, 0);
if (stateTimer >= 5.0f)
{
// 调查结束,返回巡逻
ChangeState(AIState.Patrol);
// 如果记得玩家最后位置,进入搜索状态
if (lastKnownTargetPosition != null)
{
ChangeState(AIState.Search);
targetPosition = lastKnownTargetPosition.transform.position;
}
}
}
}
private void UpdateCombatState()
{
if (currentTarget == null)
{
// 失去目标,尝试寻找
if (lastKnownTargetPosition != null)
{
ChangeState(AIState.Search);
targetPosition = lastKnownTargetPosition.transform.position;
}
else
{
ChangeState(AIState.Patrol);
}
return;
}
// 面向目标
Vector3 directionToTarget = currentTarget.transform.position - transform.position;
directionToTarget.y = 0;
if (directionToTarget.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
rotationSpeed * Time.deltaTime
);
}
// 战斗逻辑:保持距离
float distanceToTarget = Vector3.Distance(
transform.position,
currentTarget.transform.position
);
if (distanceToTarget < 5.0f)
{
// 太近,后退
moveDirection = -transform.forward;
}
else if (distanceToTarget > 10.0f)
{
// 太远,接近
targetPosition = currentTarget.transform.position;
}
else
{
// 合适距离,左右移动
moveDirection = Quaternion.Euler(0, 90, 0) * transform.forward * Mathf.Sin(Time.time);
}
// 模拟射击
if (stateTimer >= 1.0f)
{
Debug.Log("开火!");
stateTimer = 0;
}
}
private void UpdateSearchState()
{
if (Vector3.Distance(transform.position, targetPosition) < 1.0f)
{
// 到达最后已知位置,环顾四周
transform.Rotate(0, 60 * Time.deltaTime, 0);
if (stateTimer >= 8.0f)
{
// 搜索结束,返回巡逻
ChangeState(AIState.Patrol);
}
}
}
private void UpdateCollectItemState()
{
if (currentTarget == null)
{
ChangeState(AIState.Patrol);
return;
}
targetPosition = currentTarget.transform.position;
if (Vector3.Distance(transform.position, targetPosition) < 1.5f)
{
// 收集物品
Debug.Log($"收集物品:{currentTarget.name}");
// 通知物品感知系统
itemPerception.OnItemCollected(currentTarget);
// 销毁物品(在实际游戏中可能是禁用或播放动画)
Destroy(currentTarget);
// 返回巡逻
ChangeState(AIState.Patrol);
}
}
private void MoveAI()
{
if (currentState == AIState.Combat && currentTarget != null)
{
// 战斗状态的特殊移动
characterController.Move(moveDirection * moveSpeed * Time.deltaTime);
}
else
{
// 其他状态的移动:前往目标位置
Vector3 direction = (targetPosition - transform.position).normalized;
direction.y = 0;
if (direction.magnitude > 0.1f)
{
// 旋转面向移动方向
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
rotationSpeed * Time.deltaTime
);
// 移动
characterController.Move(direction * moveSpeed * Time.deltaTime);
}
}
// 重力
if (!characterController.isGrounded)
{
characterController.Move(Vector3.down * 9.81f * Time.deltaTime);
}
}
private void GeneratePatrolPoints(int count, float radius)
{
patrolPoints.Clear();
for (int i = 0; i < count; i++)
{
float angle = i * (360f / count);
Vector3 point = transform.position + new Vector3(
Mathf.Cos(angle * Mathf.Deg2Rad) * radius,
0,
Mathf.Sin(angle * Mathf.Deg2Rad) * radius
);
patrolPoints.Add(point);
}
}
private void ChangeState(AIState newState)
{
Debug.Log($"AI状态改变:{currentState} -> {newState}");
currentState = newState;
stateTimer = 0;
// 状态转换时的清理
switch (newState)
{
case AIState.Patrol:
currentTarget = null;
currentPatrolIndex = 0;
targetPosition = patrolPoints[currentPatrolIndex];
break;
case AIState.CollectItem:
// 已经设置好currentTarget
break;
}
}
private void OnPlayerSpotted(GameObject player)
{
Debug.Log($"发现玩家:{player.name}");
currentTarget = player;
// 记录玩家位置
if (lastKnownTargetPosition == null)
{
lastKnownTargetPosition = new GameObject("LastKnownPlayerPosition");
}
lastKnownTargetPosition.transform.position = player.transform.position;
// 强化玩家记忆
memoryPerception.StrengthenMemory(player, 0.5f);
// 记录事件
memoryPerception.RecordEventMemory(
"发现玩家",
player.transform.position,
0.9f
);
// 进入战斗状态
if (currentState != AIState.Combat)
{
ChangeState(AIState.Combat);
}
}
private void OnSoundHeard(Vector3 soundPosition)
{
Debug.Log($"听到声音,前往调查:{soundPosition}");
targetPosition = soundPosition;
// 记录声音位置
memoryPerception.RecordLocationMemory(
soundPosition,
"可疑声音",
0.7f
);
if (currentState == AIState.Patrol)
{
ChangeState(AIState.Investigate);
}
}
private void OnDamageTaken(Vector3 hitPoint)
{
Debug.Log($"受到攻击,攻击来自:{hitPoint}");
// 记录伤害事件
memoryPerception.RecordEventMemory(
"受到攻击",
hitPoint,
1.0f
);
// 如果不在战斗状态,进入调查状态
if (currentState != AIState.Combat)
{
targetPosition = hitPoint;
ChangeState(AIState.Investigate);
}
}
private void OnItemDetected(GameObject item)
{
Debug.Log($"发现重要物品:{item.name}");
currentTarget = item;
ChangeState(AIState.CollectItem);
}
public void OnDamageTakenExternal(GameObject damageSource, float damageAmount, Vector3 hitPoint)
{
// 供外部调用的伤害处理方法
tactilePerception.RegisterDamage(damageSource, damageAmount, hitPoint, Vector3.up);
if (damageSource.CompareTag("Player"))
{
OnPlayerSpotted(damageSource);
}
else
{
OnDamageTaken(hitPoint);
}
}
private void OnDrawGizmos()
{
if (!Application.isPlaying)
{
return;
}
// 绘制巡逻点
Gizmos.color = Color.green;
for (int i = 0; i < patrolPoints.Count; i++)
{
Gizmos.DrawSphere(patrolPoints[i], 0.3f);
int nextIndex = (i + 1) % patrolPoints.Count;
Gizmos.DrawLine(patrolPoints[i], patrolPoints[nextIndex]);
// 绘制到当前巡逻点的线
if (i == currentPatrolIndex && currentState == AIState.Patrol)
{
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, patrolPoints[i]);
Gizmos.color = Color.green;
}
}
// 绘制目标位置
if (currentState != AIState.Patrol)
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(targetPosition, 0.5f);
Gizmos.DrawLine(transform.position, targetPosition);
}
// 绘制状态标签
#if UNITY_EDITOR
UnityEditor.Handles.Label(
transform.position + Vector3.up * 2,
$"状态: {currentState}"
);
if (currentTarget != null)
{
UnityEditor.Handles.Label(
transform.position + Vector3.up * 1.5f,
$"目标: {currentTarget.name}"
);
}
#endif
}
}
5.3.3 玩家角色创建与交互
创建玩家角色,使其能够与AI系统交互:
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private float moveSpeed = 8.0f;
[SerializeField]
private float mouseSensitivity = 2.0f;
[SerializeField]
private GameObject bulletPrefab;
[SerializeField]
private Transform bulletSpawnPoint;
[SerializeField]
private float bulletSpeed = 30.0f;
private CharacterController characterController;
private float verticalRotation = 0;
private float verticalVelocity = 0;
private void Awake()
{
characterController = GetComponent<CharacterController>();
Cursor.lockState = CursorLockMode.Locked;
}
private void Update()
{
HandleMovement();
HandleRotation();
HandleActions();
if (Input.GetKeyDown(KeyCode.Escape))
{
Cursor.lockState = CursorLockMode.None;
}
}
private void HandleMovement()
{
// 键盘输入
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 move = transform.right * horizontal + transform.forward * vertical;
move = move.normalized * moveSpeed;
// 重力
if (characterController.isGrounded)
{
verticalVelocity = -1.0f;
}
else
{
verticalVelocity += Physics.gravity.y * Time.deltaTime;
}
move.y = verticalVelocity;
// 移动
characterController.Move(move * Time.deltaTime);
// 触发移动事件(供AI听觉感知)
if ((Mathf.Abs(horizontal) > 0.1f || Mathf.Abs(vertical) > 0.1f) && characterController.isGrounded)
{
TriggerFootstepEvent();
}
}
private void HandleRotation()
{
// 鼠标输入
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
// 水平旋转
transform.Rotate(0, mouseX, 0);
// 垂直旋转(摄像机)
verticalRotation -= mouseY;
verticalRotation = Mathf.Clamp(verticalRotation, -90, 90);
Camera.main.transform.localRotation = Quaternion.Euler(verticalRotation, 0, 0);
}
private void HandleActions()
{
// 开火
if (Input.GetMouseButtonDown(0))
{
FireWeapon();
}
// 制造声音(测试用)
if (Input.GetKeyDown(KeyCode.Space))
{
MakeNoise();
}
}
private void FireWeapon()
{
if (bulletPrefab == null || bulletSpawnPoint == null)
{
return;
}
// 创建子弹
GameObject bullet = Instantiate(
bulletPrefab,
bulletSpawnPoint.position,
bulletSpawnPoint.rotation
);
Rigidbody rb = bullet.GetComponent<Rigidbody>();
if (rb != null)
{
rb.velocity = bulletSpawnPoint.forward * bulletSpeed;
}
// 触发武器开火事件
GameEventData gunshotEvent = new GameEventData
{
EventType = GameEventType.WeaponFired,
EventPosition = bulletSpawnPoint.position,
EventSource = gameObject,
EventIntensity = 1.0f,
Timestamp = System.DateTime.Now
};
EnhancedEventManager.Instance.TriggerEvent(gunshotEvent);
Debug.Log("玩家开火");
}
private void TriggerFootstepEvent()
{
// 定期触发脚步声事件
if (Time.frameCount % 30 == 0) // 每30帧触发一次
{
GameEventData footstepEvent = new GameEventData
{
EventType = GameEventType.PlayerMoved,
EventPosition = transform.position,
EventSource = gameObject,
EventIntensity = 0.3f,
Timestamp = System.DateTime.Now
};
EnhancedEventManager.Instance.TriggerEvent(footstepEvent);
}
}
private void MakeNoise()
{
Debug.Log("玩家制造噪音");
GameEventData noiseEvent = new GameEventData
{
EventType = GameEventType.WeaponFired,
EventPosition = transform.position,
EventSource = gameObject,
EventIntensity = 0.5f,
Timestamp = System.DateTime.Now
};
EnhancedEventManager.Instance.TriggerEvent(noiseEvent);
}
private void OnControllerColliderHit(ControllerColliderHit hit)
{
// 玩家碰撞触发事件
if (hit.gameObject.CompareTag("Door"))
{
GameEventData doorEvent = new GameEventData
{
EventType = GameEventType.DoorOpened,
EventPosition = hit.point,
EventSource = gameObject,
EventIntensity = 0.4f,
Timestamp = System.DateTime.Now
};
EnhancedEventManager.Instance.TriggerEvent(doorEvent);
}
}
}
5.3.4 感知范围可视化与调试系统
创建一个调试系统,用于可视化AI的感知范围和信息:
using UnityEngine;
public class AIDebugVisualizer : MonoBehaviour
{
[SerializeField]
private AISoldier aiSoldier;
[SerializeField]
private bool showVisualRange = true;
[SerializeField]
private bool showAuditoryRange = true;
[SerializeField]
private bool showMemoryMarkers = true;
[SerializeField]
private bool showItemDetectionRanges = false;
private void OnDrawGizmos()
{
if (aiSoldier == null || !Application.isPlaying)
{
return;
}
// 获取感知系统组件
VisualPerceptionSystem visualPerception =
aiSoldier.GetComponent<VisualPerceptionSystem>();
AuditoryPerceptionSystem auditoryPerception =
aiSoldier.GetComponent<AuditoryPerceptionSystem>();
MemoryPerceptionSystem memoryPerception =
aiSoldier.GetComponent<MemoryPerceptionSystem>();
ItemPerceptionSystem itemPerception =
aiSoldier.GetComponent<ItemPerceptionSystem>();
if (showVisualRange && visualPerception != null)
{
DrawVisualPerception(visualPerception);
}
if (showAuditoryRange && auditoryPerception != null)
{
DrawAuditoryPerception(auditoryPerception);
}
if (showMemoryMarkers && memoryPerception != null)
{
DrawMemoryMarkers(memoryPerception);
}
if (showItemDetectionRanges && itemPerception != null)
{
DrawItemDetectionRanges(itemPerception);
}
// 绘制AI当前状态
DrawAIState(aiSoldier);
}
private void DrawVisualPerception(VisualPerceptionSystem visualPerception)
{
// 使用反射获取私有字段(在生产环境中应该提供公共方法)
#if UNITY_EDITOR
System.Reflection.FieldInfo sightRangeField =
typeof(VisualPerceptionSystem).GetField(
"sightRange",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance
);
System.Reflection.FieldInfo fieldOfViewField =
typeof(VisualPerceptionSystem).GetField(
"fieldOfView",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance
);
if (sightRangeField != null && fieldOfViewField != null)
{
float sightRange = (float)sightRangeField.GetValue(visualPerception);
float fieldOfView = (float)fieldOfViewField.GetValue(visualPerception);
// 绘制视锥
DrawVisionCone(visualPerception.transform, sightRange, fieldOfView, Color.green);
}
#endif
}
private void DrawVisionCone(Transform observer, float range, float fov, Color color)
{
int segments = 20;
float stepAngle = fov / segments;
float halfFov = fov / 2;
Vector3 forward = observer.forward * range;
Vector3 lastPoint = observer.position;
Gizmos.color = new Color(color.r, color.g, color.b, 0.1f);
// 绘制视锥扇形
for (int i = 0; i <= segments; i++)
{
float angle = -halfFov + (stepAngle * i);
Vector3 direction = Quaternion.Euler(0, angle, 0) * forward;
Vector3 point = observer.position + direction;
if (i > 0)
{
Gizmos.DrawLine(observer.position, point);
}
if (i > 0 && i < segments)
{
Gizmos.DrawLine(lastPoint, point);
}
lastPoint = point;
}
// 绘制中心线
Gizmos.color = new Color(color.r, color.g, color.b, 0.3f);
Gizmos.DrawRay(observer.position, forward);
}
private void DrawAuditoryPerception(AuditoryPerceptionSystem auditoryPerception)
{
#if UNITY_EDITOR
System.Reflection.FieldInfo hearingRangeField =
typeof(AuditoryPerceptionSystem).GetField(
"hearingRange",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance
);
if (hearingRangeField != null)
{
float hearingRange = (float)hearingRangeField.GetValue(auditoryPerception);
Gizmos.color = new Color(0, 0, 1, 0.05f);
Gizmos.DrawSphere(auditoryPerception.transform.position, hearingRange);
Gizmos.color = new Color(0, 0, 1, 0.2f);
Gizmos.DrawWireSphere(auditoryPerception.transform.position, hearingRange);
}
#endif
}
private void DrawMemoryMarkers(MemoryPerceptionSystem memoryPerception)
{
// 在实际实现中,这里应该调用MemoryPerceptionSystem的公共方法
// 获取记忆的位置并绘制标记
Gizmos.color = new Color(1, 0.5f, 0, 0.3f);
// 这里只是示例,实际应该从memoryPerception获取数据
// 绘制一些测试标记
for (int i = 0; i < 5; i++)
{
Vector3 memoryPosition = memoryPerception.transform.position +
new Vector3(
Mathf.Cos(i * 72 * Mathf.Deg2Rad) * 5,
0,
Mathf.Sin(i * 72 * Mathf.Deg2Rad) * 5
);
Gizmos.DrawSphere(memoryPosition, 0.3f);
}
}
private void DrawItemDetectionRanges(ItemPerceptionSystem itemPerception)
{
// 类似地,绘制物品检测范围
Gizmos.color = new Color(1, 1, 0, 0.1f);
Gizmos.DrawWireSphere(itemPerception.transform.position, 10.0f);
}
private void DrawAIState(AISoldier ai)
{
#if UNITY_EDITOR
// 在AI上方绘制状态信息
UnityEditor.Handles.Label(
ai.transform.position + Vector3.up * 3,
$"状态: {ai.CurrentState}\n" +
$"目标: {(ai.CurrentTarget != null ? ai.CurrentTarget.name : "无")}"
);
#endif
}
}
5.4 游戏运行结果与分析
5.4.1 测试场景运行结果
当运行包含上述系统的Unity场景时,可以观察到以下行为:
-
AI士兵的巡逻行为:
- AI士兵按照预设的巡逻点循环移动
- 在每个巡逻点停留几秒,模拟观察行为
- 巡逻路径在场景中可视化显示
-
视觉感知效果:
- 当玩家进入AI的视野范围时,AI立即发现玩家
- 视野范围受障碍物遮挡影响
- AI会面向玩家并进入战斗状态
-
听觉感知效果:
- 玩家开火或制造噪音时,AI会听到声音
- AI会前往声音来源位置进行调查
- 声音强度随距离衰减
-
触觉感知效果:
- 当玩家子弹击中AI时,AI会感知到伤害
- AI会记录伤害来源并做出相应反应
-
物品感知效果:
- AI会检测到场景中的血包、弹药等物品
- 根据物品重要性,AI可能中断当前行为去收集物品
-
记忆系统效果:
- AI记住玩家的最后已知位置
- 即使玩家离开视野,AI仍会前往最后已知位置搜索
- AI记住重要事件和位置
5.4.2 性能优化建议
在实际游戏项目中,感知系统可能涉及大量计算,以下是一些优化建议:
-
分层更新:
- 不是所有AI都需要每帧更新感知
- 根据AI的重要性或距离玩家远近,设置不同的更新频率
-
空间分区:
- 使用四叉树、八叉树或网格系统管理场景中的对象
- 只检测附近分区中的对象,减少检测数量
-
异步处理:
- 将昂贵的感知计算(如光线投射)放在单独的线程或协程中
- 使用Job System和Burst Compiler进行高性能计算
-
细节层次(LOD):
- 根据AI与玩家的距离,使用不同精度的感知
- 远处的AI使用简化的感知逻辑
-
事件聚合:
- 合并短时间内发生的多个同类事件
- 避免重复处理相似的事件
5.4.3 商业项目应用扩展
在实际商业项目中,感知系统可以进一步扩展:
-
团队感知共享:
- AI队友之间共享感知信息
- 实现团队协作和战术配合
-
环境互动感知:
- AI感知环境变化(如灯光、天气、时间)
- 根据环境调整行为模式
-
学习与适应:
- AI学习玩家的行为模式
- 根据历史互动调整感知策略
-
情绪影响感知:
- AI的情绪状态影响感知能力
- 愤怒时可能扩大攻击范围,恐惧时可能缩小感知范围
-
多感官融合:
- 整合多种感官信息,形成更完整的环境认知
- 不同感官信息相互验证,提高感知可靠性
通过本章介绍的综合感知系统,开发者可以在Unity中创建出具有高度真实感和沉浸感的AI角色。这些系统不仅适用于第一人称射击游戏,还可以通过调整参数和逻辑,应用于潜行游戏、角色扮演游戏、实时战略游戏等多种游戏类型。关键是理解每种感知机制的原理,并根据具体游戏需求进行适当的调整和扩展。
更多推荐


所有评论(0)