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