将会持续更新gameplay的一些基础知识,一同学习。
扇形检测
扇形检测是Gameplay里面很常见的场景。比如荒野乱斗中,大部分的近战角色都是扇形攻击。在扇形范围内就认为是受击。
扇形检测只有两个参数,一个是扇形的角度一个是扇形的半径大小。
效果
获取鼠标朝向
技能必然是和鼠标朝向一致的,所以学习检测务必先学一下怎么得到鼠标朝向,以及得到朝向对应的旋转角度。
思路是利用Camera.main.ScreenPointToRay(Input.mousePosition)
方法,能够得到相机朝向屏幕空间下某点的射线,从而得到射线的碰撞点,然后计算角色朝向碰撞点的方向,这时候碰撞点的横坐标x和轴坐标z是已知的,所以用反三角函数Atan2可以得到旋转到碰撞点的旋转角度。
值得一提的是Camera.main.ScreenPointToRay(Input.mousePosition)在诸如用户注视某个物品/3dui的交互上面都可以使用。
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, hit.point.y,0);
Debug.DrawLine(transform.position, horizontalPoint, color);
skillDirection = (horizontalPoint - transform.position).normalized;
skillAngle = Mathf.Atan2(skillDirection.y, 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 * 1.0f / segments;
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, 0f, curAngle) * transform.right * radius + vertices[0];
vertices[i] = pos;
}
// 画圆弧
for (int i = 1; i < vertices.Length - 1; 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);
}
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,0, angles + skillAngle) * transform.right * radius + transform.position);
}
m_LineRenderer.SetPositions(m_LineRendererPoints.ToArray());
}
private void ResetLinerRenderPoints()
{
m_LineRendererPoints.Clear();
}
扇形和点的碰撞检测
判断某个点在扇形内的办法
距离判断:
点和玩家的距离小于扇形半径。
角度判断:
设玩家到受击角色的向量为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说明在右边界以右
这里两个办法都实现一下
三角函数法
/// <summary>
/// 利用Acos检测扇形和点的碰撞
/// </summary>
/// <param name="fanPos">扇形中心位置</param>
/// <param name="fanRadius">扇形半径</param>
/// <param name="fanAngle">扇形圆心角</param>
/// <param name="fanDir">扇形中心朝向</param>
/// <param name="pointPos">点坐标</param>
/// <returns></returns>
private bool Dectect_FanAndPoint_ACos(Vector3 fanPos, float fanRadius, float fanAngle, Vector3 fanDir, Vector3 pointPos)
{
float distance = Vector3.Distance(fanPos, pointPos);
//距离超过检测半径
if (distance > fanRadius)
{
return false;
}
Vector3 circleDir = (pointPos - fanPos).normalized;
//两个单位向量的点乘等于其夹角的余弦值
float enemyAngle = Mathf.Acos(Vector3.Dot(fanDir, circleDir)) * Mathf.Rad2Deg;
//敌人朝向和技能朝向的夹角小于二分之一扇形角说明在扇形范围内
if (enemyAngle <= fanAngle / 2)
{
return true;
}
return false;
}
向量叉乘
/// <summary>
/// 利用叉乘检测扇形和点的碰撞
/// </summary>
/// <param name="enemyPos">敌人位置</param>
/// <param name="leftBound">扇形左边界</param>
/// <param name="RightBound">扇形右边界</param>
/// <returns></returns>
private bool Dectect_FanAndPoint_Cross(Vector3 fanPos, float fanRadius, float leftAngle, float RigthtAngle, Vector3 pointPos)
{
float distance = Vector3.Distance(fanPos, pointPos);
//距离超过检测半径
if (distance > fanRadius)
{
return false;
}
//扇形左边界
Vector3 leftBound = Quaternion.Euler(0f, 0f, leftAngle) * Vector3.right;
//扇形右边界
Vector3 RightBound = Quaternion.Euler(0f, 0f, RigthtAngle) * Vector3.right;
Vector3 circleDir = pointPos - fanPos;
//注意左手系的叉乘是左手定则,2D情况下z是往里的
bool isLeft = Vector3.Cross(circleDir, leftBound).z < 0 ? true : false;
bool isRight = Vector3.Cross(RightBound, circleDir).z < 0 ? true : false;
return !isLeft && !isRight;
}
进阶:扇形和圆形的碰撞检测
效果
思路
我们根据圆心的位置进行分类讨论,前面说到可以根据叉乘来得知圆心在扇形的左侧以左或者是右侧以右,或者是中间
圆心在左侧以左或者右侧以右时,题目其实就化为圆心到线段的距离是否小于圆的半径
圆心在扇形中间时,可以视为是两个圆进行碰撞检测。
求点和线段的距离
这里参考了https://blog.csdn.net/zaffix/article/details/25160505的做法,思路很巧妙。
将P投影到AB方向上,投影小于零说明最短距离是PA,大于零说明P应该做垂线或者求PB,如果投影大小大于AB则PB为最短距离,否则PP’为最短距离。图示如下
求PP’可以利用叉乘的几何意义来做
求圆和圆之间的碰撞检测
这个就很简单了,不赘述
检测函数
/// <summary>
/// 检测圆形和扇形的碰撞
/// </summary>
/// <param name="circlePos">圆心坐标</param>
/// <param name="radius">半径</param>
/// <param name="fanPos">扇形圆心坐标</param>
/// <param name="fanRadius">扇形坐标</param>
/// <param name="leftAngle">扇形左边界角度</param>
/// <param name="RigthAngle">扇形右边界角度</param>
/// <returns></returns>
private bool Dectect_FanAndCircle(Vector3 circlePos, float radius, Vector3 fanPos, float fanRadius, float leftAngle, float RigthAngle)
{
Vector3 enemyDir = circlePos - fanPos;
Vector3 leftBound = Quaternion.Euler(0f, 0f, leftAngle) * Vector3.right * fanRadius;
//圆心在扇形左边
bool isLeft = Vector3.Cross(enemyDir, leftBound).z < 0 ? true : false;
if (isLeft)
{
return GetMinDistanceFromPointToLineSegment(circlePos, fanPos, fanPos + leftBound) > radius ? false : true;
}
Vector3 RightBound = Quaternion.Euler(0f, 0f, RigthAngle) * Vector3.right * fanRadius;
bool isRight = Vector3.Cross(RightBound, enemyDir).z < 0 ? true : false;
//圆心在扇形右边
if (isRight)
{
return GetMinDistanceFromPointToLineSegment(circlePos, fanPos, fanPos + RightBound) > radius ? false : true;
}
//圆心在两者夹角
return radius + fanRadius > Vector3.Distance(fanPos, circlePos);
}
/// <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;
}