关于Untiy编写敌人可视化视野
最近遇到了一个编写可视化视野范围的问题,写出来了以后记录一下可视化视野的方法,因为用到的方法较多,我又想把原理讲解明白,所以本文的篇幅较长,如果你是没有耐心的话,建议退出看其他文章。
首先说一下视野的思路:我们可视化的视野全部都是以扇形显示的,同时为了后期的方便调整我们的视野和距离都必须是动态的。那么我们是不是可以使用度数来控制视野范围,那么我们就需要画出一个扇形。那么我们可以先画出来一个圆 然后在这个圆上面来截取扇形,在画圆之前我们需要定义圆的半径(因为我们的物体在圆的中心,所以圆的半径就代表了视野的纵向长度,代表了物体可以看多远),我们还需要定义一个视野的角度(视野 的角度代表了扇形的大小,表示视野的横向大小)。
设置半径、视野角度的变量,并利用Unity特性把视野利用滑动条显示出来,把角度限制在0~360°之后就是画圆,然后在圆上面截取扇形(写在Editor文件夹中)。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
//自定义编辑器
[CustomEditor(typeof(FieldOfView))]
public class FieldOfViewEditor : Editor
{
private void OnSceneGUI()
{
FieldOfView fow = (FieldOfView)target;
//画的颜色为白色
Handles.color = Color.white;
//画一个线弧(圆的中心,圆的法线,开始的中心角度开始的地方,弧度、旋转的度数,圆的半径)
Handles.DrawWireArc(fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);
//把视野角度的一般转为Vector3向量
Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);
//把视野角度的一般转为Vector3向量并取反
Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);
//从玩家的位置到夹角的一条边画一条线(长度为视野的半径)
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);
//从玩家的位置到夹角的另一条边画一条线(长度为视野的半径)
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);
Handles.color = Color.red;
//遍历所有打到的敌人的位置
foreach (Transform visibleTarget in fow.visibleTargets)
{
//画一条线从玩家的位置到敌人的位置
Handles.DrawLine(fow.transform.position, visibleTarget.position);
}
}
}
/// <summary>
/// 把传进来的度数转为Vector3(给出角的方向)
/// </summary>
/// <param name="angleInDegrees">传进来的角度</param>
/// <param name="angleIsGlobal">bool值判断角度是否变化</param>
/// <returns></returns>
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
{
//如果使false
if (!angleIsGlobal)
{
//角度加上自身的欧拉角的y(旋转角度的Y轴)
angleInDegrees += transform.eulerAngles.y;
}
//返回(把传进来的度数转为弧度再转为正弦,0,把传进来的角度转为弧度再转为余弦)
return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}
DireFromAngle函数可以把一个角度传进来,然后把返回一个Vector的向量。参数为一个角度和一个布尔值。(因为我们的物体是敌人也好是玩家也好是会一直移动的,为了让可视化的视野跟随玩家移动,所以需要判断我们取的度数是否跟随了玩家移动,但是我们取视野范围的时候并没有让其跟随物体的变化而变化),所以当我们把角度转成向量的时候需要添加一个布尔值的参数,如果这个角度不跟随移动的时候那么我们就需要让这个角度加上物体的旋转角度,才是视野正确的角度。
利用Handles画一个圆,然后获取到圆的角度,把角度除以2并且其中一个取反,利用DirFromAngle方法把这两个角度转换成Vector3向量,之后再利用Handles的划线方法画出两条线。
这里两条线的夹角就代表了视野的纵向范围,当敌人在这个范围内,代表已经发现了敌人。到现在为止我们已经把扇形准备好了,但是我们会发发现这个线只会在Scene视图显示,我们要怎么把它显示在Game视图呢? 同时我们怎么检测敌人是否在视野范围内呢?
我们先说检测敌人的方法:因为在一个场景中我们不止会有敌人,还会有障碍物、队友、以及其他的物体。我们的Physics中有一个方法可以检测到所有与上图的圆发生触发的物体。那么我们就可以利用layer去检测与圆发生碰撞的物体,然后计算检测到的敌人与玩家的度数是否小于视野角度的一半,如果小于一半的话我们就确定敌人在玩家的视野内,如果大于一半就判断敌人没有在玩家的视野内。然后向敌人的位置发射射线,射线的长度为视野的半径,因为敌人跟玩家之间可能存在障碍物,所以利用layer判断射线是否打到了敌人,如果射线打到敌人则把敌人保存在数组中。下面上代码:
/// <summary>
/// 把打到的玩家物体保存在数组中
/// </summary>
void FindVisibleTarget()
{
//列表清空
visibleTargets.Clear();
//创建一个数组保存所有与重叠球接触的碰撞器 。 方法的参数数据为(球的中心,球的半径,选择投射的层级)
Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, viewRadius, targetMask);
//遍历数组中所有的碰撞器
for (int i = 0; i < targetsInViewRadius.Length; i++)
{
//遍历接收所有碰撞器的位置
Transform target = targetsInViewRadius[i].transform;
//获取接收到的碰撞器与物体之间的vector3向量(方向)
Vector3 dirToTarget = (target.position - transform.position).normalized;
//如果物体与碰撞器的角度小于视野角度的二分之一
if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
{
//计算物体与碰撞器的距离
float dstToTarget = Vector3.Distance(transform.position, target.position);
//如果射线没有打到障碍物就把打到的物体保存在数组中{如果视野范围内有玩家,判断他们之间是否有障碍物}(起点,方向,射线的最大长度,射线可以照到的层级)
if (!Physics.Raycast(transform.position, dirToTarget, dstToTarget, obstacleMask))
{
//把打到的层为targetMask的物体保存在数组中
visibleTargets.Add(target);
//print("打到了" + target.tag);
isColor = true;
}
else
{
isColor = false;
}
}
}
}
可能有人会有疑问为什么判断敌人在不在视野范围内要利用玩家与敌人的角度是否小于视野角度的一半来判断呢?下面我画图来说明一下:
我们可以看到上图黑色的线为玩家的视野范围,视野范围被黄色的线分成了两半我们假如玩家的视野范围是90°,那么一半就是45°,同时有两个敌人A和敌人B,当检测到敌人的时候我们判断敌人与玩家的角度的时候计算的玩家的正前方与敌人的位置的角度,就是黄色的线与红色的线夹角代表玩家与敌人的角度,我们从上图中可以看到这个角大于了玩家视野的一半,我们也可以看到玩家A确实是在视野的外面,而敌人B是在视野范围里面,而敌人B与玩家的角度也确实小于视野的一半。那么我们就可以利用玩家与敌人的角度是否小于视野范围的一半来判断敌人是否在视野范围内。
下面需要判断场景中是否有障碍物遮挡住了视野,我们首先定义一个meshResolution属性用来决定一度可以发射多少条射线。利用射线的终点位置我们来把我们的可视化视角画出来。首先,我们要明白所有图案都是由三角形来组成的,我们看下面的图:
有0,1,2,3,4五个顶点,那么这五个顶点构成三角形的顶点为[0,1,2]、[0,2,3]、[0,3,4],这三组数据中的点全部都参与了三角形的构成,所以构成三角形的顶点就为:0、1、2、0、2、3、0、3、4这九个顶点,但是这些顶点有复用相同的,我们的初始顶点只有五个,根据这个规律我们就可以通过初始顶点的数量得知组成的三角形个数为初始顶点个数减23(5-23)
那么组成的三角形个数为初始顶点数减2。通过研究使用Mesh类画图案的话只需要顺时针赋值所有顶点的位置,以及构建三角形的所有顶点就可以画出一个美丽的弧形。
下面展示一些 内联代码片
。
void DrawFieldOfView()
{
//判断发射多少条射线
int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
print(stepCount);
//把视野分为以0.1度为单位
float stepAngleSize = viewAngle / stepCount;
//创建一个Vector3的列表
List<Vector3> viewPoints = new List<Vector3>();
//以一度为单位遍历所有的视野度数
for (int i = 0; i <= stepCount; i++)
{
//物体的旋转跟随移动
float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
ViewCastInfo newViewCast = ViewCast(angle);
//保存所有的Vector3
viewPoints.Add(newViewCast.point);
//oldViewCast = newViewCast;
}
//顶点数
int vertexCount = viewPoints.Count + 1;
//保存顶点的数组
Vector3[] vertices = new Vector3[vertexCount];
//保存构建所有三角形的所有顶点数
int[] triangles = new int[(vertexCount - 2) * 3];
//第一个顶点的位置为0
vertices[0] = Vector3.zero;
//因为设置了第一个顶点,所以需要减1
for (int i = 0; i < vertexCount - 1; i++)
{
//给保存顶点的数组赋值(世界坐标转换成局部坐标)
vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i]);
if (i < vertexCount - 2)
{
//给构建所有三角形的所有数组赋值
triangles[i * 3] = 0;
triangles[i * 3 + 1] = i + 1;
triangles[i * 3 + 2] = i + 2;
}
}
viewMesh.Clear();
//网格的顶点等于顶点数组
viewMesh.vertices = vertices;
//给构建三角形的所有顶点赋值
viewMesh.triangles = triangles;
//每次赋值结束重新计算法线
viewMesh.RecalculateNormals();
}
/// <summary>
/// 把传进来的角度转为向量,然后发射射线判断是否达到了障碍物层级(如果打到了障碍物返回碰撞信息以及障碍物的位置以及障碍物的距离)
/// </summary>
/// <param name="globalAngle"></param>
/// <returns></returns>
ViewCastInfo ViewCast(float globalAngle)
{
//把传进来的角度转为向量
Vector3 dir = DirFromAngle(globalAngle, true);
RaycastHit hit;
//如果投射了射线(射线的参数为:起点的位置,射线的方向,射线碰撞信息的返回值,射线的最大长度,射线可以投射的层级为障碍物层级)
if (Physics.Raycast(transform.position, dir, out hit, viewRadius, obstacleMask))
{
//返回值为bool值,射线碰撞信息的位置,射线发生碰撞的距离,传进来的角度
return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
}
//如果没有打到的不是障碍物层级
else
{
//返回值为bool值,玩家的位置+传进来的位置转为的Vector3,视野的半径,传进来的角度
return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);
}
}
当射线打中障碍物以后,会返回到结构体打到的位置,所以给Mesh赋值位置的时候就变成了打中障碍物点的位置,使用材质球渲染的时候也就只会渲染到障碍物的位置,如果射线没有打中障碍物,那就射线的长度为视野的长度也就是圆的半径,所以超出半径的物体射线检测不到,这样就成功模拟了一个视野可视化的效果,同时也实现了障碍物遮挡视野的效果。
渲染视野的脚本以及核心代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FieldOfView : MonoBehaviour
{
/// <summary>
/// 视野的半径
/// </summary>
public float viewRadius;
/// <summary>
/// 视野的角度
/// </summary>
[Range(0, 360)]//Unity特性(限制最大最小值)并以滑动条的形式显示出来
public float viewAngle;
/// <summary>
/// 玩家的层级
/// </summary>
public LayerMask targetMask;
/// <summary>
/// 障碍物的层级
/// </summary>
public LayerMask obstacleMask;
/// <summary>
/// 保存射线打到敌人的位置的列表
/// </summary>
[HideInInspector]//特性(使其不会在检视面板显示)
public List<Transform> visibleTargets = new List<Transform>();
/// <summary>
/// 每一度发射多少射线
/// </summary>
public float meshResolution;
public int edgeResolverations;
public float edgeDstThreshold;
/// <summary>
/// 布尔值控制是否可以进行变色
/// </summary>
bool isColor = false;
/// <summary>
/// 网格
/// </summary>
public MeshFilter viewMeshFilter;
Mesh viewMesh;
private void Start()
{
//viewMesh = new Mesh();
//viewMesh.name = "View Mesh";
//viewMeshFilter.mesh = viewMesh;
//viewMesh.name = "111";
viewMesh = new Mesh();
viewMeshFilter.mesh = viewMesh;
//开启协程
StartCoroutine("FindTargetsWithDelay",0.2f);
}
IEnumerator FindTargetsWithDelay(float delay)
{
while (true)
{
yield return new WaitForSeconds(delay);
FindVisibleTarget();
}
}
void LateUpdate()
{
DrawFieldOfView();
}
/// <summary>
/// 把打到的玩家物体保存在数组中
/// </summary>
void FindVisibleTarget()
{
//列表清空
visibleTargets.Clear();
//创建一个数组保存所有与重叠球接触的碰撞器 。 方法的参数数据为(球的中心,球的半径,选择投射的层级)
Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, viewRadius, targetMask);
//遍历数组中所有的碰撞器
for (int i = 0; i < targetsInViewRadius.Length; i++)
{
//遍历接收所有碰撞器的位置
Transform target = targetsInViewRadius[i].transform;
//获取接收到的碰撞器与物体之间的vector3向量(方向)
Vector3 dirToTarget = (target.position - transform.position).normalized;
//如果物体与碰撞器的角度小于视野角度的二分之一
if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
{
//计算物体与碰撞器的距离
float dstToTarget = Vector3.Distance(transform.position, target.position);
//如果射线没有打到障碍物就把打到的物体保存在数组中{如果视野范围内有玩家,判断他们之间是否有障碍物}(起点,方向,射线的最大长度,射线可以照到的层级)
if (!Physics.Raycast(transform.position, dirToTarget, dstToTarget, obstacleMask))
{
//把打到的层为targetMask的物体保存在数组中
visibleTargets.Add(target);
//print("打到了" + target.tag);
isColor = true;
}
else
{
isColor = false;
}
}
}
}
//控制视野的变色
float a = 0f;
private void Update()
{
if (isColor)
{
a += 0.001f;
print(a);
if (a >= 1)
{
a = 1;
}
MeshRenderer ma = GameObject.Find("View Visualisation").GetComponent<MeshRenderer>();
ma.material.color = Color.Lerp(new Color(1, 1, 1, .5f), new Color(1, 0, 0, 0.5f),/*Mathf.PingPong(Time.time,1)*/a);
print(ma.material.color);
}
else
{
a = 0;
MeshRenderer ma = GameObject.Find("View Visualisation").GetComponent<MeshRenderer>();
ma.material.color = new Color(1, 1, 1, .5f);
}
print(isColor);
}
/// <summary>
/// 绘制视图
/// </summary>
void DrawFieldOfView()
{
//判断发射多少条射线
int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
print(stepCount);
//把视野分为以0.1度为单位
float stepAngleSize = viewAngle / stepCount;
//创建一个Vector3的列表
List<Vector3> viewPoints = new List<Vector3>();
//以一度为单位遍历所有的视野度数
for (int i = 0; i <= stepCount; i++)
{
//物体的旋转跟随移动
float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
//print("欧拉角:" + angle);
//Debug.DrawLine(transform.position, transform.position + DirFromAngle(angle, true) * viewRadius, Color.red);
//把角度传进去判断是否达到了障碍物层级(如果达到了障碍物层级就把障碍物的位置,距离,还有传进去的角度全部返回结构体)
ViewCastInfo newViewCast = ViewCast(angle);
//if (i > 0)
//{
// //如果i大于0返回视野打到障碍物的距离是否大于0.5
// bool edgeDsrThresholdExceeded = Mathf.Abs(oldViewCast.dst - newViewCast.dst) > edgeDstThreshold;
// print(edgeDsrThresholdExceeded);
// print("old" + oldViewCast.point);
// print(edgeDstThreshold);
// //如果old的布尔值不等与new的布尔值或者old为true new为true,edge 为true(判断打到了障碍物)
// if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && edgeDsrThresholdExceeded))
// {
// EdgeInfo edge = FindEdge(oldViewCast, newViewCast);
// if (edge.pointA != Vector3.zero)
// {
// viewPoints.Add(edge.pointA);
// }
// if (edge.pointB != Vector3.zero)
// {
// viewPoints.Add(edge.pointB);
// }
// }
//}
//保存所有的Vector3
viewPoints.Add(newViewCast.point);
//oldViewCast = newViewCast;
}
//顶点数
int vertexCount = viewPoints.Count + 1;
//保存顶点的数组
Vector3[] vertices = new Vector3[vertexCount];
//保存构建所有三角形的所有顶点数
int[] triangles = new int[(vertexCount - 2) * 3];
//第一个顶点的位置为0
vertices[0] = Vector3.zero;
//因为设置了第一个顶点,所以需要减1
for (int i = 0; i < vertexCount - 1; i++)
{
//给保存顶点的数组赋值(世界坐标转换成局部坐标)
vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i]);
if (i < vertexCount - 2)
{
//给构建所有三角形的所有数组赋值
triangles[i * 3] = 0;
triangles[i * 3 + 1] = i + 1;
triangles[i * 3 + 2] = i + 2;
}
}
viewMesh.Clear();
//网格的顶点等于顶点数组
viewMesh.vertices = vertices;
//给构建三角形的所有顶点赋值
viewMesh.triangles = triangles;
//每次赋值结束重新计算法线
viewMesh.RecalculateNormals();
}
//EdgeInfo FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewVast)
//{
// //最小的角度
// float minAngle = minViewCast.angle;
// //最大的角度
// float maxAngle = maxViewVast.angle;
// //最小向量
// Vector3 minPoint = Vector3.zero;
// //最大向量
// Vector3 maxPoint = Vector3.zero;
// //遍历4
// for (int i = 0; i < edgeResolverations; i++)
// {
// float angle = (minAngle + maxAngle) / 2;
// ViewCastInfo newViewCast = ViewCast(angle);
// bool edgeDsrThresholdExceeded = Mathf.Abs(minViewCast.dst - newViewCast.dst) > edgeDstThreshold;
// if (newViewCast.hit == minViewCast.hit && !edgeDsrThresholdExceeded)
// {
// minAngle = angle;
// minPoint = newViewCast.point;
// }
// else
// {
// maxAngle = angle;
// maxPoint = newViewCast.point;
// }
// }
// return new EdgeInfo(minPoint, maxPoint);
//}
/// <summary>
/// 把传进来的角度转为向量,然后发射射线判断是否达到了障碍物层级(如果打到了障碍物返回碰撞信息以及障碍物的位置以及障碍物的距离)
/// </summary>
/// <param name="globalAngle"></param>
/// <returns></returns>
ViewCastInfo ViewCast(float globalAngle)
{
//把传进来的角度转为向量
Vector3 dir = DirFromAngle(globalAngle, true);
RaycastHit hit;
//如果投射了射线(射线的参数为:起点的位置,射线的方向,射线碰撞信息的返回值,射线的最大长度,射线可以投射的层级为障碍物层级)
if (Physics.Raycast(transform.position, dir, out hit, viewRadius, obstacleMask))
{
//返回值为bool值,射线碰撞信息的位置,射线发生碰撞的距离,传进来的角度
return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
}
//如果没有打到的不是障碍物层级
else
{
//返回值为bool值,玩家的位置+传进来的位置转为的Vector3,视野的半径,传进来的角度
return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);
}
}
/// <summary>
/// 把传进来的度数转为Vector3(给出角的方向)
/// </summary>
/// <param name="angleInDegrees">传进来的角度</param>
/// <param name="angleIsGlobal">bool值判断角度是否变化</param>
/// <returns></returns>
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
{
//如果使false
if (!angleIsGlobal)
{
//角度加上自身的欧拉角的y(旋转角度的Y轴)
angleInDegrees += transform.eulerAngles.y;
}
//返回(把传进来的度数转为弧度再转为正弦,0,把传进来的角度转为弧度再转为余弦)
return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}
/// <summary>
/// 结构体用来接收数据射线的信息
/// </summary>
public struct ViewCastInfo
{
/// <summary>
/// 射线是否打中物体
/// </summary>
public bool hit;
/// <summary>
/// 位置点(终点)
/// </summary>
public Vector3 point;
/// <summary>
/// 距离(射线的长度)
/// </summary>
public float dst;
/// <summary>
/// 角度
/// </summary>
public float angle;
public ViewCastInfo(bool _hit, Vector3 _point, float _dst, float _angle)
{
hit = _hit;
point = _point;
dst = _dst;
angle = _angle;
}
}
public struct EdgeInfo
{
public Vector3 pointA;
public Vector3 pointB;
public EdgeInfo(Vector3 _pointA, Vector3 _pointB)
{
pointA = _pointA;
pointB = _pointB;
}
}
}
控制人物移动以及人物跟随鼠标旋转的脚本
public class Controller : MonoBehaviour
{
public float Speed = 6.0f;//移动速度
Rigidbody rigidbody;//获取刚体
Camera viewCamera;
Vector3 velocity;
// Start is called before the first frame update
void Start()
{
rigidbody = GetComponent<Rigidbody>();
viewCamera = Camera.main;
}
// Update is called once per frame
void Update()
{
//屏幕转世界坐标(把鼠标坐标转换成Unity坐标,使游戏物体根据鼠标的移动而旋转)
Vector3 mousePos = viewCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, viewCamera.transform.position.y));
//让物体看向鼠标坐标(因为鼠标坐标已经转换成世界坐标所以物体会根据鼠标的移动而旋转)
transform.LookAt(mousePos + Vector3.up * transform.position.y);
//利用水平垂直轴使物体进行移动
velocity = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")).normalized * Speed;
}
private void FixedUpdate()
{
//让刚体根据人物的移动而移动
rigidbody.MovePosition(rigidbody.position + velocity * Time.fixedDeltaTime);
}
}
一个萌新自己一边在网上查的资料一边根据自己的逻辑慢慢写出来的,这是第一次使用Mesh类,感觉还挺新奇的,若有不足,欢迎大佬指正。