人类的视觉系统有以下几个特点:
- 距离有限。近处看得清,远处看不清
- 容易被遮挡。不能穿过任何不透明的障碍物
- 视野范围大约为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组件即可。
按照上述步骤操作并运行,就会得到一个半透明的绿色扇面模型。而且绿色范围被障碍物阻挡时仍能正确显示范围。