unity3d 为什么在减去坐标时,这种泛色填充方法会导致堆栈溢出?

2fjabf4q  于 2023-02-09  发布在  其他
关注(0)|答案(3)|浏览(155)

我试图创建一个油漆桶风格的洪水填补瓷砖系统,以创建一个编辑器。
然而,基于四路泛色填充算法会产生问题。x + 1和y + 1工作得很好,但是当它们与x - 1和y - 1配对时,它会导致堆栈溢出。另外,我比较的点是平铺的颜色。有一些情况下,它会忽略颜色是否匹配,并在它应该退出时覆盖它们。
按照这里的示例算法,这段代码看起来应该可以工作:
Flood Fill Algorithm Example
然而,如上所述,我自己的C#实现并不是在所有方面都能正常工作:

public void FloodFill(int x, int y, Color fillColor, Color oldColor)
{
    if (x < 0 || x >= boardSize) return;
    if (y < 0 || y >= boardSize) return;
    Tile tile = world.GetTileAt(x, y);

    if (tile.currentColor.Equals(fillColor))
    {
        return;
    }

    if (tile.currentColor.Equals(oldColor))
    {
        UpdateTile(tile.currentTile, fillColor);

        FloodFill(x - 1, y, fillColor, oldColor);
        FloodFill(x + 1, y, fillColor, oldColor);
        FloodFill(x, y - 1, fillColor, oldColor);
        FloodFill(x, y + 1, fillColor, oldColor);
    }

        return;
}

我最好的猜测是,打个比方,它自己绊倒了,但这不应该发生在比较和返回以阻止执行的过程中,这个结论来自于日志--它在处理正数和负数时都会跳格。
以下是到目前为止的日志:

MasterController.FloodFill (System.Int32 x, System.Int32 y, UnityEngine.Color fillColor, UnityEngine.Color oldColor) (at Assets/Game Assets/Scripts/Gameplay/MasterController.cs:134)
MasterController.FloodFill (System.Int32 x, System.Int32 y, UnityEngine.Color fillColor, UnityEngine.Color oldColor) (at Assets/Game Assets/Scripts/Gameplay/MasterController.cs:136)
MasterController.FloodFill (System.Int32 x, System.Int32 y, UnityEngine.Color fillColor, UnityEngine.Color oldColor) (at Assets/Game Assets/Scripts/Gameplay/MasterController.cs:138)
MasterController.FloodFill (System.Int32 x, System.Int32 y, UnityEngine.Color fillColor, UnityEngine.Color oldColor) (at Assets/Game Assets/Scripts/Gameplay/MasterController.cs:136)
MasterController.FloodFill (System.Int32 x, System.Int32 y, UnityEngine.Color fillColor, UnityEngine.Color oldColor) (at Assets/Game Assets/Scripts/Gameplay/MasterController.cs:138)
MasterController.FloodFill (System.Int32 x, System.Int32 y, UnityEngine.Color fillColor, UnityEngine.Color oldColor) (at Assets/Game Assets/Scripts/Gameplay/MasterController.cs:136)
MasterController.FloodFill (System.Int32 x, System.Int32 y, UnityEngine.Color fillColor, UnityEngine.Color oldColor) (at Assets/Game Assets/Scripts/Gameplay/MasterController.cs:138)

以下是更新平铺和获取平铺位置的代码:

public void UpdateTile(Transform tile, Color color)
{
    Mesh mesh = tile.GetComponent<MeshFilter>().mesh;

    Color32[] nextColor = new Color32[mesh.vertices.Length];

    for (int i = 0; i < nextColor.Length; i++)
    {
        nextColor[i] = color;
    }

    mesh.colors32 = nextColor;
}

public Tile GetTileAt(int x, int y)
{
    if (x > size || x < 0 || y > size || y < 0)
    {
        return null;
    }

    return tiles[x, y];
}
pbossiut

pbossiut1#

**大图像解决方案。**在我自己的一个项目中,我根据我发现的其他几个解决方案优化了一个解决方案。首先,这个解决方案通过使用一个带有Point堆栈的while循环避免了递归。其次,它通过检查堆栈中当前像素与目标像素的状态避免了自身重叠。如果当前像素是目标颜色,它只添加周围的项。这样,这个函数避免了再次查看一个像素,当指向的像素已经填充时快速退出。2这避免了使用不同的数据结构来存储你已经访问过的位置,正如Flater上面所建议的。3当试图填充大图像的大部分时,这个解决方案确实有它自己的弱点,下面将讨论。

System.Drawing.Image _image;// defined & populated elsewhere in my class.
    public void FloodFill(Point pt, Color replacementColor)
    {
        Color TargetColor = ((Bitmap)_image).GetPixel(pt.X, pt.Y);
        if (TargetColor == replacementColor)
        {             
            return;//Avoids overlap
        }
        Int32 overfill_counter = 0;
        Int32 overfill_max = 2400000;//Max pixels to traverse.
        
        Stack<Point> pixels = new Stack<Point>();
        pixels.Push(pt);
        while (pixels.Count > 0)
        {
            Point a = pixels.Pop();
            if(overfill_counter < overfill_max)
            {
                Color a_Color = ((Bitmap)_image).GetPixel(a.X, a.Y);
                
                if(a_Color == TargetColor)
                {
                    overfill_counter++; 
                    ((Bitmap)_image).SetPixel(a.X, a.Y, replacementColor);
                    if (a.X > 0) { pixels.Push(new Point(a.X - 1, a.Y)); }
                    if (a.X < _image.Width - 1) { pixels.Push(new Point(a.X + 1, a.Y)); }
                    if (a.Y > 0) { pixels.Push(new Point(a.X, a.Y - 1)); }
                    if (a.Y < _image.Height - 1) { pixels.Push(new Point(a.X, a.Y + 1));}
                }
            }
        }
        this.Invalidate();
        return;
    }//FloodFill()

