人类的视觉系统有以下几个特点:
- 距离有限。近处看得清,远处看不清
- 容易被遮挡。不能穿过任何不透明的障碍物
- 视野范围大约为90度。实现正前方信息丰富,具有色彩和细节;实现外侧的部分只有轮廓和运动信息
- 注意力有限。当关注某个具体的方位或物体时,其他部分被忽略,如魔术中的障眼法总是能骗过观众
对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);
}
}
}
但这只是第一步,实际还需要将射线发射出去才可以。只有将射线发射出去才能判断是否碰到了障碍物,如果碰到障碍物,视线端点就落在碰撞点上。将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);
}
}
}
效果图:
修改之后 ,用任意物体阻碍射线,会发现射线确实出现了被阻挡的效果。
在实际游戏中,当射线集中玩家时,就表示敌人发现了玩家,这时就需要进行进一步处理。简单来说,就是把逻辑代码插入上述代码的if代码段里。
以上方法虽然实现了逻辑功能,但没有清晰表现出视野范围。在经典的潜入为游戏中,为了给玩家提示具体的敌人视野范围,会加入明显的画面表现。例如在《崩坏3》中的一些关卡里,就有机器人前面有一个非常夸张的红色扇形,用来显示敌人的视野区域。
下面利用程序建模的方法,将视野范围显示出来,准备工作如下:
- 给敌人新建一个空物体作为子物体,命名为view,位置归0
- 添加Mesh Renderer组件和Mesh Filter组件
- 新建一个材质,将其渲染模式改为Transparent(透明),颜色改为绿色,透明度改为150左右。为检验材质设置可以将它拖曳到立方体上进行测试,观察透明度是否合适
- 将材质球拖曳到新建的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组件即可。
按照上述步骤操作并运行,就会得到一个半透明的绿色扇面模型。而且绿色范围被障碍物阻挡时仍能正确显示范围。