Im working on a painting game but i can't get the flood fill algorithm to work on large areas. Its a recursive algorithm and i read that implementing a stack might work however i can get it to work as well. Here's the code;
private void FillCluster(int x, int y, int colorIndex, HashSet<string> traversedCells)
{
Debug.Log(colorIndex);
// Check if this cell is within the bounds of the picture and has a color number and the
if(x < 0 || x >= ActivePictureInfo.XCells ||
y < 0 || y >= ActivePictureInfo.YCells ||
ActivePictureInfo.ColorNumbers[y][x] == -1 ||
ActivePictureInfo.ColorNumbers[y][x] != colorIndex)
{
return;
}
string cellKey = string.Format("{0}_{1}", x, y);
// Check if this cell has already been traversed by FillBlob
if (traversedCells.Contains(cellKey))
{
return;
}
// Check if this cell is already colored in with the correct color
if (!ActivePictureInfo.HasProgress || ActivePictureInfo.Progress[y][x] != -1)
{
ColorCell(x, y, colorIndex);
}
// Add this cells key to the traversed hashset to indicate it has been processed
traversedCells.Add(cellKey);
// Recursively call recursively with the four cells adjacent to this cell
FillCluster(x - 1, y, colorIndex, traversedCells);
FillCluster(x 1, y, colorIndex, traversedCells);
FillCluster(x, y - 1, colorIndex, traversedCells);
FillCluster(x, y 1, colorIndex, traversedCells);
FillCluster(x - 1, y - 1, colorIndex, traversedCells);
FillCluster(x - 1, y 1, colorIndex, traversedCells);
FillCluster(x 1, y - 1, colorIndex, traversedCells);
FillCluster(x 1, y 1, colorIndex, traversedCells);
}
CodePudding user response:
a version without having to remember all visited cells because they are already marked by the color
private void FillCluster(int x, int y, int colorIndex)
{
Debug.Log(colorIndex);
var currentSeam = new Queue<PointDirection>();
if (FillPoint(x, y, colorIndex))
{
currentSeam.Enqueue(new PointDirection(x - 1, y, Direction.Left));
currentSeam.Enqueue(new PointDirection(x 1, y, Direction.Right));
currentSeam.Enqueue(new PointDirection(x, y - 1, Direction.Up));
currentSeam.Enqueue(new PointDirection(x, y 1, Direction.Down));
}
while (currentSeam.Count > 0)
{
var current = currentSeam.Dequeue();
if (FillPoint(current.X, current.Y, colorIndex))
{
if (current.Direction != Direction.Right)
currentSeam.Enqueue(new PointDirection(x - 1, y, Direction.Left));
if (current.Direction != Direction.Left)
currentSeam.Enqueue(new PointDirection(x 1, y, Direction.Right));
if (current.Direction != Direction.Down)
currentSeam.Enqueue(new PointDirection(x, y - 1, Direction.Up));
if (current.Direction != Direction.Up)
currentSeam.Enqueue(new PointDirection(x, y 1, Direction.Down));
}
}
}
private bool FillPoint(int x, int y, int colorIndex)
{
if (x < 0 || x >= ActivePictureInfo.XCells ||
y < 0 || y >= ActivePictureInfo.YCells ||
ActivePictureInfo.ColorNumbers[y][x] == -1 ||
ActivePictureInfo.ColorNumbers[y][x] == colorIndex)
{
return false;
}
ActivePictureInfo.ColorNumbers[y][x] = colorIndex;
return true;
}
private struct PointDirection
{
public PointDirection(int x, int y, Direction direction)
{
X = x;
Y = y;
Direction = direction;
}
public int X { get; }
public int Y { get; }
public Direction Direction { get; }
}
private enum Direction : byte
{
Up,
Right,
Down,
Left
}
CodePudding user response:
So the reason you are getting a stackOverflow is because the state of the previous iteration is saved on the stack unless both of these condition are met:
- You are doing tail recursion (look it up, but it isn't relevant right now)
- Your language optimizes tail recursion (C# doesn't)
As the stack size is limited and your algorithm very quickly reaches thousands of calls one after the other, you cannot make the recursive version work in C#, that is impossible, but it seems like you understood this already
Here is a pseudocode solution to do it without recursion. I don't know C# so the synthax is not correct, but the idea is correct, you will have to transform it into proper C#
private void FillCluster(int x, int y, int colorIndex, HashSet<string> traversedCells) {
Debug.Log(colorIndex);
// Check if this cell is within the bounds of the picture and has a color number and the
//Declare a set, add inside the current node
Set<Tuple> nodesToFill = new Set<>()
nodesToFill.add(new Tuple(x, y));
//Replace the recursion by a loop
for (Tuple tuple in nodesToFill) {
//initialize the current position
x = tuple.first
y = tuple.second
//Deal with the current node
if(FillClusterInner(x, y, colorIndex, traversedCells)) {
//Add the new nodes to the set instead of using recursion
nodesToFill.add(new Tuple(x-1, y))
nodesToFill.add(new Tuple(x 1, y))
nodesToFill.add(new Tuple(x, y - 1))
nodesToFill.add(new Tuple(x, y 1))
nodesToFill.add(new Tuple(x - 1, y - 1))
nodesToFill.add(new Tuple(x - 1, y 1))
nodesToFill.add(new Tuple(x 1, y - 1))
nodesToFill.add(new Tuple(x 1, y 1))
}
//Remove the current tuple from the set as you have dealt with it
nodesToFill.remove(tuple)
}
}
//This is a non-recursive method which fills a single specified node
bool FillClusterInner(int x, int y, int colorIndex, HashSet<string> traversedCells) {
if(x < 0 || x >= ActivePictureInfo.XCells ||
y < 0 || y >= ActivePictureInfo.YCells ||
ActivePictureInfo.ColorNumbers[y][x] == -1 ||
ActivePictureInfo.ColorNumbers[y][x] != colorIndex)
{
return false;
}
string cellKey = string.Format("{0}_{1}", x, y);
// Check if this cell has already been traversed by FillBlob
if (traversedCells.Contains(cellKey))
{
return false;
}
// Check if this cell is already colored in with the correct color
if (!ActivePictureInfo.HasProgress || ActivePictureInfo.Progress[y][x] != -1)
{
ColorCell(x, y, colorIndex);
}
// Add this cells key to the traversed hashset to indicate it has been processed
traversedCells.Add(cellKey);
return true;
}
You can see the idea: instead of recursion, we use a set, which contains all the indexes of the nodes we have yet to fill. You add to this set instead of calling the recursive function, and you remove from the set each position you have handled. When the set is empty you have filled every cell that had to be filled.