Unity——模拟AI视觉

人类的视觉系统有以下几个特点:

  1. 距离有限。近处看得清,远处看不清
  2. 容易被遮挡。不能穿过任何不透明的障碍物
  3. 视野范围大约为90度。实现正前方信息丰富,具有色彩和细节;实现外侧的部分只有轮廓和运动信息
  4. 注意力有限。当关注某个具体的方位或物体时,其他部分被忽略,如魔术中的障眼法总是能骗过观众

对AI视觉的模拟就是基于以上这些基本特点,在此基础上有各种各样的实现思路。如果从第二个特点出发,很容易联想到射线也具有不能穿过物体的特点,而且涉嫌也可以设定发射距离。最大的难点在于“视野范围大约为90°”这一特点,在3D自由视角的游戏中,视野范围是一个圆锥体,在俯视角游戏中视野范围是一个扇形区域。无论圆锥体还是扇形区域,都无法直接用射线、球形射线或盒子射线等简单形状模拟,必须找到一种变通的解决方案。

针对用射线模拟扇形区域的问题,这里给出一个易于理解的方法:用多条射线模拟区域。

脚本和效果图如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AIEnemy : MonoBehaviour
{
    public int viewRadius = 4;  //视野距离
    public int viewLines = 30; //射线数量
    void Start(){ }

   
    void Update()
    {
        FieldOfView();
         
    }
    void FieldOfView()
    {
        //获得最左边那条射线的向量,相对正前方,角度是-45°
        Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
        //依次处理每条射线
        for(int i = 0; i <= viewLines; i++)
        {
            Vector3 v = Quaternion.Euler(0, (90.0f / viewLines) * i, 0) * forward_left;
            //角色位置+v,就是射线终点pos
            Vector3 pos = transform.position + v;
            //从玩家为之到pos画线段,只会在编辑器里看到
            Debug.DrawLine(transform.position, pos, Color.red);
        }
    }
}

 6580e9a0e296452c9a8e469c04c4a939.png

但这只是第一步,实际还需要将射线发射出去才可以。只有将射线发射出去才能判断是否碰到了障碍物,如果碰到障碍物,视线端点就落在碰撞点上。将FieldOfView的代码修改如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AIEnemy : MonoBehaviour
{
    public int viewRadius = 4;  //视野距离
    public int viewLines = 30; //射线数量
    void Start(){ }

   
    void Update()
    {
        FieldOfView();
    }
    void FieldOfView()
    {
        //获得最左边那条射线的向量,相对正前方,角度是-45°
        Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
        //依次处理每条射线
        for(int i = 0; i <= viewLines; i++)
        {
            Vector3 v = Quaternion.Euler(0, (90.0f / viewLines) * i, 0) * forward_left;
            //角色位置+v,就是射线终点pos
            Vector3 pos = transform.position + v;

            //实际发射射线。注意RayCast的参数,重载很多容易搞错
            RaycastHit hitInfo;
            if(Physics.Raycast(transform.position,v,out hitInfo, viewRadius))
            {
                //碰到物体,终点改为碰到的点
                pos = hitInfo.point;
            }

            //从玩家位置到pos画线段,只会在编辑器里看到
            Debug.DrawLine(transform.position, pos, Color.red);
        }
    }
}

效果图:

81d26d83217049f3a3c636cc1f97dce6.png

修改之后 ,用任意物体阻碍射线,会发现射线确实出现了被阻挡的效果。

在实际游戏中,当射线集中玩家时,就表示敌人发现了玩家,这时就需要进行进一步处理。简单来说,就是把逻辑代码插入上述代码的if代码段里。

以上方法虽然实现了逻辑功能,但没有清晰表现出视野范围。在经典的潜入为游戏中,为了给玩家提示具体的敌人视野范围,会加入明显的画面表现。例如在《崩坏3》中的一些关卡里,就有机器人前面有一个非常夸张的红色扇形,用来显示敌人的视野区域。

