mirror of
https://github.com/Stone-Red-Code/LargeGridPathfinding.git
synced 2026-09-04 00:56:10 +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)
|
||||
{
|
||||
// Capture zones before recalculation
|
||||
// Capture zones and rectangles before recalculation
|
||||
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
||||
var rectBefore = new Dictionary<int, Rectangle>(PlacedRectangles);
|
||||
|
||||
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
||||
{
|
||||
@@ -259,9 +260,19 @@ public class GridFiller
|
||||
|
||||
Debug.WriteLine("Placed obstacle");
|
||||
|
||||
// Return all zones that were added or removed (symmetric difference + union)
|
||||
var affected = new HashSet<int>(zonesBefore);
|
||||
affected.UnionWith(zonesAfter);
|
||||
// Return zones that changed (added, removed, or had their rectangle modified)
|
||||
HashSet<int> affected = new(zonesBefore);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -320,8 +331,9 @@ public class GridFiller
|
||||
|
||||
if (changed)
|
||||
{
|
||||
// Capture zones before recalculation
|
||||
// Capture zones and rectangles before recalculation
|
||||
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
||||
var rectBefore = new Dictionary<int, Rectangle>(PlacedRectangles);
|
||||
|
||||
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
||||
{
|
||||
@@ -347,9 +359,18 @@ public class GridFiller
|
||||
|
||||
Debug.WriteLine("Removed obstacle");
|
||||
|
||||
// Return all zones that were added or removed
|
||||
var affected = new HashSet<int>(zonesBefore);
|
||||
affected.UnionWith(zonesAfter);
|
||||
// Return zones that changed (added, removed, or had their rectangle modified)
|
||||
HashSet<int> affected = new(zonesBefore);
|
||||
affected.SymmetricExceptWith(zonesAfter);
|
||||
|
||||
foreach (int zoneId in zonesBefore.Where(z => zonesAfter.Contains(z)))
|
||||
{
|
||||
if (PlacedRectangles[zoneId] != rectBefore[zoneId])
|
||||
{
|
||||
affected.Add(zoneId);
|
||||
}
|
||||
}
|
||||
|
||||
return affected;
|
||||
}
|
||||
|
||||
@@ -399,8 +420,9 @@ public class GridFiller
|
||||
|
||||
if (changed)
|
||||
{
|
||||
// Capture zones before recalculation
|
||||
// Capture zones and rectangles before recalculation
|
||||
HashSet<int> zonesBefore = new(PlacedRectangles.Keys);
|
||||
var rectBefore = new Dictionary<int, Rectangle>(PlacedRectangles);
|
||||
|
||||
RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () =>
|
||||
{
|
||||
@@ -418,9 +440,18 @@ public class GridFiller
|
||||
// Capture zones after recalculation
|
||||
HashSet<int> zonesAfter = new(PlacedRectangles.Keys);
|
||||
|
||||
// Return all zones that were added or removed
|
||||
var affected = new HashSet<int>(zonesBefore);
|
||||
affected.UnionWith(zonesAfter);
|
||||
// Return zones that changed (added, removed, or had their rectangle modified)
|
||||
HashSet<int> affected = new(zonesBefore);
|
||||
affected.SymmetricExceptWith(zonesAfter);
|
||||
|
||||
foreach (int zoneId in zonesBefore.Where(z => zonesAfter.Contains(z)))
|
||||
{
|
||||
if (PlacedRectangles[zoneId] != rectBefore[zoneId])
|
||||
{
|
||||
affected.Add(zoneId);
|
||||
}
|
||||
}
|
||||
|
||||
return affected;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ public class LargeGridPathfindingGame : Game
|
||||
private readonly object pendingOperationsLock = new();
|
||||
private readonly ProgressTracker progressTracker = new ProgressTracker();
|
||||
private readonly List<Agent> agents = [];
|
||||
private readonly HashSet<int> affectedZones = [];
|
||||
private readonly object affectedZonesLock = new();
|
||||
private SpriteBatch spriteBatch = null!;
|
||||
private SpriteBatch uiSpriteBatch = null!;
|
||||
private SpriteFont uiFont = null!;
|
||||
@@ -626,8 +628,8 @@ public class LargeGridPathfindingGame : Game
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
// Configuration options
|
||||
int width = 10000;
|
||||
int height = 10000;
|
||||
int width = 1000;
|
||||
int height = 1000;
|
||||
int agentCount = 10000;
|
||||
|
||||
bool pathRandomization = false; // Randomize path costs to prevent agents from following the same path
|
||||
@@ -749,42 +751,88 @@ public class LargeGridPathfindingGame : Game
|
||||
{
|
||||
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;
|
||||
ProgressTracker.ProgressData progressDataBuildGraph = progressTracker.AddProgress("Rebuilding graph", true, out _);
|
||||
pathfinder.RebuildGraph();
|
||||
progressTracker.RemoveProgress(progressDataBuildGraph);
|
||||
}
|
||||
|
||||
Agent[] agentsRequirePath = agents.Where(a => a.Path is null).ToArray();
|
||||
|
||||
if (agentsRequirePath.Length != 0)
|
||||
// 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
|
||||
foreach (Agent agent in agents)
|
||||
{
|
||||
ProgressTracker.ProgressData progressDataPaths = progressTracker.AddProgress($"Calculating {agentsRequirePath.Length} paths", out IProgress<float> progress);
|
||||
|
||||
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 =>
|
||||
if (agent.Path is null)
|
||||
{
|
||||
Agent agent = agentsRequirePath[i];
|
||||
agentsRequirePathList.Add(agent);
|
||||
}
|
||||
}
|
||||
|
||||
agent.Path = CalculatePath(agent.GridPosition, agent.Destination);
|
||||
agent.Path ??= CalculatePath();
|
||||
agent.Position = agent.Path?[0] ?? agent.Position;
|
||||
agent.NextPosition = agent.Path?[1] ?? agent.Position;
|
||||
agent.Destination = agent.Path?.LastOrDefault().ToPoint();
|
||||
if (agentsRequirePathList.Count != 0)
|
||||
{
|
||||
int agentCount = agentsRequirePathList.Count;
|
||||
ProgressTracker.ProgressData progressDataPaths = progressTracker.AddProgress($"Calculating {agentCount} paths", out IProgress<float> progress);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -867,24 +915,28 @@ public class LargeGridPathfindingGame : Game
|
||||
.Where(op => op.Value.Kind == PendingOperationKind.RemoveObstacle)
|
||||
.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)
|
||||
{
|
||||
gridFiller.ResetTileWeights(resetWeightPoints);
|
||||
}
|
||||
if (resetWeightPoints.Length > 0)
|
||||
{
|
||||
affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(resetWeightPoints, 1));
|
||||
}
|
||||
|
||||
if (placeObstacleRectangles.Length > 0)
|
||||
{
|
||||
gridFiller.PlaceObstacles(placeObstacleRectangles);
|
||||
}
|
||||
if (placeObstacleRectangles.Length > 0)
|
||||
{
|
||||
affectedZones.UnionWith(gridFiller.PlaceObstaclesWithAffected(placeObstacleRectangles));
|
||||
}
|
||||
|
||||
if (removeObstacleRectangles.Length > 0)
|
||||
{
|
||||
gridFiller.RemoveObstacles(removeObstacleRectangles);
|
||||
if (removeObstacleRectangles.Length > 0)
|
||||
{
|
||||
affectedZones.UnionWith(gridFiller.RemoveObstaclesWithAffected(removeObstacleRectangles));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<Point, PendingOperation> operation in operationsBatch)
|
||||
|
||||
@@ -21,6 +21,13 @@ public class Pathfinder
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
var allZonesList = rectangleMap.ToList();
|
||||
|
||||
Reference in New Issue
Block a user