在房间网格中查找具有最大可见性的点

问题描述

我有一个 2D 单元格网格,如下所示:

enter image description here

我想找到“质心”,即每个房间中可以看到最多其他单元格的位置。例如,“centroid”1 可以看到 500 个其他单元格,“centroid”2 可以看到其他 400 个单元格(不包括在前 500 个中),依此类推(如果有更好的名称,请告诉我)。

我目前正在使用以下代码执行此操作。

    public void SetCentroids(CellWalkableState[,] grid)
    {
        centroids = new List<((int,int),int)>();
        List<(int,int)> cellscopy = new List<(int,int)>();
        for (int i = 0; i < cells.Count; i++)
        {
            cellscopy.Add(cells[i]);
        }
        Debug.Log(DateTime.Now.ToString("o") + " - Setting centroids for room with " + cells.Count + " cells");
        var perCellInView = cellscopy.AsParallel().Select(x => (x,Staticclass.FindInView(x,grid))).ToList();
        var force_start = perCellInView.First();
        Debug.Log(DateTime.Now.ToString("o") + " - got in view");
        var perCellInViewOrdered = perCellInView.AsParallel().OrderByDescending(xxx => xxx.Item2.Count);
        var force_start_1 = perCellInViewOrdered.First();
        Debug.Log(DateTime.Now.ToString("o") + " - sorted");
        List<(int,int)> roomCellsAdded = new List<(int,int)>();
        while(roomCellsAdded.Count < (cells.Count*0.9))
        {
            if(cellscopy.Count == 0)
            {
                Debug.LogError("something is wrong here.");
            }
            var centroid = perCellInViewOrdered.First().x;
            var centroidCells = perCellInViewOrdered.First().Item2;
            if(centroidCells.Count == 0)
            {
                Debug.Log("this shouldnt be happening");
                break;
            }
            roomCellsAdded.AddRange(centroidCells);
            centroids.Add((centroid,centroidCells.Count));
            Debug.Log(DateTime.Now.ToString("o") + " - added centroids," + roomCellsAdded.Count + " cells in view");
            var loopPerCellInView = perCellInView.AsParallel().Where(x => centroids.Select(y => y.Item1).Contains(x.x) == false).Select(x => (x.x,x.Item2.Except(roomCellsAdded).ToList())).ToList();
            Debug.Log(DateTime.Now.ToString("o") + " - excluded");
            perCellInViewOrdered = loopPerCellInView.AsParallel().OrderByDescending(xxx => xxx.Item2.Count);
            Debug.Log(DateTime.Now.ToString("o") + " - resorted");
        }
    }

    public static List<(int,int)> FindInView((int,int) start,CellWalkableState[,] grid)
    {
        List<(int,int)> visible = new List<(int,int)>() { start };
        bool alive = true;
        int r = 1;
        var length_x = grid.GetLength(0);
        var length_y = grid.GetLength(1);
        List<(int,int)> searched = new List<(int,int)>();
        List<double> angles = new List<double>();
        while(alive)
        {
            //alive = false;
            int newR = r;
            int count = CountFromr(newR);
            var angleInc = 360.0 / count;
            var rNexts = Enumerable.Repeat(1,count).ToArray();
            for (int i = 0; i < count; i++)
            {
                var angle = angleInc * i;
                if(angles.Contains(angle) == false)
                {
                    angles.Add(angle);
                    float cos = Mathf.Cos((float)(Mathf.Deg2Rad * angle));
                    float sin = Mathf.Sin((float)(Mathf.Deg2Rad * angle));
                    var b = r;
                    var p = i % (r * 2);
                    var d = math.sqrt(math.pow(b,2) + math.pow(p,2));
                    var dScaled = d / r;
                    bool keepGoing = true;
                    while(keepGoing)
                    {
                        var rCur = dScaled * (rNexts[i]);
                        var loc = (start.Item1 + Mathf.RoundToInt(rCur * cos),start.Item2 + Mathf.RoundToInt(rCur * sin));
                        if (searched.Contains(loc) == false)
                        {
                            searched.Add(loc);
                            if (loc.Item1 >= 0 && loc.Item1 < length_x && loc.Item2 >= 0 && loc.Item2 < length_y)
                            {
                                if (grid[loc.Item1,loc.Item2] == CellWalkableState.Interactive || grid[loc.Item1,loc.Item2] == CellWalkableState.Walkable)
                                {
                                    visible.Add(loc);
                                }
                                else
                                {
                                    keepGoing = false;
                                }
                            }
                            else
                            {
                                keepGoing = false; // invalid,stop
                            }
                        }
                        else
                        {
                            if (visible.Contains(loc) == false)
                            {
                                keepGoing = false; //  can stop,because we can't see past this place
                            }
                        }
                        if(keepGoing)
                        {
                            rNexts[i]++;
                        }
                    }
                }
            }
            angles = angles.distinct().ToList();
            searched = searched.distinct().ToList();
            visible = visible.distinct().ToList();
            if(rNexts.All(x => x <= r))
            {
                alive = false;
            }
            else
            {
                r = rNexts.Max();
            }
        }
        return visible;
    }

    static int CountFromr(int r)
    {
        return 8 * r;
    }

