Unity人工智能编程精粹学习笔记 AI角色对游戏世界的感知
作者:互联网
目录
本章主要介绍AI角色如何感知周围环境,即4.1图中“与游戏世界的接口”部分,我们称它为游戏中的“感知系统”。
在游戏中,感知的开销可能会很大,通常情况下,每个角色都需要查询其他所有角色。n个A角色感知n个B角色需要O(n*n)时间,因此,很多情况下,感知不能也不需要在每帧中进行。
AI角色感知的信息多种多样,通常会包含视觉和听觉信息,也可能包括脚步声、死去的同伴或敌人等。其中,视线查询几乎是必不可少的。在Unity3D中,Raycast调用可以视线视线查询,遗憾的是速度相对较慢,当场景中有大量物体时进行调用,或调用过于频繁时,开销很大。
另外,一个AI角色可能有多个感知器。例如,一个士兵可能有一个战术感知器,用来扫描埋伏点和好的地点,以便躲藏或战斗;有一个环境感知器,检测墙和障碍;还有一个感知器,用来检测动态的物体等。
感知系统涉及到一些复杂的计算,由于它们包含Raycast,因此计算资源开销很大,因此,为了确保游戏的效率,必须确定游戏中到底需要处理哪些信息。不同的游戏需要的感知系统有很大不同。举例来说,对于简单的单人小游戏,可能只需要直到玩家的位置就够了,而对于潜行类游戏,就需要强大的感知系统来提供好的游戏体验。传感系统是游戏的主要部分,它消耗了许多留给AI系统的CPU预算,“抢走”了用于寻路、战术分析和其他决策过程的时间。
AI角色对环境信息的感知方式
在游戏中,AI角色可以通过两种方式获得游戏世界的信息——轮询和事件驱动。简略的说,轮询是通过积极地观察世界地方式来获得信息,事件驱动是通过坐等消息地方式来获得信息。
例如,想象一个导弹爆炸地瞬间,引起地区域破坏影响到大约15个左右地游戏对象,如果让每个游戏对象周期性地查询是否附近有爆炸发生,就是轮询;如果让爆炸的导弹告诉每个游戏对象它被击中了以及击中的程度,这就是事件驱动。
轮询方式
很显然,如果想知道周围的世界发生了什么,最简单的方式就是去“查询”,如果角色想知道周围的世界发生了什么,最简单的方式就是去“查询”。如果角色想知道周围有没有其他AI角色,它可以在代码中直接查找所有AI角色,看看它们是否在附近。这种主动查找感兴趣的信息的方式,就是轮询。这个过程很快也很容易实现,AI角色知道它对哪些事件感兴趣,并且查询相应的信息,不需要什么特别的框架。
但是,当可能感兴趣的事件数量增加时,AI角色就要花大量的时间用于查询,并且查询返回的大部分信息都是无用信息,而且很难调试。
一种让基于轮询的感知系统更容易维护的方式是建立一个轮询中心,在这里进行所有的查询。有时,采用轮询是最好的选择,例如,如果AI角色想检测玩家是否接近,那么直接查询玩家的当前位置就可以了,但有些情况下,还有更好的方式。
事件驱动方式
在Unity3D中,如果想知道附近是否有AI角色,有一种方式可以很容易地实现。这种方法利用了Unity3D地物理引擎,为AI角色(或它的子物体)添加一个大半径(这个半径与AI角色自身尺寸无关,而取决于它的感知范围)的Collider组件,选中IsTrigger,当Unity3D的物体引擎检测到碰撞时,就会自动调用OnTriggerEnter函数,这样,只需要在OnTriggerEnter()函数中写出相应的代码就可以了。
这种方式可以看作是事件驱动的。在事件驱动的感知系统中,有一个中心检测系统,它查找角色感兴趣的事件是否发生。当事件发生时,它会通知到每个角色,这可以看作时某种事件传递机制。例如,当场景中突然响起了枪声,那么中心检测系统会检测到它,然后通知在枪声附近的所有角色,这些角色再做出相应的反应。
这个中心检测系统可以称为“事件管理器”,它记录每个AI角色所感兴趣的事件,并负责检查、处理和分发事件。由于条件和检查都是集中完成的,因此采用这种方式可以很方便地进行记录和显示相关信息,非常有利于调试。
实现时,由于可能发生的事件多种多样,而且它们的检测方式也是多种多样的,因此,一种选择是采用多个专用的事件管理器,每种事件管理器只处理特定类型的信息,例如碰撞、声音或开关状态等,也只有少量的监听者。另一种选择是采用通用的事件管理器,能够处理各种不同类型的信息。
另外,事件检测机制与事件管理器也常常分开实现。检测机制也可有不同的实现方法。
一种可能的事件检测方法是采用独立的代码,以固定的频率检测事件是否发生。如果事件发生,就向事件管理器发送一个事件。这种机制相当于轮询游戏世界的状态,然后将查询结果与感兴趣的所有AI角色分享。
另一种可能的事件检测方法是基于“触发器”的,可以认为,触发器是我们希望AI角色能做出反应的任何“刺激源”,换句话说,是它们触发了AI角色感兴趣的事件,因此,可以直接由它们通知事件管理器发生了某些事件。
事件可能是多种多样的,例如视觉信息、声音、触觉等,采用这种机制时,对事件感兴趣的角色通常称为“监听者”,因为它们正在“倾听”事件的发生。每个“listener”必须事先向事件管理器“注册”,告知事件管理器它对哪些事件感兴趣,以便事件管理器只对它感兴趣的事件通知它,而忽略它不感兴趣的那些事件。
要通知“listener”事件的发生,最简单常用的方法就是以事件为参数,调用某个函数,例如某个类中的一个方法。
触发器
触发器这个概念是与事件驱动系统相对应的,正如之前介绍过的,触发器是AI角色能对其做出反应的任何“刺激源”,是它们触发了AI角色感兴趣的事件。例如,听觉或视觉刺激。例如枪声、爆炸、临近的敌人或尸体,也可能由游戏中的非AI角色产生。许多触发器具有这样的特性,即当游戏实体进入触发器所在的范围内时,这个触发器就会被触发。触发器范围一般是以触发器为中心的一个区域,在二维游戏中通常是圆形或矩形的,在三维游戏中通常是球体、立方体或圆柱体的。
在游戏设计中、触发器是非常常见的,可以用它们创建各种事件和行为。
- 当玩家射击时,一个声音触发器就被添加到场景中,这样,周围的AI角色会注意到枪声,决定是否逃避或赶来参与战斗。
- 当玩家打倒一个护卫,护卫倒在地下时,相应的视觉触发器使得其他靠近的AI角色对尸体做出反应,决定避开这个区域或上前查看等。
- 门的手柄可以是一个接触触发器,当玩家触碰到它时,门就会被打开。
- AI角色沿着昏暗的走廊走向某个地方,地面是对压力敏感的,这样随着AI角色的走动,触发器发出脚步声的回响。
- 在雪地上行走的角色会留下脚印,当角色被击中而逃走时可能会留下血迹,这些都可以是视觉触发器,AI角色可以沿着脚印或血迹追逐角色。
如果只考虑模拟人的感觉,那么上面提到的触发器似乎已经够了,味觉和嗅觉在游戏中很少使用,而且也可以模拟听觉感知的方式实现。但是游戏中还有一些其他种类的触发器。例如:
- 时间相关的触发器。
- 来自输入接口的触发器。
- 当玩家开采资源或建造单位达到一个值后触发,发生某些事情。
- 当一个单位发生事故后触发,比如,死亡、被攻击、升级、释放一项节能、购买物品等。
- 单位进入或离开特定区域时触发。
- 指定单位的生命值在某个值以上或以下时触发,可以用于设定剧情。
由于每个AI角色的特定和能力不同,AI角色可以自己决定对哪些触发器做出反应,而忽略另一些触发器。
常用感知类型的实现
游戏中最常用的感知类型是视觉和听觉。对于视觉,需要配对的视觉触发器和听觉感知器,为了实现听觉,需要配对的声音触发器和声音感知器。总的来说,游戏中有多个触发器以及多个感知器,可以通过一个管理中心——事件管理器,统一对它们进行管理。
另外,游戏中还常常需要模拟人的记忆。例如,如果玩家为了躲避AI角色的射击,向右跨一步,躲到墙的后面,如果AI角色马上忘了玩家,重新进入巡逻状态,那就太不真实了。为此,感知系统还要包括一个记忆感知器。
所有触发器的基类——Trigger类
在介绍视觉和听觉感知之前,需要实现一个触发器类Trigger。这个类是所有触发器的基类,视觉触发器和听觉触发器都是它的派生类。
Trigger类包含所有触发器共有的相关信息和方法,例如,触发器当前的位置触发器的作用半径(假设是一个以触发器为中心的圆)以及这个触发器是否完成使命而被移除等。
using UnityEngine;
namespace AI.Sensor
{
public class Trigger : MonoBehaviour
{
//保存管理中心对象
protected TriggerSystemManager _manager;
//触发器的位置
protected Vector3 _position;
//触发器的半径
public int Radius;
//当前触发器是否需要被移除
public bool ToBeRemoved;
/// <summary>
/// 这个方法检查作为参数的感知器是否在触发器的作用范围内
/// (或当前触发器是否能真正被感知器s感知到),如果是,那么采取相应
/// 的行动,这个方法在派生类中实现
/// </summary>
/// <param name="s"></param>
public virtual void Try(Sensor s)
{
}
/// <summary>
/// 这个方法更新触发器的内部状态,例如声音触发器的剩余有效时间等
/// </summary>
public virtual void UpdateSelf()
{
}
/// <summary>
/// 这个方法检查作为参数的感知器是否在触发器的作用范围内
/// (或当前触发器是否能真正被感知器s感知到),如果是,返回true
/// 如果不是,返回false,它被Try()调用,需要在派生类中实现
/// </summary>
/// <param name="sensor"></param>
/// <returns></returns>
protected virtual bool IsTouchingTrigger(Sensor sensor)
{
return false;
}
private void Awake()
{
_manager = FindObjectOfType<TriggerSystemManager>();
}
protected void Start()
{
ToBeRemoved = false;
}
}
}
所有感知器的基类——Sensor类
Sensor类是所有感知器的基类,视觉感知器和听觉感知器都是它的派生类。
这个类中包含了对感知器类型的变量,还保存了事件管理器。
namespace AI.Sensor
{
public enum SensorType
{
Sight,
Sound,
Health,
}
}
using UnityEngine;
namespace AI.Sensor
{
public class Sensor : MonoBehaviour
{
protected TriggerSystemManager _manager;
public SensorType SensorType;
private void Awake()
{
_manager = FindObjectOfType<TriggerSystemManager>();
}
public virtual void Notify(Trigger t)
{
}
}
}
事件管理器
这个类负责管理触发器的集合。它维护一个当前所有触发器的列表,当每个触发器被创建时,都会向这个管理器注册自身,加入到这个列表中。事件管理器负责更新和处理所有的触发器,并且当触发器已过期需要被移除时,从列表中删除它们。
事件管理器还维护了一个感知器列表,每个感知器被创建时,向这个管理器注册,加入到感知器列表中。
using System.Collections.Generic;
using UnityEngine;
namespace AI.Sensor
{
public class TriggerSystemManager : MonoBehaviour
{
/// <summary>
/// 初始化当前感知器列表
/// </summary>
private List<Sensor> _currentSensors = new List<Sensor>();
/// <summary>
/// 初始化当前触发器列表
/// </summary>
private List<Trigger> _currentTriggers = new List<Trigger>();
/// <summary>
/// 记录当前时刻需要被移除的感知器,例如感知体死亡,需要移除感知器时
/// </summary>
private List<Sensor> _sensorsToRemove;
/// <summary>
/// 记录当前时刻需要被移除的触发器,例如触发器已过期时
/// </summary>
private List<Trigger> _triggersToRemove;
private void Start()
{
_sensorsToRemove = new List<Sensor>();
_triggersToRemove = new List<Trigger>();
}
private void UpdateTriggers()
{
foreach (var t in _currentTriggers)
{
if (t.ToBeRemoved)
{
_triggersToRemove.Add(t);
}
else
{
t.UpdateSelf();
}
}
foreach (var t in _triggersToRemove)
{
_currentTriggers.Remove(t);
}
}
private void TryTriggers()
{
foreach (var s in _currentSensors)
{
if (s.gameObject != null)
{
foreach (var t in _currentTriggers)
{
t.Try(s);
}
}
else
{
_sensorsToRemove.Add(s);
}
}
foreach (var s in _sensorsToRemove)
{
_currentSensors.Remove(s);
}
}
private void Update()
{
//更新所有的触发器内部状态
UpdateTriggers();
//迭代所有感知器和触发器,做出相应的行为
TryTriggers();
}
/// <summary>
/// 用于注册触发器
/// </summary>
/// <param name="t"></param>
public void RegisterTrigger(Trigger t)
{
Debug.Log($"Register Trigger : {t.name}");
_currentTriggers.Add(t);
}
public void RegisterSensor(Sensor s)
{
Debug.Log($"Register Sensor : {s.name} {s.SensorType}");
_currentSensors.Add(s);
}
}
}
视觉感知
视觉是常见的感觉,玩家可以很容易看出视觉感知部分设计的好坏,这就意味着设计者需要尽量将这部分设计得好一些,让AI角色看上去更加真实。
在对视觉感知要求较高的系统中,可以用不同的圆锥来模拟不同类型的视觉。一个近距离、大锥角的圆锥可以模拟出视觉中的余光,而远距离的视觉通常用更长、更窄的圆锥体来表示。
视锥体是模拟视觉的基本方法,它告诉AI角色在以眼睛为中心,一定锥角范围内有哪些敌人。
视觉的另一个特性是它不能穿过障碍物,因此在眼睛与能看到的物体之间,不能有障碍物的遮挡(暂不考虑障碍物和物体的尺寸)。也就是说,只有判断物体是否在视锥体范围之内是不够的,还需要进行视线测试,才能确定最终的结果。
如果游戏的真实性要求很高,那么亮度也会影响到可视性。
在设计游戏的过程中,需要注意的是,AI角色不能过于聪明,如果突然被不知道哪里冒出来的AI角色所打倒,显然是一件不合理的事情。因此,可以增加限制条件,只有当玩家看到AI角色的情况下,才能让AI能够看到玩家。
为了视线视觉感知,要为感兴趣的、能被看到的那些游戏对象加上一个视觉触发器,视觉触发器(SightTrigger)是Trigger的派生类,对于AI角色能看到并需要做出响应的每个游戏对象,都需要添加它。相反,如果某个游戏对象只是一般的无智能障碍物,例如建筑物等,仅仅需要在行走时避开,而不需要其他特定行为,那么就不需要加上本触发器,而只需要在寻路时将其设置为障碍物就行了。
需要注意的是,AI角色的感知器中定义的是这个角色的“视力”能力,而这个SightTrigger中定义的半径表示这个触发器的影响范围。例如,如果包含这个触发器的游戏对象尺寸很小,那么显然对应小的作用范围,即小的半径,而如果包含这个触发器的游戏对象(例如一个Boss)的体积很大,那么它的作用范围就会很大,对应大的半径。这里为了简化,只考虑了感知器的感知范围,实际中还可以将触发器的影响范围考虑在内。
using UnityEngine;
namespace AI.Sensor
{
public class SightTrigger : Trigger
{
public override void Try(Sensor sensor)
{
//如果感知器能感觉到这个触发器,那么向感知器发出通知,感知器做出相应的
//决策或行动
if (IsTouchingTrigger(sensor))
{
sensor.Notify(this);
}
}
/// <summary>
/// 判断感知器是否能感知到这个触发器
/// </summary>
/// <param name="sensor"></param>
/// <returns></returns>
protected override bool IsTouchingTrigger(Sensor sensor)
{
GameObject g = sensor.gameObject;
//如果能感知视觉信息
if (sensor.SensorType == SensorType.Sight)
{
RaycastHit hit;
Vector3 rayDirection = transform.position - g.transform.position;
rayDirection.y = 0;
//判断感知体的向前方向与物体所在方向的夹角是否在视域范围内
if ((Vector3.Angle(rayDirection, g.transform.forward)) < (sensor as SightSensor).fieldOfView)
{
//在视线范围内是否存在其他障碍物遮挡,如果没有障碍物,则返回true
if (Physics.Raycast(g.transform.position + new Vector3(0, 1, 0), rayDirection, out hit,
(sensor as SightSensor).viewDistance))
{
if (hit.collider.gameObject == gameObject)
{
return true;
}
}
}
}
return false;
}
public override void UpdateSelf()
{
_position = transform.position;
}
private void Start()
{
base.OnStart();
_manager.RegisterTrigger(this);
}
}
}
我们还需要一个视觉感知器,SightSensor是Sensor类的派生类,能够感知到视觉信息的AI角色都需要加上它,用来感知视觉触发器所触发的视觉信息。
using UnityEngine;
namespace AI.Sensor
{
public class SightSensor : Sensor
{
/// <summary>
/// 定义这个AI角色的视域范围
/// </summary>
public float fieldOfView = 45;
/// <summary>
/// 定义这个AI角色最远能看到的距离
/// </summary>
public float viewDistance = 100.0f;
private AIController _controller;
// Start is called before the first frame update
void Start()
{
_controller = GetComponent<AIController>();
SensorType = SensorType.Sight;
_manager.RegisterSensor(this);
}
public override void Notify(Trigger trigger)
{
//当感知器能够真正察觉到某个触发器的信息时被调用,产生相应的行为或做出
//某些决策,这里打印一条信息,在感知体和触发器之间画一条红色连线,然后角色
//走向看到的物体
Debug.Log($"See {trigger.gameObject.name}");
Debug.DrawLine(transform.position, trigger.transform.position, Color.red);
_controller.MoveToTarget(trigger.transform.position);
}
private void OnDrawGizmos()
{
Vector3 frontRayPoint = transform.position + (transform.forward * viewDistance);
float fieldOfViewinRadians = fieldOfView*3.14f/180.0f;
Vector3 leftRayPoint = transform.TransformPoint(new Vector3(viewDistance * Mathf.Sin(fieldOfViewinRadians),0,viewDistance * Mathf.Cos(fieldOfViewinRadians)));
Vector3 rightRayPoint = transform.TransformPoint(new Vector3(-viewDistance * Mathf.Sin(fieldOfViewinRadians),0,viewDistance * Mathf.Cos(fieldOfViewinRadians)));
Debug.DrawLine(transform.position+new Vector3(0,1,0), frontRayPoint+new Vector3(0,1,0), Color.green);
Debug.DrawLine(transform.position+new Vector3(0,1,0), leftRayPoint+new Vector3(0,1,0), Color.green);
Debug.DrawLine(transform.position+new Vector3(0,1,0), rightRayPoint+new Vector3(0,1,0), Color.green);
}
}
}
听觉感知
听觉感知可以用一个球形区域来模拟。另一种方法是当声音被创建时,为它加上一个强度属性,随着传播距离的增加,声音强度会衰减,而每个AI角色也有自己的听觉阈值,如果声音小于这个阈值,AI角色就听不到这个声音,如图4.6所示。
听觉的特殊之处是它很快消失。它的存在会持续一定时间,然后自行消失。例如,某个爆炸声音或枪声,会在持续两秒后消失。
除了声音之外,还有其他对象,例如血包可能也有这样的时间特性。所有这种具有生命周期的触发器,都可以从下面的TriggerLimitedLifetime类派生出来。
namespace AI.Sensor
{
public class TriggerLimitedLifetime : Trigger
{
/// <summary>
/// 该触发器的持续时间
/// </summary>
protected int _lifetime;
public override void UpdateSelf()
{
if (--_lifetime <= 0)
{
ToBeRemoved = true;
}
}
private void Start()
{
base.OnStart();
}
}
}
声音触发器是TriggerLimitedLifetime的派生类,它可以用来通知AI角色其他游戏实体的武器发射声音、爆炸声、窗户被打碎或物体被撞倒的声音(在潜行类游戏中非常重要)等。
例如,当武器开火时,在开火的位置会创建一个SoundTrigger,它的半径(作用范围)可以设置为与武器的声音大小成正比。此时,在一定范围内,且具有声音感知器的感知体就能够“听到”这个声音,并做出反应。
using UnityEngine;
namespace AI.Sensor
{
public class SoundTrigger : TriggerLimitedLifetime
{
/// <summary>
/// 判断感知体是否能够听到触发器发出的声音,如果能,通知感知器
/// </summary>
/// <param name="sensor"></param>
public override void Try(Sensor sensor)
{
if (IsTouchingTrigger(sensor))
{
sensor.Notify(this);
}
}
/// <summary>
/// 通知感知体是否听到声音触发器发出的声音
/// </summary>
/// <param name="sensor"></param>
/// <returns></returns>
protected override bool IsTouchingTrigger(Sensor sensor)
{
//如果感知器能够感知声音
if (sensor.SensorType == SensorType.Sound)
{
GameObject g = sensor.gameObject;
//如果感知体与声音触发器的距离在声音触发器的作用范围内,返回true
if ((Vector3.Distance(transform.position, g.transform.position) < Radius))
{
return true;
}
}
return false;
}
private void Start()
{
//设置该触发器的持续时间
_lifetime = 3;
//调用基类的Start()函数
OnStart();
//将这个触发器加入到管理器的触发器列表中
_manager.RegisterTrigger(this);
}
private void OnDrawGizmos()
{
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, Radius);
}
}
}
为具有“听觉”的AI角色加上声音感知器,这个感知器是Sensor的派生类,用来感知由声音触发器触发的那些声音信息。
using UnityEngine;
namespace AI.Sensor
{
public class SoundSensor : Sensor
{
/// <summary>
/// 定义感知体的听觉范围,这里并没有实际使用
/// </summary>
public float HearingDistance = 30.0f;
private AIController _controller;
private void Start()
{
_controller = GetComponent<AIController>();
//设置感知器类型为声音感知器
SensorType = SensorType.Sound;
//注册感知器
_manager.RegisterSensor(this);
}
public override void Notify(Trigger trigger)
{
//当感知器能够听到触发器的声音时被调用,做出相应行为,这里打印信息,并走向声音的位置
Debug.Log($"Hear Sound{trigger.transform.position} {Time.time}");
_controller.MoveToTarget(trigger.transform.position);
}
}
}
触觉感知
触觉感知可以交给Unity3D的物理引擎来处理。通过为一个游戏物体加上碰撞体,并选中Inspector面板中的IsTrigger属性,就可以把它标记为“触发器”。触发器不受物理引擎的控制,当触发器和另一个Collider发生碰撞时(其中至少有一个附加了Rigidbody组件),会发出3个触发信息,分别是OnTriggerEnter(进入触发器时调用),OnTriggerExit(停止触发器时调用),OnTriggerStay(接触触发器时每帧调用)。在这3个函数中编写相应的代码,就可以实现触觉感知了。
因此,Unity3D已经为触觉感知提供了事件管理器,所以在事件感知器中,不再需要编写触觉相关的代码。
灵活应用触觉感知可以实现许多事件,比如显示提示信息、自动门的开启、生命值供给器、武器供给器等。
记忆感知
为了让角色具有记忆,实现了一个SensorMemory类,这个类具有一个记忆列表,列表中保存了每个最近感知到的对象、感知类型、最后感知到该对象的时间以及还能在记忆中保留的时间,当有一段时间没有感知到这个对象,这个时间超出了记忆时长时,就会将这个对象从记忆列表中删除。
using UnityEngine;
namespace AI.Sensor
{
public class MemoryItem
{
/// <summary>
/// 感知到的游戏对象
/// </summary>
public GameObject ObjectToAdd;
/// <summary>
/// 最近的感知时间
/// </summary>
public float LastMemoryTime;
/// <summary>
/// 还能留存在记忆中的时间
/// </summary>
public float MemoryTimeLeft;
/// <summary>
/// 通过哪种方式感知到的对象,视觉为1,听觉为0.66.
/// </summary>
public float SensorType;
public MemoryItem(GameObject objectToAdd, float lastMemoryTime, float memoryTimeLeft, float sensorType)
{
ObjectToAdd = objectToAdd;
LastMemoryTime = lastMemoryTime;
MemoryTimeLeft = memoryTimeLeft;
SensorType = sensorType;
}
}
}
using System.Collections.Generic;
using UnityEngine;
namespace AI.Sensor
{
public class SensorMemory : MonoBehaviour
{
/// <summary>
/// 以及留存时间
/// </summary>
public float MemoryTime = 4.0f;
/// <summary>
/// 记忆列表
/// </summary>
public List<MemoryItem> MemoryList = new List<MemoryItem>();
/// <summary>
/// 此时需要从记忆列表中删除的项
/// </summary>
private List<MemoryItem> _removeList = new List<MemoryItem>();
/// <summary>
/// 在记忆列表中寻找玩家信息
/// </summary>
/// <returns></returns>
public bool FindInList()
{
foreach (var memoryItem in MemoryList)
{
if (memoryItem.ObjectToAdd.tag == "Player")
{
return true;
}
}
return false;
}
/// <summary>
/// 向记忆列表中添加一个项
/// </summary>
/// <param name="g">物体</param>
/// <param name="type">感知类型</param>
public void AddToList(GameObject g, float type)
{
bool alreadyInList = false;
foreach (var memoryItem in MemoryList)
{
if (g == memoryItem.ObjectToAdd)
{
alreadyInList = true;
memoryItem.LastMemoryTime = Time.time;
memoryItem.MemoryTimeLeft = MemoryTime;
if (type > memoryItem.SensorType)
{
memoryItem.SensorType = type;
}
break;
}
}
if (!alreadyInList)
{
MemoryItem newItem = new MemoryItem(g, Time.time, MemoryTime, type);
MemoryList.Add(newItem);
}
}
private void Update()
{
_removeList.Clear();
//遍历所有项,找到那些超时需要“忘记”的项,删除
foreach (var memoryItem in MemoryList)
{
memoryItem.MemoryTimeLeft -= Time.deltaTime;
if (memoryItem.MemoryTimeLeft < 0)
{
_removeList.Add(memoryItem);
}
else
{
//对没删除的项,画出线,表示仍在记忆中;
if (memoryItem.ObjectToAdd != null)
{
Debug.DrawLine(transform.position, memoryItem.ObjectToAdd.transform.position, Color.blue);
}
}
}
foreach (var removeItem in _removeList)
{
MemoryList.Remove(removeItem);
}
}
}
}
其他类型的感知——血包、宝物等物品的感知
这个感知系统还可以包含其他类型的触发与感知,下面以生命值供给器为例,来说明它在其他方面的应用。
有一些游戏对象,在被一个实体触发后,会保持一定时间的非活动状态,例如,一些角色可以“捡起”的物件,如血包或武器。当它被捡起后,会在一定时间内处于非活动状态,之后又重新变为活动的,可以再次被捡起。
这种触发器都可以从下面的TriggerRespawning类派生出来。
namespace AI.Sensor
{
public class TriggerRespawning : Trigger
{
/// <summary>
/// 两次活跃之间的间隔时间
/// </summary>
protected int _numUpdateBetweenRespawns;
/// <summary>
/// 距离下次再生还需要等待的时间
/// </summary>
protected int _numUpdateRemainingUntilRespwn;
/// <summary>
/// 当前是否为活动状态
/// </summary>
protected bool _isActive;
/// <summary>
/// 设置IsActive为活动状态
/// </summary>
protected void SetActive()
{
_isActive = true;
}
/// <summary>
/// 设置IsActive为非活动状态
/// </summary>
protected void SetInActive()
{
_isActive = false;
}
/// <summary>
/// 将触发器设置为非活动状态
/// </summary>
protected void DeActivate()
{
SetInActive();
//重置剩余时间为两次活跃之间的间隔时间
_numUpdateRemainingUntilRespwn = _numUpdateBetweenRespawns;
}
public override void UpdateSelf()
{
//倒计时,如果距离变为活动时间的剩余时间小于等于0且非活动状态下
if ((--_numUpdateRemainingUntilRespwn <= 0) && !_isActive)
{
//将触发器设置为活动状态
SetActive();
}
}
private void Start()
{
_isActive = true;
base.OnStart();
}
}
}
下面的血包供给器是TriggerRespawning类的派生类,当能够感知它的角色接近它时,就可以增加生命值。
using System.Collections;
using UnityEngine;
namespace AI.Sensor
{
public class TriggerHealthGiver : TriggerRespawning
{
/// <summary>
/// 设置每次增加的生命值
/// </summary>
public int HealthGiver = 10;
private Renderer _renderer;
/// <summary>
/// 检测当前触发器是否是活动的,并且感知器是否在这个触发器的作用范围内
/// </summary>
/// <param name="sensor"></param>
public override void Try(Sensor sensor)
{
if (_isActive && IsTouchingTrigger(sensor))
{
AIController controller = sensor.GetComponent<AIController>();
if (controller != null)
{
//增加生命值
controller.Health += HealthGiver;
Debug.Log($"Now Health is : {controller.Health}");
_renderer.material.color = Color.green;
//调用Coroutine开始计时
//调用感知器的Notify函数,以便感知体做出相应行动
StartCoroutine(TurnColorBack());
sensor.Notify(this);
}
else
{
Debug.Log($"Can't' Get Health");
}
//设置为非激活状态
DeActivate();
}
}
/// <summary>
/// 过3秒之后,生命供给器变为黑色;实际上应该立刻变为非激活状态,为了更容易观察,多等待3s
/// </summary>
/// <returns></returns>
private IEnumerator TurnColorBack()
{
yield return new WaitForSeconds(3);
_renderer.material.color = Color.black;
}
/// <summary>
/// 检查感知器是否在这个触发器的作用范围内
/// </summary>
/// <param name="sensor"></param>
/// <returns></returns>
protected override bool IsTouchingTrigger(Sensor sensor)
{
GameObject g = sensor.gameObject;
//如果感知器能够感觉到health
if (sensor.SensorType == SensorType.Health)
{
//触发器与感知器的距离是否小于触发器的作用半径
if (Vector3.Distance(transform.position,g.transform.position) < Radius)
{
return true;
}
}
return false;
}
private void Start()
{
//设置两次活动状态之间的间隔时间
_numUpdateBetweenRespawns = 6000;
OnStart();
//注册这个触发器
_manager.RegisterTrigger(this);
_renderer = GetComponent<Renderer>();
}
private void OnDrawGizmos()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, Radius);
}
}
}
下面的HealthSensor类是Sensor的派生类,添加了它的AI角色在靠近生命值触发器(如血包时),能够增加自身的生命值
using UnityEngine;
namespace AI.Sensor
{
public class HealthSensor : Sensor
{
private void Start()
{
SensorType = SensorType.Health;
_manager.RegisterSensor(this);
}
public override void Notify(Trigger trigger)
{
Debug.Log($"HealthSensor Notify!");
}
}
}
标签:精粹,触发器,感知器,角色,AI,Unity,感知,public 来源: https://blog.csdn.net/dmk17771552304/article/details/114595423