将会持续更新gameplay的一些基础知识,一同学习。
扇形检测
扇形检测是Gameplay里面很常见的场景。比如荒野乱斗中,大部分的近战角色都是扇形攻击。在扇形范围内就认为是受击。
扇形检测只有两个参数,一个是扇形的角度一个是扇形的半径大小。
效果
获取鼠标朝向
技能必然是和鼠标朝向一致的,所以学习检测务必先学一下怎么得到鼠标朝向,以及得到朝向对应的旋转角度。
思路是利用Camera.main.ScreenPointToRay(Input.mousePosition)
方法,能够得到相机朝向屏幕空间下某点的射线,从而得到射线的碰撞点,然后计算角色朝向碰撞点的方向,这时候碰撞点的横坐标x和轴坐标z是已知的,所以用反三角函数Atan2可以得到旋转到碰撞点的旋转角度。
值得一提的是Camera.main.ScreenPointToRay(Input.mousePosition)在诸如用户注视某个物品/3dui的交互上面都可以使用。
void Update(){ray = Camera.main.ScreenPointToRay(Input.mousePosition);RaycastHit hit;if (Physics.Raycast(ray, out hit)){//射线检测的是平面点的位置,实际需要的是平行于平面的方向,所以修改y坐标Vector3 horizontalPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);Debug.DrawLine(transform.position, horizontalPoint, Color.red);//方便观察skillDirection = (horizontalPoint - transform.position).normalized;//z轴取反是因为unity的旋转api是顺时针旋转的skillAngle = Mathf.Atan2(-skillDirection.z, skillDirection.x) * Mathf.Rad2Deg;}}
尝试利用画出一个扇形
为了方便我们看检测的结果,先实现一个绘制扇形的工具将技能范围画出来,这里提供使用Debug.DrawLine和使用LineRender两种办法。需要注意如果用LineRender的SetPostition方法,需要在组件里面设置好Position数组的大小,防止出现数组越界的报错。
利用LineRender和Debug.DrawLine的思路是一样的,先绘制扇形的中心点以及左边界点,再从左边界点往另一个边界以某个角度间隔绘制一个个点,连接起来就是一个圆弧。
private void DrawFan_DebugDrawLine(float radius, int euler){int segments = 100; //圆弧那一段需要用多少个点来表示float deltaAngle = euler / segments;Vector3 forward = transform.forward;Vector3[] vertices = new Vector3[segments + 2];vertices[0] = transform.position;for (int i = 1; i < vertices.Length; i++){float curAngle = -euler / 2 + deltaAngle * (i - 1) + skillAngle;//从-1/2扇形角度开始绘制,每次偏移deltaAngleVector3 pos = Quaternion.Euler(0f, curAngle, 0f) * forward * radius + transform.position;vertices[i] = pos;}// 画圆弧for (int i = 1; i < vertices.Length - 1; i++){Debug.Log(vertices[i]);Debug.DrawLine(vertices[i], vertices[i + 1], showColor);}// 画两条边Debug.DrawLine(vertices[0], vertices[vertices.Length - 1], showColor);Debug.DrawLine(vertices[0], vertices[1], showColor);}/// <summary>/// 利用LinerRender绘制扇形/// </summary>private void DrawFan_LineRender(float radius, int euler){ResetLinerRenderPoints();m_LineRenderer.startColor = showColor; //碰撞的时候会切换展示的颜色m_LineRenderer.endColor = showColor;m_LineRendererPoints.Add(transform.position);//每一度一个点,绘制思路和debug.DrawLine相同for(int angles = -euler/2; angles <= euler/2; angles++){m_LineRendererPoints.Add(Quaternion.Euler(0, angles + skillAngle, 0) * transform.right * radius + transform.position);}m_LineRenderer.SetPositions(m_LineRendererPoints.ToArray());}
扇形和点的碰撞检测
判断某个点在扇形内的办法
距离判断:
点和玩家的距离小于扇形半径。
角度判断:
设玩家到受击角色的向量为a,玩家技能朝向为b
方法一:判断a和b所形成的角度小于1/2的扇形夹角。
只需要拿到a和单位向量和b的单位向量进行点积,就能够得到a和b夹角的cos值,再利用Acos得到夹角大小。
设扇形左边界为left,扇形右边界为right
方法二:利用向量叉乘,如果a在扇形的左边界之外或扇形的右边界之外则不满足。
这里需要注意Unity的世界坐标系是左手系,叉乘的方向满足的是左手定则。
即a x left
所得向量的y轴坐标大于0说明在左边界以左,
right x a
所得向量的的y轴左边大于0说明在右边界以右
这里两个办法都实现一下
三角函数法
public bool Dectect_ACos(Vector3 enemyPos){float distance = Vector3.Distance(transform.position, enemyPos);//距离超过检测半径if (distance > radius){return false;}Vector3 enemyDirection = (enemyPos - transform.position).normalized;//两个单位向量的点乘等于其夹角的余弦值float enemyAngle = Mathf.Acos(Vector3.Dot(skillDirection, enemyDirection)) * Mathf.Rad2Deg;//敌人朝向和技能朝向的夹角小于二分之一扇形角说明在扇形范围内if (enemyAngle <= angle / 2){return true;}return false;}
向量叉乘
public bool Dectect_Cross(Vector3 enemyPos){float distance = Vector3.Distance(transform.position, enemyPos);//距离超过检测半径if (distance > radius){return false;}//扇形左边界Vector3 leftBound = Quaternion.Euler(0f, -angle / 2 + skillAngle, 0f) * transform.right;Debug.DrawLine(transform.position, transform.position + leftBound, Color.blue);//扇形右边界Vector3 RightBound = Quaternion.Euler(0f, angle / 2 + skillAngle, 0f) * transform.right;Debug.DrawLine(transform.position, transform.position + RightBound , Color.yellow);Vector3 enemyDir = enemyPos - transform.position;//注意左手系的叉乘是左手定则bool isLeft = Vector3.Cross(enemyDir, leftBound).y > 0 ? true : false; bool isRight = Vector3.Cross(RightBound, enemyDir).y > 0 ? true : false;Debug.Log("isLeft:" + isLeft + "isRight" + isRight);return !isLeft && !isRight;}
完整实现
挂载在角色身上的脚本,注意鼠标是利用射线检测,所以需要再角色下面放一个平面接收射线。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class FanDetector : MonoBehaviour
{public float radius; //扇形半径public int angle; //扇形角度public Vector3 skillDirection;private Ray ray;private LineRenderer m_LineRenderer;private List<Vector3> m_LineRendererPoints;private Color showColor;public float skillAngle;// Start is called before the first frame updatevoid Start(){InitLineRender();}// Update is called once per framevoid Update(){GetSkillDirection(Color.green);//DrawFan_DebugDrawLine(radius, angle);DrawFan_LineRender(radius, angle);}/// <summary>/// 对外接口,敌人是否在技能范围内/// </summary>public bool Dectect(Vector3 enemyPos){bool isDecteced = Dectect_Cross(enemyPos);if(isDecteced){showColor = Color.red;return isDecteced;}showColor = Color.green;return isDecteced;}/// <summary>/// 利用Acos检测碰撞/// </summary>/// <param name="enemyPos"></param>/// <returns></returns>public bool Dectect_ACos(Vector3 enemyPos){float distance = Vector3.Distance(transform.position, enemyPos);//距离超过检测半径if (distance > radius){return false;}Vector3 enemyDirection = (enemyPos - transform.position).normalized;//两个单位向量的点乘等于其夹角的余弦值float enemyAngle = Mathf.Acos(Vector3.Dot(skillDirection, enemyDirection)) * Mathf.Rad2Deg;//敌人朝向和技能朝向的夹角小于二分之一扇形角说明在扇形范围内if (enemyAngle <= angle / 2){return true;}return false;}/// <summary>/// 利用叉乘检测/// </summary>/// <param name="enemyPos"></param>/// <returns></returns>public bool Dectect_Cross(Vector3 enemyPos){float distance = Vector3.Distance(transform.position, enemyPos);//距离超过检测半径if (distance > radius){return false;}//扇形左边界Vector3 leftBound = Quaternion.Euler(0f, -angle / 2 + skillAngle, 0f) * transform.right;Debug.DrawLine(transform.position, transform.position + leftBound, Color.blue);//扇形右边界Vector3 RightBound = Quaternion.Euler(0f, angle / 2 + skillAngle, 0f) * transform.right;Debug.DrawLine(transform.position, transform.position + RightBound , Color.yellow);Vector3 enemyDir = enemyPos - transform.position;//注意左手系的叉乘是左手定则bool isLeft = Vector3.Cross(enemyDir, leftBound).y > 0 ? true : false; bool isRight = Vector3.Cross(RightBound, enemyDir).y > 0 ? true : false;Debug.Log("isLeft:" + isLeft + "isRight" + isRight);return !isLeft && !isRight;}/// <summary>/// 获取鼠标朝向/// </summary>private void GetSkillDirection(Color color){ray = Camera.main.ScreenPointToRay(Input.mousePosition);RaycastHit hit;if (Physics.Raycast(ray, out hit)){//射线检测的是平面点的位置,实际需要的是平行于平面的方向,所以修改y坐标Vector3 horizontalPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);Debug.DrawLine(transform.position, horizontalPoint, color);skillDirection = (horizontalPoint - transform.position).normalized;//z轴取反是因为unity的旋转api是顺时针旋转的skillAngle = Mathf.Atan2(-skillDirection.z, skillDirection.x) * Mathf.Rad2Deg;}}/// <summary>/// 利用Debug工具绘制扇形/// </summary>private void DrawFan_DebugDrawLine(float radius, int euler){int segments = 100; //圆弧那一段需要用多少个点来表示float deltaAngle = euler / segments;Vector3 right = transform.right;Vector3[] vertices = new Vector3[segments + 2];vertices[0] = transform.position;for (int i = 1; i < vertices.Length; i++){float curAngle = -euler / 2 + deltaAngle * (i - 1) + skillAngle;//从-1/2扇形角度开始绘制,每次偏移deltaAngleVector3 pos = Quaternion.Euler(0f, curAngle, 0f) * right * radius + transform.position;vertices[i] = pos;}// 画圆弧for (int i = 1; i < vertices.Length - 1; i++){Debug.Log(vertices[i]);Debug.DrawLine(vertices[i], vertices[i + 1], showColor);}// 画两条边Debug.DrawLine(vertices[0], vertices[vertices.Length - 1], showColor);Debug.DrawLine(vertices[0], vertices[1], showColor);}/// <summary>/// 利用LinerRender绘制扇形/// </summary>private void DrawFan_LineRender(float radius, int euler){ResetLinerRenderPoints();m_LineRenderer.startColor = showColor;m_LineRenderer.endColor = showColor;m_LineRendererPoints.Add(transform.position);//每一度一个点,绘制思路和debug.DrawLine相同for(int angles = -euler/2; angles <= euler/2; angles++){m_LineRendererPoints.Add(Quaternion.Euler(0, angles + skillAngle, 0) * transform.right * radius + transform.position);}m_LineRenderer.SetPositions(m_LineRendererPoints.ToArray());}private void InitLineRender(){m_LineRendererPoints = new List<Vector3>();m_LineRenderer = GetComponent<LineRenderer>();m_LineRenderer.endWidth = 0.1f;m_LineRenderer.startWidth = 0.1f;m_LineRenderer.loop = true; //绘制路径将会闭合}private void ResetLinerRenderPoints(){m_LineRendererPoints.Clear();}
}
敌人身上的脚本,可以移动
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Enemy : MonoBehaviour
{// Start is called before the first frame updatepublic float speed = 2f;private FanDetector fanSkill;void Start(){fanSkill = GameObject.FindWithTag("Player").GetComponent<FanDetector>();}// Update is called once per framevoid Update(){transform.position += Input.GetAxis("Horizontal") * speed * transform.right * Time.deltaTime;transform.position += Input.GetAxis("Vertical") * speed * transform.forward * Time.deltaTime;SkillDetect();}private void SkillDetect(){if (fanSkill.Dectect(transform.position)){Debug.Log("进入扇形技能范围");}}}