Add thread safety and optimize grid obstacle, weight, and recalculation logic

This commit is contained in:
Stone_Red
2026-05-05 01:28:21 +02:00
parent cd9718d61c
commit 1b79459b36
2 changed files with 305 additions and 237 deletions
+178 -105
View File
@@ -11,6 +11,8 @@ namespace LargeGridPathfinding;
internal class GridFiller internal class GridFiller
{ {
private const int RecalculationRadius = 24;
private readonly object mutationLock = new();
private int currentLabel = 1; private int currentLabel = 1;
private int currentObstacleLabel = -1; private int currentObstacleLabel = -1;
public int[,] Grid { get; } public int[,] Grid { get; }
@@ -35,13 +37,12 @@ internal class GridFiller
} }
} }
foreach (Rectangle rectangle in obstacles) PlaceObstacles(obstacles);
{
PlaceObstacle(rectangle);
}
} }
public void FillGrid(int? x1 = null, int? y1 = null, int? x2 = null, int? y2 = null, bool fillAll = false, IProgress<float>? totalProgress = null, IProgress<float>? calculatingCandidatesProgress = null, IProgress<float>? placingCandidatesProgress = null) public void FillGrid(int? x1 = null, int? y1 = null, int? x2 = null, int? y2 = null, bool fillAll = false, IProgress<float>? totalProgress = null, IProgress<float>? calculatingCandidatesProgress = null, IProgress<float>? placingCandidatesProgress = null)
{
lock (mutationLock)
{ {
x1 ??= 0; x1 ??= 0;
y1 ??= 0; y1 ??= 0;
@@ -63,13 +64,12 @@ internal class GridFiller
} }
} }
// Collect all valid rectangle placements
ConcurrentBag<(int x, int y, int w, int h, int weight)> candidates = []; ConcurrentBag<(int x, int y, int w, int h, int weight)> candidates = [];
int cells = Grid.GetLength(0) * Grid.GetLength(1); int cells = Grid.GetLength(0) * Grid.GetLength(1);
int areaPlaced = 0; int areaPlaced = 0;
int maxRectangleSize = Math.Max((Grid.GetLength(0) + Grid.GetLength(1)) / 20, 100); int maxRectangleSize = Math.Max((Grid.GetLength(0) + Grid.GetLength(1)) / 20, 100);
int reportInterval = cells / 100; int reportInterval = Math.Max(cells / 100, 1);
do do
{ {
@@ -77,7 +77,6 @@ internal class GridFiller
Debug.WriteLine("Calculating candidates..."); Debug.WriteLine("Calculating candidates...");
// Calculate all possible rectangle placements
_ = Parallel.For(y1.Value, y2.Value, (y, loopState) => _ = Parallel.For(y1.Value, y2.Value, (y, loopState) =>
{ {
for (int x = x1.Value; x < x2; x++) for (int x = x1.Value; x < x2; x++)
@@ -110,7 +109,6 @@ internal class GridFiller
Debug.WriteLine($"Candidates: {candidates.Count}"); Debug.WriteLine($"Candidates: {candidates.Count}");
Debug.WriteLine("Sorting candidates..."); Debug.WriteLine("Sorting candidates...");
// Sort by area (largest first) and prefer less stretched rectangles on ties
List<(int x, int y, int w, int h, int weight)> sortedCandidates = [.. candidates]; List<(int x, int y, int w, int h, int weight)> sortedCandidates = [.. candidates];
sortedCandidates.Sort((a, b) => sortedCandidates.Sort((a, b) =>
{ {
@@ -136,10 +134,8 @@ internal class GridFiller
int placed = 0; int placed = 0;
int reportIntervalPlacing = Math.Max(sortedCandidates.Count / 10, 1); int reportIntervalPlacing = Math.Max(sortedCandidates.Count / 10, 1);
// Place rectangles, ensuring no overlap
foreach ((int x, int y, int w, int h, int weight) in sortedCandidates) foreach ((int x, int y, int w, int h, int weight) in sortedCandidates)
{ {
// Check if area is still free
if (IsAreaFree(x, y, w, h, weight)) if (IsAreaFree(x, y, w, h, weight))
{ {
Rectangle rectangle = new Rectangle(x, y, w, h); Rectangle rectangle = new Rectangle(x, y, w, h);
@@ -164,92 +160,123 @@ internal class GridFiller
calculatingCandidatesProgress?.Report(1); calculatingCandidatesProgress?.Report(1);
placingCandidatesProgress?.Report(1); placingCandidatesProgress?.Report(1);
} }
}
public void PlaceObstacle(Rectangle rectangle) public void PlaceObstacle(Rectangle rectangle)
{ {
// Check area around the rectangle to update all adjacent rectangles PlaceObstacles([rectangle]);
int y = int.Clamp(rectangle.Y - 1, 0, Height);
int x = int.Clamp(rectangle.X - 1, 0, Width);
int h = int.Clamp(rectangle.Y + rectangle.Height + 2, 0, Height);
int w = int.Clamp(rectangle.X + rectangle.Width + 2, 0, Width);
List<Rectangle> removedRectangles = [new Rectangle(x, y, w - x, h - y)];
int obstacle = currentObstacleLabel--;
for (int dy = y; dy < h; dy++)
{
for (int dx = x; dx < w; dx++)
{
if (Grid[dy, dx] > 0 && PlacedRectangles.ContainsKey(Grid[dy, dx]))
{
removedRectangles.Add(RemoveRectangle(Grid[dy, dx]));
} }
if (Grid[dy, dx] >= 0 && dy < rectangle.Y + rectangle.Height && dx < rectangle.X + rectangle.Width && dy >= rectangle.Y && dx >= rectangle.X) public void PlaceObstacles(IEnumerable<Rectangle> rectangles)
{
lock (mutationLock)
{
List<Rectangle> clampedRectangles = [.. rectangles
.Select(ClampToGrid)
.Where(r => r != Rectangle.Empty)];
if (clampedRectangles.Count == 0)
{
return;
}
int minX = Width;
int minY = Height;
int maxX = -1;
int maxY = -1;
bool changed = false;
foreach (Rectangle rectangle in clampedRectangles)
{
for (int dy = rectangle.Top; dy < rectangle.Bottom; dy++)
{
for (int dx = rectangle.Left; dx < rectangle.Right; dx++)
{
if (Grid[dy, dx] >= 0)
{
changed = true;
}
}
}
minX = Math.Min(minX, rectangle.Left);
minY = Math.Min(minY, rectangle.Top);
maxX = Math.Max(maxX, rectangle.Right);
maxY = Math.Max(maxY, rectangle.Bottom);
}
if (changed)
{
RecalculateAroundArea(minX, minY, maxX, maxY, () =>
{
foreach (Rectangle rectangle in clampedRectangles)
{
int obstacle = currentObstacleLabel--;
for (int dy = rectangle.Top; dy < rectangle.Bottom; dy++)
{
for (int dx = rectangle.Left; dx < rectangle.Right; dx++)
{
if (Grid[dy, dx] >= 0)
{ {
Grid[dy, dx] = obstacle; Grid[dy, dx] = obstacle;
WeightGrid[dy, dx] = 1; WeightGrid[dy, dx] = 1;
} }
} }
} }
}
if (removedRectangles.Count == 1) });
{ }
return;
} }
int minStartX = removedRectangles.Min(r => r.X);
int minStartY = removedRectangles.Min(r => r.Y);
int maxEndX = removedRectangles.Max(r => r.X + r.Width);
int maxEndY = removedRectangles.Max(r => r.Y + r.Height);
FillGrid(minStartX, minStartY, maxEndX, maxEndY);
Debug.WriteLine("Placed obstacle"); Debug.WriteLine("Placed obstacle");
Debug.WriteLine($"Placed obstacle {rectangle.X}, {rectangle.Y}, {rectangle.Width}, {rectangle.Height}");
Debug.WriteLine($"minX: {minStartX}, minY: {minStartY}, maxX: {maxEndX}, maxY: {maxEndY}");
} }
public void RemoveObstacle(Rectangle rectangle) public void RemoveObstacle(Rectangle rectangle)
{ {
// Check area around the rectangle to update all adjacent rectangles lock (mutationLock)
int y = int.Clamp(rectangle.Y - 1, 0, Height);
int x = int.Clamp(rectangle.X - 1, 0, Width);
int h = int.Clamp(rectangle.Y + rectangle.Height + 2, 0, Height);
int w = int.Clamp(rectangle.X + rectangle.Width + 2, 0, Width);
List<Rectangle> removedRectangles = [new Rectangle(x, y, w - x, h - y)];
for (int dy = y; dy < h; dy++)
{ {
for (int dx = x; dx < w; dx++) Rectangle clampedRectangle = ClampToGrid(rectangle);
if (clampedRectangle == Rectangle.Empty)
{ {
if (Grid[dy, dx] > 0) return;
{
removedRectangles.Add(RemoveRectangle(Grid[dy, dx]));
} }
if (Grid[dy, dx] < 0 && dy < rectangle.Y + rectangle.Height && dx < rectangle.X + rectangle.Width && dy >= rectangle.Y && dx >= rectangle.X) bool changed = false;
for (int dy = clampedRectangle.Top; dy < clampedRectangle.Bottom; dy++)
{
for (int dx = clampedRectangle.Left; dx < clampedRectangle.Right; dx++)
{
if (Grid[dy, dx] < 0)
{
Grid[dy, dx] = 0;
WeightGrid[dy, dx] = 1;
changed = true;
}
}
}
if (changed)
{
RecalculateAroundArea(clampedRectangle.Left, clampedRectangle.Top, clampedRectangle.Right, clampedRectangle.Bottom, () =>
{
for (int dy = clampedRectangle.Top; dy < clampedRectangle.Bottom; dy++)
{
for (int dx = clampedRectangle.Left; dx < clampedRectangle.Right; dx++)
{
if (Grid[dy, dx] < 0)
{ {
Grid[dy, dx] = 0; Grid[dy, dx] = 0;
WeightGrid[dy, dx] = 1; WeightGrid[dy, dx] = 1;
} }
} }
} }
});
int minStartX = removedRectangles.Min(r => r.X); }
int minStartY = removedRectangles.Min(r => r.Y); }
int maxEndX = removedRectangles.Max(r => r.X + r.Width);
int maxEndY = removedRectangles.Max(r => r.Y + r.Height);
FillGrid(minStartX, minStartY, maxEndX, maxEndY);
Debug.WriteLine("Removed obstacle"); Debug.WriteLine("Removed obstacle");
Debug.WriteLine($"Placed obstacle {rectangle.X}, {rectangle.Y}, {rectangle.Width}, {rectangle.Height}");
Debug.WriteLine($"minX: {minStartX}, minY: {minStartY}, maxX: {maxEndX}, maxY: {maxEndY}");
} }
public void SetTileWeight(int x, int y, int weight) public void SetTileWeight(int x, int y, int weight)
@@ -263,14 +290,15 @@ internal class GridFiller
} }
public void SetTileWeights(IEnumerable<Point> points, int weight) public void SetTileWeights(IEnumerable<Point> points, int weight)
{
lock (mutationLock)
{ {
int clampedWeight = Math.Max(1, weight); int clampedWeight = Math.Max(1, weight);
HashSet<Point> changedPoints = [];
int minX = Width; int minX = Width;
int minY = Height; int minY = Height;
int maxX = -1; int maxX = -1;
int maxY = -1; int maxY = -1;
bool changed = false;
foreach (Point point in points) foreach (Point point in points)
{ {
@@ -279,49 +307,29 @@ internal class GridFiller
continue; continue;
} }
_ = changedPoints.Add(point); changed = true;
minX = Math.Min(minX, point.X); minX = Math.Min(minX, point.X);
minY = Math.Min(minY, point.Y); minY = Math.Min(minY, point.Y);
maxX = Math.Max(maxX, point.X); maxX = Math.Max(maxX, point.X + 1);
maxY = Math.Max(maxY, point.Y); maxY = Math.Max(maxY, point.Y + 1);
} }
if (changedPoints.Count == 0) if (changed)
{ {
return; RecalculateAroundArea(minX, minY, maxX, maxY, () =>
{
foreach (Point point in points)
{
if (point.X < 0 || point.Y < 0 || point.X >= Width || point.Y >= Height || Grid[point.Y, point.X] < 0)
{
continue;
} }
int x = int.Clamp(minX - 1, 0, Width);
int y = int.Clamp(minY - 1, 0, Height);
int w = int.Clamp(maxX + 2, 0, Width);
int h = int.Clamp(maxY + 2, 0, Height);
List<Rectangle> removedRectangles = [new Rectangle(x, y, w - x, h - y)];
HashSet<int> removedLabels = [];
for (int dy = y; dy < h; dy++)
{
for (int dx = x; dx < w; dx++)
{
int label = Grid[dy, dx];
if (label > 0 && removedLabels.Add(label))
{
removedRectangles.Add(RemoveRectangle(label));
}
}
}
foreach (Point point in changedPoints)
{
WeightGrid[point.Y, point.X] = clampedWeight; WeightGrid[point.Y, point.X] = clampedWeight;
} }
});
int minStartX = removedRectangles.Min(r => r.X); }
int minStartY = removedRectangles.Min(r => r.Y); }
int maxEndX = removedRectangles.Max(r => r.X + r.Width);
int maxEndY = removedRectangles.Max(r => r.Y + r.Height);
FillGrid(minStartX, minStartY, maxEndX, maxEndY);
} }
public void ResetTileWeights(IEnumerable<Point> points) public void ResetTileWeights(IEnumerable<Point> points)
@@ -329,14 +337,13 @@ internal class GridFiller
SetTileWeights(points, 1); SetTileWeights(points, 1);
} }
// Helper method to check if a rectangle can still be placed
private bool IsAreaFree(int x, int y, int w, int h, int requiredWeight) private bool IsAreaFree(int x, int y, int w, int h, int requiredWeight)
{ {
for (int dy = 0; dy < h; dy++) for (int dy = 0; dy < h; dy++)
{ {
for (int dx = 0; dx < w; dx++) for (int dx = 0; dx < w; dx++)
{ {
if (Grid[y + dy, x + dx] != 0 || WeightGrid[y + dy, x + dx] != requiredWeight) // Not empty or mismatched weight if (Grid[y + dy, x + dx] != 0 || WeightGrid[y + dy, x + dx] != requiredWeight)
{ {
return false; return false;
} }
@@ -428,6 +435,73 @@ internal class GridFiller
return rectangle; return rectangle;
} }
private void RecalculateAroundArea(int minX, int minY, int maxX, int maxY, Action applyChanges)
{
int left = int.Clamp(minX - RecalculationRadius, 0, Width);
int top = int.Clamp(minY - RecalculationRadius, 0, Height);
int right = int.Clamp(maxX + RecalculationRadius, 0, Width);
int bottom = int.Clamp(maxY + RecalculationRadius, 0, Height);
if (left >= right || top >= bottom)
{
applyChanges();
return;
}
HashSet<int> labelsToRemove = [];
for (int y = top; y < bottom; y++)
{
for (int x = left; x < right; x++)
{
int label = Grid[y, x];
if (label > 0)
{
_ = labelsToRemove.Add(label);
}
}
}
List<Rectangle> removedRectangles = [];
foreach (int label in labelsToRemove)
{
Rectangle removed = RemoveRectangle(label);
if (removed != Rectangle.Empty)
{
removedRectangles.Add(removed);
}
}
if (removedRectangles.Count == 0)
{
applyChanges();
FillGrid(left, top, right, bottom);
return;
}
int fillLeft = Math.Min(left, removedRectangles.Min(r => r.Left));
int fillTop = Math.Min(top, removedRectangles.Min(r => r.Top));
int fillRight = Math.Max(right, removedRectangles.Max(r => r.Right));
int fillBottom = Math.Max(bottom, removedRectangles.Max(r => r.Bottom));
applyChanges();
FillGrid(fillLeft, fillTop, fillRight, fillBottom);
}
private Rectangle ClampToGrid(Rectangle rectangle)
{
int left = int.Clamp(rectangle.Left, 0, Width);
int top = int.Clamp(rectangle.Top, 0, Height);
int right = int.Clamp(rectangle.Right, 0, Width);
int bottom = int.Clamp(rectangle.Bottom, 0, Height);
if (right <= left || bottom <= top)
{
return Rectangle.Empty;
}
return new Rectangle(left, top, right - left, bottom - top);
}
private int GetRowContinuousWidth(int startX, int y, int maxWidth, int requiredWeight) private int GetRowContinuousWidth(int startX, int y, int maxWidth, int requiredWeight)
{ {
int width = 0; int width = 0;
@@ -457,5 +531,4 @@ internal class GridFiller
int maxSide = Math.Max(width, height); int maxSide = Math.Max(width, height);
return maxSide / minSide; return maxSide / minSide;
} }
} }
@@ -286,13 +286,8 @@ public class LargeGridPathfindingGame : Game
} }
else else
{ {
foreach (Point point in brushPoints) List<Rectangle> obstacleRectangles = [.. brushPoints.Select(point => new Rectangle(point.X, point.Y, 1, 1))];
{ gridFiller.PlaceObstacles(obstacleRectangles);
if (point.X >= 0 && point.Y >= 0 && point.X < gridFiller.Width && point.Y < gridFiller.Height && gridFiller.Grid[point.Y, point.X] >= 0)
{
gridFiller.PlaceObstacle(new Rectangle(point.X, point.Y, 1, 1));
}
}
} }
foreach (Point point in brushPoints) foreach (Point point in brushPoints)