其他优化我发现了其他直接使用位图的解决方案,在创建项目时,我通过在每个FloodFill开始时将System.Drawing.Image转换为位图来使用这些解决方案()调用,后来我发现这种转换消耗了大量内存,因为我正在处理的大型图像对象的BMP表示变得笨拙,相反,将System.Drawing.Image转换为位图并使用必要的GetPixel()和SetPixel()函数避免了大量的内存占用。2这使得编辑/泛洪大图像的部分非常友好。
弱点这个算法使用了一个Stack,当处理大图像时,它会很快变大。对于我的项目,限制泛色填充区域的大小是可以接受的,我已经在上面的代码中使用了overfill_counter。

vx6bjr1n

vx6bjr1n2#

经过几个小时的尝试,我终于找到了为什么事情不能正常工作的答案。在进入洪水填充步骤之前,还有几个步骤需要确认。
(Note:activeColor是方法外部的公共变量)

private void RevisedFloodFill(int x, int y, Color targetColor)
{
    if (isValid(x, y))
    {
        Tile node = world.GetTileAt(x, y);
        if (node.hasActiveTile)
        {
            if (node.currentColor != activeColor)
            {
                string target = ColorUtility.ToHtmlStringRGB(targetColor);
                string compare = ColorUtility.ToHtmlStringRGB(node.currentColor);

                if (string.Equals(target.Trim(), compare.Trim()))
                {
                    node.currentColor = activeColor;
                    UpdateTile(node.currentTile, activeColor);

                    RevisedFloodFill(x + 1, y, targetColor);
                    RevisedFloodFill(x - 1, y, targetColor);
                    RevisedFloodFill(x, y + 1, targetColor);
                    RevisedFloodFill(x, y - 1, targetColor);
                }
            }

        }

    }
}

private bool isValid(int x, int y)
{
    if (x >= 0 && x < boardSize && y >= 0 && y < boardSize)
    {
        return true;
    }

    return false;
}

然而,虽然这没有崩溃,但它并不完美。有一些情况下,它往往忽略下一个颜色,并填补了整个董事会与错误的颜色,所以我的比较仍然需要工作。

5f0d552i

5f0d552i3#

问题是

你遇到了一个递归问题,看看下面的伪代码会有所帮助:

function ColorThisTile()

    if needed set tile color

    check tile on the left
    check tile on the right
    check tile on the top
    check tile on the bottom

在我将要使用的示例中,让我们使用国际象棋棋盘作为参考:

假设我们从B4开始,这意味着该方法将对B4执行以下步骤:

if needed set B4 color

    check A4
    check C4
    check B5
    check B3

那么check A4需要什么呢?这是这个方法的另一个示例,对于A4,它执行以下操作

if needed set A4 color

    check ?4  // This does nothing since there's no tile to the left
    check B4  // The problem is here!
    check A5
    check A3

你最初的B4检查需要检查A4,随后的A4检查又会导致检查B4,这个新的B4检查会再次检查A4,那个A4检查会再次检查B4,如此无限重复。

解决方案

为了防止这个问题,你必须写你的逻辑,这样你就可以避免一遍又一遍地检查同一个单元格。
这可以通过几种方法来实现,但最简单的方法是跟踪您访问过的每个单元格,如果您对已经访问过的单元格运行检查,则立即从函数返回。
大致是这样的:

public void FloodFill(Cell cell, Color fillColor, List<Cell> visitedCells)
{
    if(visitedCells.Any(visitedCell => visitedCell.X = cell.X && visitedCell.Y = cell.Y)
        return;

    visitedCells.Add(cell);

    // the rest of the code

    FloodFill(cell.GetLeft(),  fillColor, visitedCells);
    FloodFill(cell.GetRight(), fillColor, visitedCells);
    FloodFill(cell.GetUp(),    fillColor, visitedCells);
    FloodFill(cell.GetDown(),  fillColor, visitedCells);
}

public class Cell
{
    public int X { get; set; }
    public int Y { get; set; }

    public Cell GetLeft()
    {
        return new Cell() { X = this.X-1, Y = this.Y };
    }

    public Cell GetRight()
    {
        return new Cell() { X = this.X+1, Y = this.Y };
    }

    public Cell GetUp()
    {
        return new Cell() { X = this.X, Y = this.Y-1 };
    }

    public Cell GetDown()
    {
        return new Cell() { X = this.X, Y = this.Y+1 };
    }
}

这可以确保您不会遇到两个邻居不断相互检查的问题。
我添加了Cell类,以避免在每个回合都要处理坐标变量-请务必检查您的环境中是否还没有现有的类/结构来表示2D点。

一个好建议

如果您记录了正在检查的单元格,您可能会遇到这个问题。

public void FloodFill(Cell cell, Color fillColor, List<Cell> visitedCells)
{
    _log.Info($"Checking cell ({cell.X},{cell.Y})");

    // ...
}

你会发现它会一遍又一遍地重复检查相同的两个单元格。

相关问题