基础GamePlay知识-扇形检测

将会持续更新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扇形角度开始绘制,每次偏移deltaAngle
            Vector3 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 update
    void Start()
    {
        InitLineRender();
    }

    // Update is called once per frame
    void 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扇形角度开始绘制,每次偏移deltaAngle
            Vector3 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 update
    public float speed = 2f;
    private FanDetector fanSkill;
    void Start()
    {
        fanSkill = GameObject.FindWithTag("Player").GetComponent<FanDetector>();
    }

    // Update is called once per frame
    void 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("进入扇形技能范围");
        }
    }

}

进阶:扇形和圆形的碰撞检测

效果

因为相机视角的问题,会有检测的误差,完全俯视就没有这些问题

思路

我们根据圆心的位置进行分类讨论,前面说到可以根据叉乘来得知圆心在扇形的左侧以左或者是右侧以右,或者是中间
圆心在左侧以左或者右侧以右时,题目其实就化为圆心到线段的距离是否小于圆的半径
圆心在扇形中间时,可以视为是两个圆进行碰撞检测。

求点和线段的距离

这里参考了https://blog.csdn.net/zaffix/article/details/25160505的做法,思路很巧妙。
将P投影到AB方向上,投影小于零说明最短距离是PA,大于零说明P应该做垂线或者求PB,如果投影大小大于AB则PB为最短距离,否则PP'为最短距离。图示如下

求PP'可以利用叉乘的几何意义来做

求圆和圆之间的碰撞检测

这个就很简单了,不赘述

完整实现

在FanDetector.cs中提供Dectect_Circle接口

public bool Dectect(Vector3 enemyPos, float enemyRadius)
{
     //bool isDecteced = Dectect_Cross(enemyPos);
     bool isDecteced = Dectect_Circle(enemyPos, enemyRadius);
     if (isDecteced)
     {
        showColor = Color.red;
        return isDecteced;
     }
     showColor = Color.green;
     return isDecteced;
}
 private bool Dectect_Circle(Vector3 enemyPos, float radius)
 {
     Vector3 enemyDir = enemyPos - transform.position;
     Vector3 leftBound = Quaternion.Euler(0f, -angle / 2 + skillAngle, 0f) * transform.right;
     //圆心在扇形左边
     bool isLeft = Vector3.Cross(enemyDir, leftBound).y > 0 ? true : false;
     if(isLeft)
     {
         return GetMinDistanceFromPointToLineSegment(enemyPos, transform.position, transform.position + leftBound) > radius ? false : true;
     }
     Vector3 RightBound = Quaternion.Euler(0f, angle / 2 + skillAngle, 0f) * transform.right;
     bool isRight = Vector3.Cross(RightBound, enemyDir).y > 0 ? true : false;
     if(isRight)
     {
         return GetMinDistanceFromPointToLineSegment(enemyPos, transform.position, transform.position + RightBound) > radius ? false : true;
     }
     return radius + this.radius > Vector3.Distance(transform.position, enemyPos);
 }
 /// <summary>
 /// 检测点和线段的碰撞
 /// </summary>
 private float GetMinDistanceFromPointToLineSegment(Vector3 point, Vector3 starPos, Vector3 endPos)
 {
     Vector3 start2Point = point - starPos;
     Vector3 line = Vector3.Normalize(endPos - starPos);
     float projection = Vector3.Dot(start2Point, line);
     //投影在线段起始位置的前面
     if (projection <= 0f)
     {
         return start2Point.magnitude;
     }
     //投影在线段终点位置的后面
     if (projection > start2Point.magnitude )
     {
         return (point - endPos).magnitude;
     }
     //投影在线段中间,等价于求点到直线的距离,利用叉乘法
     float area = Vector3.Cross(start2Point, endPos - starPos).magnitude; //求平行四边形面积
     float distance = area / (endPos - starPos).magnitude;
     return distance;
 }

稍微修改敌人代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEditor.PlayerSettings;

public class Enemy : MonoBehaviour
{
    // Start is called before the first frame update
    public float speed = 2f;
    public float radius = 0.3f;
    private FanDetector fanSkill;
    void Start()
    {
        fanSkill = GameObject.FindWithTag("Player").GetComponent<FanDetector>();
    }

    // Update is called once per frame
    void Update()
    {
        transform.position += Input.GetAxis("Horizontal") * speed * transform.right * Time.deltaTime;
        transform.position += Input.GetAxis("Vertical") * speed * transform.forward * Time.deltaTime;
        DrawCircle();
        SkillDetect();
    }

    private void SkillDetect()
    {
        if (fanSkill.Dectect(transform.position, radius))
        {
            Debug.Log("进入扇形技能范围");
        }
    }
    /// <summary>
    /// 画出圆形检测范围,方便观察
    /// </summary>
    private void DrawCircle()
    {
        for(int i = 0; i < 360; i++)
        {
            Debug.DrawLine(Quaternion.Euler(0f, i, 0f) * transform.right * radius + transform.position, Quaternion.Euler(0f, i + 1, 0f) * transform.right * radius + transform.position, Color.black);
        }
    }
}

posted @ 2024-03-09 18:21  只懂得unity的JL  阅读(76)  评论(0编辑  收藏  举报