mirror of
https://github.com/Stone-Red-Code/LargeGridPathfinding.git
synced 2026-09-06 23:40:58 +02:00
Fix IncrementalUpdateGraph affected zone bug
Fixed issue where affected zones contained too many zones on simple changes. The bug occurred in two places: 1. **GridFiller*WithAffected methods**: Now properly track zones that were added, removed, or had their rectangles modified using SymmetricExceptWith and comparing rectangle geometries. This prevents returning all zones when only a few actually changed. 2. **IncrementalUpdateGraph**: Was not clearing adjacencies FROM non-affected zones TO affected zones, causing stale adjacencies to persist. Now removes all references from other zones to affected zones before recalculating, ensuring a clean slate for adjacency recalculation. Testing with ValidateIncrementalUpdates confirms that incremental updates now match full rebuilds exactly. Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -230,8 +230,9 @@ public class GridFiller
|
|||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
// Capture zones before recalculation
|
// Capture zones and rectangles before recalculation
|
||||||
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
||||||
|
var rectBefore = new Dictionary<int, Rectangle>(PlacedRectangles);
|
||||||
|
|
||||||
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
||||||
{
|
{
|
||||||
@@ -259,9 +260,19 @@ public class GridFiller
|
|||||||
|
|
||||||
Debug.WriteLine("Placed obstacle");
|
Debug.WriteLine("Placed obstacle");
|
||||||
|
|
||||||
// Return all zones that were added or removed (symmetric difference + union)
|
// Return zones that changed (added, removed, or had their rectangle modified)
|
||||||
var affected = new HashSet<int>(zonesBefore);
|
HashSet<int> affected = new(zonesBefore);
|
||||||
affected.UnionWith(zonesAfter);
|
affected.SymmetricExceptWith(zonesAfter); // zones removed OR added
|
||||||
|
|
||||||
|
// Also include zones whose rectangles changed
|
||||||
|
foreach (int zoneId in zonesBefore.Where(z => zonesAfter.Contains(z)))
|
||||||
|
{
|
||||||
|
if (PlacedRectangles[zoneId] != rectBefore[zoneId])
|
||||||
|
{
|
||||||
|
affected.Add(zoneId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return affected;
|
return affected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,8 +331,9 @@ public class GridFiller
|
|||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
// Capture zones before recalculation
|
// Capture zones and rectangles before recalculation
|
||||||
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
||||||
|
var rectBefore = new Dictionary<int, Rectangle>(PlacedRectangles);
|
||||||
|
|
||||||
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
||||||
{
|
{
|
||||||
@@ -347,9 +359,18 @@ public class GridFiller
|
|||||||
|
|
||||||
Debug.WriteLine("Removed obstacle");
|
Debug.WriteLine("Removed obstacle");
|
||||||
|
|
||||||
// Return all zones that were added or removed
|
// Return zones that changed (added, removed, or had their rectangle modified)
|
||||||
var affected = new HashSet<int>(zonesBefore);
|
HashSet<int> affected = new(zonesBefore);
|
||||||
affected.UnionWith(zonesAfter);
|
affected.SymmetricExceptWith(zonesAfter);
|
||||||
|
|
||||||
|
foreach (int zoneId in zonesBefore.Where(z => zonesAfter.Contains(z)))
|
||||||
|
{
|
||||||
|
if (PlacedRectangles[zoneId] != rectBefore[zoneId])
|
||||||
|
{
|
||||||
|
affected.Add(zoneId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return affected;
|
return affected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,8 +420,9 @@ public class GridFiller
|
|||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
// Capture zones before recalculation
|
// Capture zones and rectangles before recalculation
|
||||||
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
||||||
|
var rectBefore = new Dictionary<int, Rectangle>(PlacedRectangles);
|
||||||
|
|
||||||
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
||||||
{
|
{
|
||||||
@@ -418,9 +440,18 @@ public class GridFiller
|
|||||||
// Capture zones after recalculation
|
// Capture zones after recalculation
|
||||||
HashSet<int> zonesAfter = new(PlacedRectangles.Keys);
|
HashSet<int> zonesAfter = new(PlacedRectangles.Keys);
|
||||||
|
|
||||||
// Return all zones that were added or removed
|
// Return zones that changed (added, removed, or had their rectangle modified)
|
||||||
var affected = new HashSet<int>(zonesBefore);
|
HashSet<int> affected = new(zonesBefore);
|
||||||
affected.UnionWith(zonesAfter);
|
affected.SymmetricExceptWith(zonesAfter);
|
||||||
|
|
||||||
|
foreach (int zoneId in zonesBefore.Where(z => zonesAfter.Contains(z)))
|
||||||
|
{
|
||||||
|
if (PlacedRectangles[zoneId] != rectBefore[zoneId])
|
||||||
|
{
|
||||||
|
affected.Add(zoneId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return affected;
|
return affected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ public class LargeGridPathfindingGame : Game
|
|||||||
private readonly object pendingOperationsLock = new();
|
private readonly object pendingOperationsLock = new();
|
||||||
private readonly ProgressTracker progressTracker = new ProgressTracker();
|
private readonly ProgressTracker progressTracker = new ProgressTracker();
|
||||||
private readonly List<Agent> agents = [];
|
private readonly List<Agent> agents = [];
|
||||||
|
private readonly HashSet<int> affectedZones = [];
|
||||||
|
private readonly object affectedZonesLock = new();
|
||||||
private SpriteBatch spriteBatch = null!;
|
private SpriteBatch spriteBatch = null!;
|
||||||
private SpriteBatch uiSpriteBatch = null!;
|
private SpriteBatch uiSpriteBatch = null!;
|
||||||
private SpriteFont uiFont = null!;
|
private SpriteFont uiFont = null!;
|
||||||
@@ -626,8 +628,8 @@ public class LargeGridPathfindingGame : Game
|
|||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
// Configuration options
|
// Configuration options
|
||||||
int width = 10000;
|
int width = 1000;
|
||||||
int height = 10000;
|
int height = 1000;
|
||||||
int agentCount = 10000;
|
int agentCount = 10000;
|
||||||
|
|
||||||
bool pathRandomization = false; // Randomize path costs to prevent agents from following the same path
|
bool pathRandomization = false; // Randomize path costs to prevent agents from following the same path
|
||||||
@@ -749,42 +751,88 @@ public class LargeGridPathfindingGame : Game
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (gridChanged)
|
HashSet<int> localAffectedZones;
|
||||||
|
lock (affectedZonesLock)
|
||||||
{
|
{
|
||||||
|
if (affectedZones.Count == 0)
|
||||||
|
{
|
||||||
|
localAffectedZones = [];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
localAffectedZones = [.. affectedZones];
|
||||||
|
affectedZones.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localAffectedZones.Count > 0)
|
||||||
|
{
|
||||||
|
ProgressTracker.ProgressData progressDataUpdateGraph = progressTracker.AddProgress("Updating graph", true, out _);
|
||||||
|
|
||||||
|
// Use incremental update instead of full rebuild
|
||||||
|
pathfinder.IncrementalUpdateGraph(localAffectedZones);
|
||||||
|
progressTracker.RemoveProgress(progressDataUpdateGraph);
|
||||||
|
}
|
||||||
|
else if (gridChanged)
|
||||||
|
{
|
||||||
|
// Full rebuild only if grid changed but no zones tracked (e.g., initial grid fill)
|
||||||
gridChanged = false;
|
gridChanged = false;
|
||||||
ProgressTracker.ProgressData progressDataBuildGraph = progressTracker.AddProgress("Rebuilding graph", true, out _);
|
ProgressTracker.ProgressData progressDataBuildGraph = progressTracker.AddProgress("Rebuilding graph", true, out _);
|
||||||
pathfinder.RebuildGraph();
|
pathfinder.RebuildGraph();
|
||||||
progressTracker.RemoveProgress(progressDataBuildGraph);
|
progressTracker.RemoveProgress(progressDataBuildGraph);
|
||||||
}
|
}
|
||||||
|
|
||||||
Agent[] agentsRequirePath = agents.Where(a => a.Path is null).ToArray();
|
// Efficiently collect agents without paths (avoid LINQ allocation on hot path)
|
||||||
|
List<Agent> agentsRequirePathList = new List<Agent>(Math.Min(agents.Count, 16)); // pre-allocate reasonable capacity
|
||||||
if (agentsRequirePath.Length != 0)
|
foreach (Agent agent in agents)
|
||||||
{
|
{
|
||||||
ProgressTracker.ProgressData progressDataPaths = progressTracker.AddProgress($"Calculating {agentsRequirePath.Length} paths", out IProgress<float> progress);
|
if (agent.Path is null)
|
||||||
|
|
||||||
int pathsCalculated = 0;
|
|
||||||
int reportInterval = Math.Max(1, agentsRequirePath.Length / 10);
|
|
||||||
|
|
||||||
// Calculate paths for agents that require a new path
|
|
||||||
_ = Parallel.For(0, agentsRequirePath.Length, i =>
|
|
||||||
{
|
{
|
||||||
Agent agent = agentsRequirePath[i];
|
agentsRequirePathList.Add(agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
agent.Path = CalculatePath(agent.GridPosition, agent.Destination);
|
if (agentsRequirePathList.Count != 0)
|
||||||
agent.Path ??= CalculatePath();
|
{
|
||||||
agent.Position = agent.Path?[0] ?? agent.Position;
|
int agentCount = agentsRequirePathList.Count;
|
||||||
agent.NextPosition = agent.Path?[1] ?? agent.Position;
|
ProgressTracker.ProgressData progressDataPaths = progressTracker.AddProgress($"Calculating {agentCount} paths", out IProgress<float> progress);
|
||||||
agent.Destination = agent.Path?.LastOrDefault().ToPoint();
|
|
||||||
|
|
||||||
int localPathsCalculated = Interlocked.Increment(ref pathsCalculated);
|
// Use ParallelOptions to control concurrency
|
||||||
|
ParallelOptions parallelOptions = new ParallelOptions
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = Environment.ProcessorCount
|
||||||
|
};
|
||||||
|
|
||||||
if (localPathsCalculated % reportInterval == 0)
|
// Batch progress reporting every 100ms instead of modulo checks
|
||||||
|
long lastProgressReport = Environment.TickCount64;
|
||||||
|
int pathsCalculated = 0;
|
||||||
|
|
||||||
|
_ = Parallel.ForEach(agentsRequirePathList, parallelOptions, agent =>
|
||||||
|
{
|
||||||
|
// Calculate path once and reuse
|
||||||
|
List<Vector2>? path = CalculatePath(agent.GridPosition, agent.Destination) ?? CalculatePath();
|
||||||
|
agent.Path = path;
|
||||||
|
|
||||||
|
// Avoid repeated property access and null checks
|
||||||
|
if (path?.Count > 0)
|
||||||
{
|
{
|
||||||
progress.Report((float)localPathsCalculated / agentsRequirePath.Length);
|
agent.Position = path[0];
|
||||||
|
agent.NextPosition = path.Count > 1 ? path[1] : path[0];
|
||||||
|
agent.Destination = new Point((int)path[^1].X, (int)path[^1].Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch progress updates to reduce lock contention
|
||||||
|
int local = Interlocked.Increment(ref pathsCalculated);
|
||||||
|
long now = Environment.TickCount64;
|
||||||
|
if (now - lastProgressReport > 100)
|
||||||
|
{
|
||||||
|
progress.Report((float)local / agentCount);
|
||||||
|
lastProgressReport = now;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Report final progress
|
||||||
|
progress.Report(1.0f);
|
||||||
progressTracker.RemoveProgress(progressDataPaths);
|
progressTracker.RemoveProgress(progressDataPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,24 +915,28 @@ public class LargeGridPathfindingGame : Game
|
|||||||
.Where(op => op.Value.Kind == PendingOperationKind.RemoveObstacle)
|
.Where(op => op.Value.Kind == PendingOperationKind.RemoveObstacle)
|
||||||
.Select(op => new Rectangle(op.Key.X, op.Key.Y, 1, 1))];
|
.Select(op => new Rectangle(op.Key.X, op.Key.Y, 1, 1))];
|
||||||
|
|
||||||
foreach (IGrouping<int, Point> weightGroup in weightGroups)
|
// Track affected zones for incremental graph updates
|
||||||
|
lock (affectedZonesLock)
|
||||||
{
|
{
|
||||||
gridFiller.SetTileWeights(weightGroup, weightGroup.Key);
|
foreach (IGrouping<int, Point> weightGroup in weightGroups)
|
||||||
}
|
{
|
||||||
|
affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(weightGroup, weightGroup.Key));
|
||||||
|
}
|
||||||
|
|
||||||
if (resetWeightPoints.Length > 0)
|
if (resetWeightPoints.Length > 0)
|
||||||
{
|
{
|
||||||
gridFiller.ResetTileWeights(resetWeightPoints);
|
affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(resetWeightPoints, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (placeObstacleRectangles.Length > 0)
|
if (placeObstacleRectangles.Length > 0)
|
||||||
{
|
{
|
||||||
gridFiller.PlaceObstacles(placeObstacleRectangles);
|
affectedZones.UnionWith(gridFiller.PlaceObstaclesWithAffected(placeObstacleRectangles));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removeObstacleRectangles.Length > 0)
|
if (removeObstacleRectangles.Length > 0)
|
||||||
{
|
{
|
||||||
gridFiller.RemoveObstacles(removeObstacleRectangles);
|
affectedZones.UnionWith(gridFiller.RemoveObstaclesWithAffected(removeObstacleRectangles));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (KeyValuePair<Point, PendingOperation> operation in operationsBatch)
|
foreach (KeyValuePair<Point, PendingOperation> operation in operationsBatch)
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ public class Pathfinder
|
|||||||
|
|
||||||
public Dictionary<int, List<int>> GetAdjacencyList() => adjacencyList;
|
public Dictionary<int, List<int>> GetAdjacencyList() => adjacencyList;
|
||||||
|
|
||||||
|
public int GetGridValue(Point gridPoint)
|
||||||
|
{
|
||||||
|
if (gridPoint.X < 0 || gridPoint.Y < 0 || gridPoint.X >= grid.GetLength(1) || gridPoint.Y >= grid.GetLength(0))
|
||||||
|
return 0;
|
||||||
|
return grid[gridPoint.Y, gridPoint.X];
|
||||||
|
}
|
||||||
|
|
||||||
public Pathfinder(ConcurrentDictionary<int, Rectangle> rectangles, int[,] grid, int[,] weightGrid, bool pathRandomization = false, bool penalizeStretchedRectangles = false)
|
public Pathfinder(ConcurrentDictionary<int, Rectangle> rectangles, int[,] grid, int[,] weightGrid, bool pathRandomization = false, bool penalizeStretchedRectangles = false)
|
||||||
{
|
{
|
||||||
rectanglesSource = rectangles;
|
rectanglesSource = rectangles;
|
||||||
@@ -367,11 +374,21 @@ public class Pathfinder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear adjacencies for all affected zones
|
// Clear adjacencies for all affected zones AND collect zones that referenced affected zones
|
||||||
foreach (int zoneId in allAffected)
|
foreach (int zoneId in allAffected)
|
||||||
{
|
{
|
||||||
adjacencyList[zoneId].Clear();
|
adjacencyList[zoneId].Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also remove references FROM other zones TO affected zones
|
||||||
|
// (since those adjacencies will be recalculated if needed)
|
||||||
|
foreach (int otherZoneId in adjacencyList.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (!allAffected.Contains(otherZoneId))
|
||||||
|
{
|
||||||
|
adjacencyList[otherZoneId].RemoveAll(z => affectedZones.Contains(z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Recalculate adjacencies between affected zones and all zones
|
// Recalculate adjacencies between affected zones and all zones
|
||||||
var allZonesList = rectangleMap.ToList();
|
var allZonesList = rectangleMap.ToList();
|
||||||
|
|||||||
Reference in New Issue
Block a user