【Unity】判断一个圆是否被一组圆覆盖(1)

应用背景:项目要求大量圆形筹码(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的耗时了。所以需要进行算法优化,方向也很简单,就是用动态规划思想把计算缓存下来。具体实现放在第二篇。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值