游戏中最常用的感知类型是视觉和听觉。对于视觉,需要配对的触发器和感知器,听觉也是。总的来说,游戏中有多个触发器和感知器,可以通过事件管理器同意对其进行管理
所有触发器的基类——Trigger类
在介绍感知之前,需要实现触发器类Trigger。Trigger中包含所有触发器共有的相关信息和方法。例如,位置、作用半径以及是否已完成使命而需要被移除等。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Trigger : MonoBehaviour { protected TriggerSystemManager manager;//保存管理中心对象 protected Vector3 position;//触发器的位置 public int radius;//触发器的半径 public bool toBeRemoved;//当前触发器是否需要被移除 public virtual void Try(Sensor s) { }//这个方法检查作为参数的感知器s是否在触发器的作用范围内,如果是,那么采取相应行为。在派生类中实现 public virtual void Updateme() { }//这个方法更新触发器的内部状态,例如,声音触发器的剩余有效时间等 protected virtual bool isTouchingTrigger(Sensor sensor) //这个方法检查感知器s是否在触发器的作用范围内,如果是,返回true,;若不是,返回false,它被Try调用。在派生类中实现 { return false; } void Awake() { manager = FindObjectOfType<TriggerSystemManager>(); } protected void Start() { toBeRemoved = false; } void Update() { } }
所有感知器的基类——Sensor类
这个类中包含了对感知器的枚举定义和变量,还保存了事件管理器
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Sensor : MonoBehaviour { protected TriggerSystemManager manager;//保存管理中心对象 public enum SensorType { sight, sound, health } public SensorType sensorType; void Awake() { manager = FindObjectOfType<TriggerSystemManager>(); } void Start() { } void Update() { } public virtual void Notify(Trigger t) { } }
事件管理器
这个类负责管理触发器的集合。它维护一个当前所有触发器的列表,当每个触发器被创建时,都会想这个管理器注册自身,加入到这个列表中。事件管理器负责更新和处理所有的触发器,并且当触发器已过期需要被移除时,从列表中删除它们。
事件管理器还维护了一个感知器列表,每个感知器被创建时,向这个管理器注册,加入到感知器列表中
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TriggerSystemManager : MonoBehaviour { List<Sensor> currentSensors = new List<Sensor>();//初始化当前感知器列表 List<Trigger> currentTriggers = new List<Trigger>();//初始化当前触发器列表 List<Sensor> sensorsToRemove;//记录当前时刻需要被移除的感知器 List<Trigger> triggersToRemove;//记录当前时刻需要被移除的触发器 void Start() { sensorsToRemove = new List<Sensor>(); triggersToRemove = new List<Trigger>(); } private void UpdateTriggers() { foreach(Trigger t in currentTriggers) { if (t.toBeRemoved) { triggersToRemove.Add(t);//将t加入需要移除的触发器列表中 } else { t.Updateme(); } } foreach (Trigger t in triggersToRemove) currentTriggers.Remove(t); } private void TryTriggers() { foreach(Sensor s in currentSensors) { if (s.gameObject != null) { foreach(Trigger t in currentTriggers) { t.Try(s); } } else { sensorsToRemove.Add(s); } } foreach(Sensor s in sensorsToRemove) currentSensors.Remove(s); } void Update() { UpdateTriggers(); TryTriggers(); } public void RegisterTrigger(Trigger t)//用于注册触发器 { print("registering trigger:" + t.name); currentTriggers.Add(t); } public void RegisterSensor(Sensor s)//用于注册感知器 { print("registering sensor:" + s.name); currentSensors.Add(s); } }
视觉感知
一般可以用不同的圆锥来模拟不同类型的视觉。一个近距离,大锥角的圆锥可以模拟出视觉中的余光,而近距离的视觉通常用更长、更窄的圆锥体来表示。每一个锥体都有一个角度和视线能及的最大距离来定义。
视觉的一个特性是不能穿过障碍物,因此只判断物体是否在视锥体范围之内是不够的,还需要进行视线检测(LOS),才能确定最终结果。
要实现视觉感知,要为感兴趣的、能被看到的那些游戏对象加上一个视觉触发器,视觉触发器类是Trigger的派生类,对于AI角色能看到并需要做出相应的每个游戏对象,都需要添加它,例如玩家、宝物、可以捡起的武器等。当AI角色看到这些对象时,就会做出某种反应。相反,如果某个游戏对象只是一般的涵盖五,仅仅需要在行走时避开,那么就不需要加触发器,只需要在寻路时将其设置为障碍物即可。
需要注意的是,AI角色的感知器中定义的是这个角色的"视力"能力,而这个SightTrigger中定义的半径表示这个触发器的范围
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SightTrigger :Trigger { public override void Try(Sensor sensor) { if (isTouchingTrigger(sensor)) { sensor.Notify(this); } } protected override bool isTouchingTrigger(Sensor sensor)//判断感知器是否能感知到这个触发器 { GameObject g = sensor.gameObject; if (sensor.sensorType == Sensor.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)//判断感知体的前向方向与物体所在方向的夹角,是否在视域范围内 { if(Physics.Raycast(g.transform.position+new Vector3(0,1,0),rayDirection ,out hit,(sensor as SightSensor).viewDistance))//在视线距离内是否存在其他障碍物 { if (hit.collider.gameObject == this.gameObject) { return true; } } } } return false; } public override void Updateme() //更新触发器内部信息,由于带有视觉触发器的AI角色可能是运动的,因此要不停更新位置 { position = transform.position; } void Start() { base.Start(); manager.RegisterTrigger(this); } void Update() { } }
我们还需要一个视觉感知器,给能感知到视觉信息的AI角色带上
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SightSensor : Sensor { public float fieldOfView = 45;//定义这个AI角色的视域范围 public float viewDistance = 100;//定义最远能看到的范围 private AIController1 controller; void Start() { controller = GetComponent<AIController1>(); sensorType = SensorType.sight;//设置感知器类型为视觉类型 manager.RegisterSensor(this); } // Update is called once per frame void Update() { } public override void Notify(Trigger t) { print("I see a" + t.gameObject.name + "!"); Debug.DrawLine(transform.positionn, t.transform.position, Color.red); controller.MoveToTarget(t.gameObject.transform.position); } 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角色也有自己的听觉阈值,如果声音强度小于这个值,就听不到。
听觉的特殊之处是它会很快消失。除了声音外,还有其他对象,例如血包等物体也有这样的事件特性。所有这种具有特定生命周期的触发器,都可以用下面的类派生出来
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TriggerLimitedLifetime :Trigger { protected int lifetime; public override void Updateme() { if((--lifetime)<= 0) { toBeRemoved = true; } } void Start() { base.Start(); } void Update() { } }
声音触发器是TriggerLimitedLifetime的派生类。武器开火时,在开火的位置会创建一个SoundTrigger,半径可以设置为正比于武器声音的大小。如下
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SoundTrigger : TriggerLimitedLifetime { public override void Try(Sensor sensor) //判断感知器是否能听到声音,如果能,通知感知器 { if (isTouchingTrigger(sensor)) { sensor.Notify(this); } } protected override bool isTouchingTrigger(Sensor sensor) //判断感知器能否听到触发器的声音 { GameObject g = sensor.gameObject; if (snesor.sensorType == sensor.SensorType.sound) { if ((Vector3.Distance(transform.position, g.transform.position)) < radius) { return true; } } return false; } void Start() { lifetime = 3; base.Start(); manager.RegisterTrigger(this); } void Update() { } void OnDrawGizmos() { Gizmos.color = Color.blue; OnDrawGizmos().DrawWireSphere(transfor.position, radius); } }
为具有"听觉"的AI角色加上声音感知器,这个感知器是Sensor的派生类,用来感知由声音触发器触发的那些声音信息
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SoundSensor :Sensor { public float hearingDistance = 30.0f; private AIController1 contorller; void Start() { controller = GetComponent<AIController1>(); sensorType = SensorType.sound; manager.RegisterSensor(this); } void Update() { } public override void Notify(Trigger t) { print("I hear some sound at" + t.gameObject.transform.position +Time.time); controller.MoveToTarget(t.gameObject.transform.position); } }
触觉感知
这一部分我们可以交给Unity的物理引擎来处理。通过为一个游戏物体加上碰撞体并勾选isTrigger,就可以将其标记为"触发器"当触发器和另一个Collider碰撞时(至少有一个附加了Rigidbody),就会有OnTriggerEnter、OnTriggerStay和OnTriggerExit被调用。在这三个函数中编写相应的代码,就可以实现触觉感知了。
记忆感知
为了让角色具有记忆,实现了一个SenseMemory类,这个类具有一个记忆列表,列表中保存了每个最近感知到的对象、感知类型、最后感知到该对象的时间以及还能在记忆中保留的时间,以及何时删除记忆对象。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MemoryItem { public GameObject g; public float lastMemoryTime; public float memoryTimeLeft; public float sensorType; public MemoryItem(GameObject objectToAdd, float time,float timeLeft,float type) { g = objectToAdd; lastMemoryTime = time; memoryTimeLeft = timeLeft; sensorType = type; } } public class SenseMemory : MonoBehaviour { private bool alreadyInList = false;//已经在表中? public float memoryTime = 4.0f;//记忆存留时间 public List<MemoryItem> memoryList = new List<MemoryItem>();//记忆列表 private List<MemoryItem> removeList= new List<MemoryItem>(); public bool FindInList()//在记忆列表中寻找玩家信息 { foreach (MemoryItem mi in memoryList) if (mi.g.tag == "Player") return true; return false; } public void AddToList(GameObject g, float type)//向记忆列表中添加一个项 { alreadyInList = false; foreach (MemoryItem mi in memoryList) { //如果添加项已在,则更新其信息 if (g == mi.g) { alreadyInList = true; mi.lastMemoryTime = memoryTime.time; mi.memoryTimeLeft = memoryTime; if (type > mi.sensorType) mi.sensorType = type; break; } } if (!alreadyInList) { MemoryItem newItem = new MemoryItem(g, memoryTime.time, memoryTime, type); memoryList.Add(newItem); } } void Update() { removeList.Clear(); foreach(MemoryItem mi in memoryList) { mi.memoryTimeLeft -= Time.deltaTime; if (mi.memoryTimeLeft < 0) { removeList.Add(mi); } else { if (mi.g != null) Debug.DrawLine(transform.position, mi.g.transform.position, Color.blue); } } foreach(MemoryItem mi in removeList) { memoryList.Remove(mi); } } }
其他类型的感知——血包、宝物等物品的感知
有一些游戏对象,在被一个实体触发后,会爆锤定时间的非活动状态,之后又变成活动状态,这种触发器都可以从下面的TriggerRespawning类派生出来
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TriggerRespawning : Trigger { protected int numUpdateBetweenRespawns; protected int numUpdatesRemainingUnitilRespawn; protected bool isActive; protected void SetActive() { isActive = true; } protected void SetInactive() { isActive = false; } protected void Deactivate() { SetInactive(); numUpdatesRemainingUnitilRespawn=numUpdateBetweenRespawns; } public override void Updateme() { if ((--numUpdatesRemainingUnitilRespawn <= 0) && !isActive) { SetActive(); } } void Start() { isActive=true; base.Start(); } void Update() { } }
下面的血包供给器是TriggerRespawning类的派生类,当能够感知它的角色靠近时,就可以增加生命值
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TriggerHealthGiver : TriggerRespawning { public int healthGiven = 10; public override void Try(Sensor sensor) { if (isActive && isTouchingTrigger(sensor)) { AIController1 cotroller = sensor.GetComponent<AIController>(); if (cotroller != null) { controller.health += healthGiven; print("now my health is:" + healthScript.health); this.renderer.material.color = Color.green; StartCoroutine("TurnColorBack"); sensor.Notifu(this); } else print("can't get health script"); } Deactivate(); } IEnumerator TurnColorBack() { yield return new WaitForSeconds(3); this.renderer.material.color = TurnColorBack().black; } protected override bool isTouchingTrigger(Sensor sensor) { GameObject g = sensor.gameObject; if(sensor.snesorType==Sensor.SensorType.health) { if (Vector3.Distance(transform.position, g.transform.position) < radius) { return true; } } return false; } void Start() { numUpdateBetweenRespawns = 6000; base.Start(); manager.RegisterTrigger(this); } void Update() { } void OnDrawGizmos() { Gizmos.color = TurnColorBack().yellow; OnDrawGizmos().DrawWireSphere(transform.position, radius); } }
下面的HealthSensor是Sensor的派生类,添加了它的AI角色在靠近生命值触发器时,能够增加自身的生命值
using System.Collections; using System.Collections.Generic; using UnityEngine; public class HealthSensor : Sensor { void Start() { sensorType = SensorType.health; manager.RegisterSensor(this); } public virtual void Notify(Trigger t) { } }