UnityAI——常用感知类型的实现

简介: UnityAI——常用感知类型的实现

游戏中最常用的感知类型是视觉和听觉。对于视觉,需要配对的触发器和感知器,听觉也是。总的来说,游戏中有多个触发器和感知器,可以通过事件管理器同意对其进行管理


所有触发器的基类——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)
    {
 
    }
}
相关文章
|
1月前
|
Python
布尔类型的值和类型
布尔类型的值和类型。
29 0
|
4月前
|
Kubernetes 负载均衡 网络协议
在k8S中,Servic类型有哪些?
在k8S中,Servic类型有哪些?
|
6月前
|
编译器 程序员 语音技术
C++的超20种函数类型分享
C++超20种函数类型:编程语言规定规则,编译器实现预定规则
|
程序员 数据库
软件文档的类型有哪些?
软件文档的类型有哪些?
241 0
|
JavaScript
🎖️typeScrpt中如何返回正确的类型?
条件返回类型确实是 TypeScript 中非常有用的强大功能,它允许您根据参数的类型为函数指定不同的返回类型,从而实现更强的类型安全性。
87 0
|
JavaScript 前端开发
比较不同的类型
比较不同的类型
103 0
|
JSON JavaScript C语言
转换类型的那些事儿
转换类型的那些事儿
120 0
|
Go
类型
类型
174 0
类型和值
类型和值
76 0