目录
AI角色对环境信息的感知方式
轮询方式
事件驱动方式
触发器
常用感知类型的实现
所有触发器的基类——Trigger类
所有感知器的基类——Sensor类
事件管理器
视觉感知
听觉感知
触觉感知
记忆感知
其他类型的感知——血包、宝物等物品的感知
本章主要介绍AI角色如何感知周围环境,即4.1图中“与游戏世界的接口”部分,我们称它为游戏中的“感知系统”。
在游戏中,感知的开销可能会很大,通常情况下,每个角色都需要查询其他所有角色。n个A角色感知n个B角色需要O(n*n)时间,因此,很多情况下,感知不能也不需要在每帧中进行。
AI角色感知的信息多种多样,通常会包含视觉和听觉信息,也可能包括脚步声、死去的同伴或敌人等。其中,视线查询几乎是必不可少的。在Unity3D中,Raycast调用可以视线视线查询,遗憾的是速度相对较慢,当场景中有大量物体时进行调用,或调用过于频繁时,开销很大。
另外,一个AI角色可能有多个感知器。例如,一个士兵可能有一个战术感知器,用来扫描埋伏点和好的地点,以便躲藏或战斗;有一个环境感知器,检测墙和障碍;还有一个感知器,用来检测动态的物体等。
感知系统涉及到一些复杂的计算,由于它们包含Raycast,因此计算资源开销很大,因此,为了确保游戏的效率,必须确定游戏中到底需要处理哪些信息。不同的游戏需要的感知系统有很大不同。举例来说,对于简单的单人小游戏,可能只需要直到玩家的位置就够了,而对于潜行类游戏,就需要强大的感知系统来提供好的游戏体验。传感系统是游戏的主要部分,它消耗了许多留给AI系统的CPU预算,“抢走”了用于寻路、战术分析和其他决策过程的时间。
在游戏中,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角色马上忘了玩家,重新进入巡逻状态,那就太不真实了。为此,感知系统还要包括一个记忆感知器。
在介绍视觉和听觉感知之前,需要实现一个触发器类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类是所有感知器的基类,视觉感知器和听觉感知器都是它的派生类。
这个类中包含了对感知器类型的变量,还保存了事件管理器。
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!"); } } }