上面代码的“简短”总结是每个位置首先确定它可以看到自己周围的哪些单元格。这变成了一个元组列表 List<((int,List<(int,int)>)>,其中第一项是位置,第二项是它查看的所有单元格。该主列表按子列表的计数排序,因此具有最多 cell-it-can-vew 的项目排在最前面。这是作为质心添加的,它可以查看的所有单元格都添加到第二个(“已处理”)列表中。形成了一个修改过的“主列表”,现在每个子列表都排除了第二个列表中的任何内容。它会循环执行此操作,直到添加了 90% 的单元格。

一些输出

2021-04-27T15:24:39.8678545-04:00 - 为具有 7129 个单元格的房间设置质心 2021-04-27T15:45:26.4418515-04:00 - 进入视野 2021-04-27T15:45:26.4578551-04:00 - 排序 2021-04-27T15:45:27.3168517-04:00 - 添加质心,查看 4756 个单元格 2021-04-27T15:45:27.9868523-04:00 - 排除 2021-04-27T15:45:27.9868523-04:00 - 重新使用 2021-04-27T15:45:28.1058514-04:00 - 添加了质心,可查看 6838 个单元格 2021-04-27T15:45:28.2513513-04:00 - 排除 2021-04-27T15:45:28.2513513-04:00 - 重新使用 2021-04-27T15:45:28.2523509-04:00 - 为具有 20671 个单元格的房间设置质心

这对我来说太慢了。任何人都可以建议这样做的替代方法吗?对于所有的细胞,基本上唯一的信息是它们是否“打开”或者是否可以看穿它们(相对于像墙一样的东西)。

解决方法

具有固定整数斜率的近似解

要大幅加速(从房间数量的二次方变为线性),您可以决定只检查每个点的几个整数斜率。这些是可见性的等价类,即如果单元格 x 可以看到沿着这样一条线的单元格 y,并且单元格 y 可以看到沿着同一条线的单元格 z,那么所有 3 个单元格都可以看到彼此。然后你只需要沿着特定的线计算每个“可见性间隔”一次,而不是每个单元。

您可能至少要检查水平、垂直和 45 度对角线斜率。对于水平,从单元格 (1,1) 开始,然后向右移动直到碰到墙,假设在 (5,1)。然后单元格 (1,1),(2,(3,1) 和 (4,1) 都可以沿着这个斜率看到彼此,所以虽然你从 (1,1) 开始,但没有必要对其他 3 个单元重复计算——只需将此列表的副本(或什至是指向它的指针,速度更快)添加到所有 4 个单元的可见性列表中。继续向右行驶,并在碰到非墙壁时立即重复该过程。然后在下一行重新开始。

45 度对角线的能见度检查稍微困难一些,但不是很多,检查水平前进 1 和垂直前进 k 的其他斜坡(反之亦然)大致相同。 (检查一般的斜坡,比如每 2 步向右上 3 步,可能有点棘手。)

如果您使用指针而不是列表副本,对于给定的斜率,这种方法会在每个单元格中花费固定的分摊时间:虽然找到某个单元格的 k 个水平可见邻居需要 O(k) 时间,但这意味着不需要进一步的水平处理需要为他们中的任何一个做。因此,如果您检查恒定数量的斜率(例如,我建议的四个),则处理 n 个单元格的总体时间为 O(n)。相比之下,我认为您当前的方法至少需要 O(nq) 时间,其中 q 是一个单元格可见的单元格平均数。