应用背景:项目要求大量圆形筹码(500+)随机堆放到桌面上,有大量筹码实际上是被覆盖的,所以要求出哪些筹码是被覆盖的以移除桌面。
算法来源,英语还行的直接看原网页就好:algorithm - Find totally covered circles in a set of circles - Stack Overflow
原理很简单:如果一个圆(A)被一组圆覆盖的话,那么这组圆互相之间且在圆A内的交点,必在这组圆的某个圆内。
红色圆为判断目标圆,另外三个圆的交点如图所示,白色的交点即为在目标圆内且不在任意其他圆内的点。判断为没有完全覆盖。
这张图中,一组圆互相的交点均在某个圆中,因此判断目标圆被完全覆盖。
通过算法,把圆覆盖问题变成了求交点以及判断是否在圆内(即交点和圆心距离与半径的比较),就很好用代码实现了。(项目中所有圆半径均相同,所以算法中两圆半径相关的都取同一半径进行了修改,如果需要不同半径的注意修改)
第一步:求交点算法,我采用的是这个:计算两个圆的交点 - 知乎。只需要三角函数和反三角函数就能计算,非常简洁。
C#代码贴出:
void CalIntersection(Vector3 a, Vector3 b, out Vector3 intersec1, out Vector3 intersec2)
{
//圆a需要在圆b的左下方以方便计算夹角
if(a.x > b.x || a.y > b.y)
{
var i = a;
a = b;
b = i;
}
var dx = b.x - a.x;
var dy = b.y - a.y;
var dis2 = dx*dx + dy*dy;
var t = Mathf.Atan2(dy, dx);
var t2 = Mathf.Acos(dis2 / (2 * radius *(Mathf.Sqrt(dis2))));
intersec1 = new Vector3(a.x + radius * Mathf.Cos(t + t2), a.y + radius * Mathf.Sin(t + t2), 0);
intersec2 = new Vector3(a.x + radius * Mathf.Cos(t - t2), a.y + radius * Mathf.Sin(t - t2), 0);
}
第二步:判断是否在圆内。因为筹码是动态生成的,每多一个筹码,首先要判断是否有圆因为新加的筹码被完全覆盖(可能有多个),其次该筹码也会成为一个新的需要被判断的目标圆。所以在新加筹码时,要判断所有与该筹码相交的圆是否被覆盖。因此我采用了一个二维数组存储数据。
private List<List<Vector3>> circleGroups;
辅助函数
//判断点是否在圆内
bool InCircle(Vector3 point, Vector3 circleCenter)
{
return Vector3.Distance(point, circleCenter) < radius;
}
//判断两圆是否相交
bool IsIntersecting(Vector3 circleCenterA, Vector3 circleCenterB)
{
return Vector3.Distance(circleCenterA, circleCenterB) < radius * 2;
}
核心算法(显示交点的部分可以删掉让代码更简洁一点,只是为了直观显示算法准确度)
bool IsCovered(GameObject[] circles)
{
var baseCircle = circles[0];
bool haveIntersection = false;
for (int i = 2;i < circles.Length; i++)
{
for(int j = i - 1; j >= 1; j--)
{
var circle = circles[i];
var target = circles[j];
if(IsIntersecting(circle.transform.position,target.transform.position))
{
haveIntersection = true;
Vector3 interSec1;
Vector3 interSec2;
CalIntersection(circle.transform.position, target.transform.position,out interSec1,out interSec2,circle.GetInstanceID(),target.GetInstanceID());
//显示交点位置
//GameObject dot1 = Instantiate(dot, interSec1, dot.transform.rotation);
//GameObject dot2 = Instantiate(dot, interSec2, dot.transform.rotation);
bool inBase = false;
if(InCircle(interSec1,baseCircle.transform.position))
{
inBase = true;
bool inList = false;
for(int k = 1; k < circles.Length; k++)
{
if(k != i && k != j)
{
if(InCircle(interSec1,circles[k].transform.position))
{
inList = true;
break;
}
}
}
//有交点不在任意非目标圆内,没有完全覆盖,直接返回
if (inList == false)
{
//dot1.GetComponent<SpriteRenderer>().color = new Color(1, 1, 1);
return false;
}
}
if (InCircle(interSec2, baseCircle.transform.position))
{
inBase = true;
bool inList = false;
for (int k = 1; k < circles.Length; k++)
{
if (k != i && k != j)
{
if (InCircle(interSec2, circles[k].transform.position))
{
inList = true;
break;
}
}
}
if (inList == false)
{
//dot2.GetComponent<SpriteRenderer>().color = new Color(1, 1, 1);
return false;
}
}
if (inBase == false)
{
//交点都不存在于目标圆中,说明其中一个圆的覆盖部分完全在另一个圆内。
haveIntersection = false;
}
}
}
}
//被覆盖的判断:产生了新交点,且所有新交点都在某个非目标圆内
return haveIntersection;
}
注意代码中循环变量的值,因为传入数组中,默认第一个为要判断是否被覆盖的圆,所以不加入交点计算。所有循环判断从1起。
同时必须要有2个圆才能产生交点,所以第一个循环i从数组的第三位也就是2起。
求交点只需要求在自己之前的圆即可,不然会重复求交点。圆2和圆1求交点,不和圆3求,不然圆2X圆3 与 圆3X圆2 会重复。因此第二个循环j从i-1起,到1结束。
两个圆都与目标圆相交但是不产生目标圆内交点的情况
算法调用
bool NewBet(GameObject bet,out List<GameObject> coveredList)
{
bool isCovered = false;
coveredList = new List<GameObject>();
//判断新圆对旧圆的覆盖
for(int i = circleGroups.Count - 1; i >= 0; i--)
{
GameObject baseCircle = circleGroups[i][0];
//临界情况,正好重叠
if(Vector3.Distance(baseCircle.transform.position, bet.transform.position) == 0)
{
isCovered = true;
coveredList.Add(baseCircle);
circleGroups.RemoveAt(i);
}else if (IsIntersecting(baseCircle.transform.position, bet.transform.position))
{
circleGroups[i].Add(bet);
if (IsCovered(circleGroups[i].ToArray()))
{
isCovered = true;
coveredList.Add(baseCircle);
circleGroups.RemoveAt(i);
}
}
}
//新圆加入判断组
List<GameObject> newGroup = new List<GameObject>();
newGroup.Add(bet);
circleGroups.Add(newGroup);
return isCovered;
}
到此算法已经能完美判断圆覆盖情况了。然而如果真的按这个写会发现这个算法性能堪忧。对于500+以上的圆,算法耗时已经远远超过drawcall的耗时了。所以需要进行算法优化,方向也很简单,就是用动态规划思想把计算缓存下来。具体实现放在第二篇。