Unity人工智能编程精粹学习笔记 AI角色对游戏世界的感知

2021/4/12 20:55:25

本文主要是介绍Unity人工智能编程精粹学习笔记 AI角色对游戏世界的感知,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

AI角色对环境信息的感知方式

轮询方式

事件驱动方式

触发器

常用感知类型的实现

所有触发器的基类——Trigger类

所有感知器的基类——Sensor类

事件管理器

视觉感知

听觉感知

触觉感知

记忆感知

其他类型的感知——血包、宝物等物品的感知


本章主要介绍AI角色如何感知周围环境,即4.1图中“与游戏世界的接口”部分,我们称它为游戏中的“感知系统”。

在游戏中,感知的开销可能会很大,通常情况下,每个角色都需要查询其他所有角色。n个A角色感知n个B角色需要O(n*n)时间,因此,很多情况下,感知不能也不需要在每帧中进行。

AI角色感知的信息多种多样,通常会包含视觉和听觉信息,也可能包括脚步声、死去的同伴或敌人等。其中,视线查询几乎是必不可少的。在Unity3D中,Raycast调用可以视线视线查询,遗憾的是速度相对较慢,当场景中有大量物体时进行调用,或调用过于频繁时,开销很大。

另外,一个AI角色可能有多个感知器。例如,一个士兵可能有一个战术感知器,用来扫描埋伏点和好的地点,以便躲藏或战斗;有一个环境感知器,检测墙和障碍;还有一个感知器,用来检测动态的物体等。

感知系统涉及到一些复杂的计算,由于它们包含Raycast,因此计算资源开销很大,因此,为了确保游戏的效率,必须确定游戏中到底需要处理哪些信息。不同的游戏需要的感知系统有很大不同。举例来说,对于简单的单人小游戏,可能只需要直到玩家的位置就够了,而对于潜行类游戏,就需要强大的感知系统来提供好的游戏体验。传感系统是游戏的主要部分,它消耗了许多留给AI系统的CPU预算,“抢走”了用于寻路、战术分析和其他决策过程的时间。

 

AI角色对环境信息的感知方式

在游戏中,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角色的走动,触发器发出脚步声的回响。
  • 在雪地上行走的角色会留下脚印,当角色被击中而逃走时可能会留下血迹,这些都可以是视觉触发器,AI角色可以沿着脚印或血迹追逐角色。

如果只考虑模拟人的感觉,那么上面提到的触发器似乎已经够了,味觉和嗅觉在游戏中很少使用,而且也可以模拟听觉感知的方式实现。但是游戏中还有一些其他种类的触发器。例如:

  • 时间相关的触发器。
  • 来自输入接口的触发器。
  • 当玩家开采资源或建造单位达到一个值后触发,发生某些事情。
  • 当一个单位发生事故后触发,比如,死亡、被攻击、升级、释放一项节能、购买物品等。
  • 单位进入或离开特定区域时触发。
  • 指定单位的生命值在某个值以上或以下时触发,可以用于设定剧情。

由于每个AI角色的特定和能力不同,AI角色可以自己决定对哪些触发器做出反应,而忽略另一些触发器。

 

常用感知类型的实现

游戏中最常用的感知类型是视觉和听觉。对于视觉,需要配对的视觉触发器和听觉感知器,为了实现听觉,需要配对的声音触发器和声音感知器。总的来说,游戏中有多个触发器以及多个感知器,可以通过一个管理中心——事件管理器,统一对它们进行管理。

另外,游戏中还常常需要模拟人的记忆。例如,如果玩家为了躲避AI角色的射击,向右跨一步,躲到墙的后面,如果AI角色马上忘了玩家,重新进入巡逻状态,那就太不真实了。为此,感知系统还要包括一个记忆感知器。

 

所有触发器的基类——Trigger类

在介绍视觉和听觉感知之前,需要实现一个触发器类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类

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!");
        }
    }
}

 

 

 

 



这篇关于Unity人工智能编程精粹学习笔记 AI角色对游戏世界的感知的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程