游戏中最常用的感知类型是视觉和听觉。对于视觉,需要配对的触发器和感知器,听觉也是。总的来说,游戏中有多个触发器和感知器,可以通过事件管理器同意对其进行管理
所有触发器的基类——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)
{
}
}