下面利用程序建模的方法,将视野范围显示出来,准备工作如下:

  1. 给敌人新建一个空物体作为子物体,命名为view,位置归0
  2. 添加Mesh Renderer组件和Mesh Filter组件
  3. 新建一个材质,将其渲染模式改为Transparent(透明),颜色改为绿色,透明度改为150左右。为检验材质设置可以将它拖曳到立方体上进行测试,观察透明度是否合适
  4. 将材质球拖曳到新建的view物体上。由于view物体暂时没有模型,因此显示不出来

由于视野范围是动态变化的,因此需要用代码拼出一个扇形平面模型并赋予view物体,以表现视野范围。由于改动较多,将修改后的完整脚本展示如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AIEnemy : MonoBehaviour
{
    public int viewRadius = 4;  //视野距离
    public int viewLines = 30; //射线数量
    public MeshFilter viewMeshFilter;
    List<Vector3> viewVerts;  //定点列表
    List<int> viewIndices;  //定点序号列表
    void Start()
    {
        Transform view = transform.Find("view");
        viewMeshFilter = view.GetComponent<MeshFilter>();
        viewVerts=new List<Vector3>();
        viewIndices = new List<int>();
    }

   
    void Update()
    {
        FieldOfView();
    }
    void FieldOfView()
    {
        viewVerts.Clear();
        viewVerts.Add(Vector3.zero);  //加入起点坐标,局部坐标系


        //获得最左边那条射线的向量,相对正前方,角度是-45°
        Vector3 forward_left = Quaternion.Euler(0, -45, 0) * transform.forward * viewRadius;
        //依次处理每条射线
        for(int i = 0; i <= viewLines; i++)
        {
            Vector3 v = Quaternion.Euler(0, (90.0f / viewLines) * i, 0) * forward_left;
            //角色位置+v,就是射线终点pos
            Vector3 pos = transform.position + v;

            //实际发射射线。注意RayCast的参数,重载很多容易搞错
            RaycastHit hitInfo;
            if(Physics.Raycast(transform.position,v,out hitInfo, viewRadius))
            {
                //碰到物体,终点改为碰到的点
                pos = hitInfo.point;
            }
            //将每个点的位置加入列表,注意转为局部坐标系
            Vector3 p = transform.InverseTransformPoint(pos);
            viewVerts.Add(p);
         
        }
        //根据顶点绘制模型
        RefreshView();
    }
    void RefreshView()
    {
        viewIndices.Clear();
        //逐个加入三角面,每个三角面都以起点开始
        for(int i = 1; i < viewVerts.Count-1; i++)
        {
            viewIndices.Add(0);
            viewIndices.Add(i);
            viewIndices.Add(i+1);
        }
        //填写Mesh信息
       Mesh mesh = new Mesh();
        mesh.vertices= viewVerts.ToArray();
        mesh.triangles = viewIndices.ToArray();
        viewMeshFilter.mesh = mesh;
    }
}

简单来说,网格信息是由"顶点"和"顶点序号"组成的。顶点是空间位置,因此用Vector3表示,所有顶点放在一个大数组中,每个顶点对应一个数组的下标。

而顶点序号指的正是数组的下标。由于网格是由三角面组成的,因此3个序号为一组,3个又3个地填写顶点序号,就代表着1个又1个的三角面。

三角面的正反:

三角面是由3个顶点序号组成的,而3个点的顺序可能有两种,分别是a-b-c和a-c-b,即顺时针和逆时针。一般的材质默认都是单面渲染,每个面只能从一侧看到,而从另一侧看不到。

只要是顺时针具体点的先后顺序不同是没有影响的,如a-b-c=b-c-a=c-a-b,而c-b-a就是相反的。

如果在写代码时看不到三角面,那么就查看反面是否显示。交换3个点中的任意2个序号的顺序,即可将三角面反向。

准备好顶点列表和顶点序列表后,就可以创建Mesh对象了。Mesh对象的几个属性都是数组类型,因此需要用List.ToArray方法将列表转为数组。最后将Mesh对象赋予Mesh FIlter组件即可。

按照上述步骤操作并运行,就会得到一个半透明的绿色扇面模型。而且绿色范围被障碍物阻挡时仍能正确显示范围。

猜你喜欢

转载自blog.csdn.net/m0_63024355/article/details/132779219