Add xml docs

This commit is contained in:
Stone_Red
2026-05-05 23:37:43 +02:00
parent 83bb3dc2f3
commit ced5509983
6 changed files with 169 additions and 12 deletions
+18
View File
@@ -5,15 +5,33 @@ using System.Collections.Generic;
namespace LargeGridPathfinding;
/// <summary>
/// Represents a moving entity that follows a precomputed grid path.
/// </summary>
internal class Agent(Vector2 position)
{
/// <summary>
/// Gets or sets the current world-grid position as floating point coordinates.
/// </summary>
public Vector2 Position { get; set; } = position;
/// <summary>
/// Gets the integer grid cell for the current position.
/// </summary>
public Point GridPosition => new Point((int)Math.Round(Position.X), (int)Math.Round(Position.Y));
/// <summary>
/// Gets or sets the next waypoint currently being approached.
/// </summary>
public Vector2 NextPosition { get; set; }
/// <summary>
/// Gets or sets the current target destination.
/// </summary>
public Point? Destination { get; set; }
/// <summary>
/// Gets or sets the active path.
/// </summary>
public List<Vector2>? Path { get; set; }
}
+53 -3
View File
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding;
/// <summary>
/// Maintains grid labels/weights and generates rectangular walkable zones.
/// </summary>
public class GridFiller
{
private const int RecalculationRadius = 24;
@@ -21,6 +24,9 @@ public class GridFiller
public int Width { get; }
public int Height { get; }
/// <summary>
/// Initializes an empty grid with default tile weights.
/// </summary>
public GridFiller(int width, int height)
{
Width = width;
@@ -38,6 +44,9 @@ public class GridFiller
}
}
/// <summary>
/// Fills the selected region with maximal same-weight rectangles.
/// </summary>
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)
@@ -185,16 +194,25 @@ public class GridFiller
}
}
/// <summary>
/// Places a single obstacle rectangle.
/// </summary>
public void PlaceObstacle(Rectangle rectangle)
{
PlaceObstacles([rectangle]);
}
/// <summary>
/// Places obstacle rectangles and optionally recalculates affected zones.
/// </summary>
public void PlaceObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{
_ = PlaceObstaclesWithAffected(rectangles, recalculate);
}
/// <summary>
/// Places obstacles and returns changed zone labels.
/// </summary>
public HashSet<int> PlaceObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{
lock (mutationLock)
@@ -266,7 +284,7 @@ public class GridFiller
Debug.WriteLine("Placed obstacle");
// Return zones that changed (added, removed, or had their rectangle modified)
// Symmetric diff captures both split and merge operations from recalculation.
HashSet<int> affected = [.. zonesBefore];
affected.SymmetricExceptWith(zonesAfter); // zones removed OR added
@@ -286,16 +304,25 @@ public class GridFiller
}
}
/// <summary>
/// Removes a single obstacle rectangle.
/// </summary>
public void RemoveObstacle(Rectangle rectangle)
{
RemoveObstacles([rectangle]);
}
/// <summary>
/// Removes obstacle rectangles and optionally recalculates affected zones.
/// </summary>
public void RemoveObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{
_ = RemoveObstaclesWithAffected(rectangles, recalculate);
}
/// <summary>
/// Removes obstacles and returns changed zone labels.
/// </summary>
public HashSet<int> RemoveObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{
lock (mutationLock)
@@ -384,21 +411,33 @@ public class GridFiller
}
}
/// <summary>
/// Sets a single tile weight.
/// </summary>
public void SetTileWeight(int x, int y, int weight)
{
SetTileWeights([new Point(x, y)], weight);
}
/// <summary>
/// Resets a single tile weight to default.
/// </summary>
public void ResetTileWeight(int x, int y)
{
ResetTileWeights([new Point(x, y)]);
}
/// <summary>
/// Sets weights for tiles and optionally recalculates affected zones.
/// </summary>
public void SetTileWeights(IEnumerable<Point> points, int weight, bool recalculate = true)
{
_ = SetTileWeightsWithAffected(points, weight, recalculate);
}
/// <summary>
/// Sets weights and returns changed zone labels.
/// </summary>
public HashSet<int> SetTileWeightsWithAffected(IEnumerable<Point> points, int weight, bool recalculate = true)
{
lock (mutationLock)
@@ -465,9 +504,17 @@ public class GridFiller
}
}
public void ResetTileWeights(IEnumerable<Point> points)
public HashSet<int> ResetTileWeightsWithAffected(IEnumerable<Point> points, bool recalculate = true)
{
SetTileWeights(points, 1);
return SetTileWeightsWithAffected(points, 1, recalculate);
}
/// <summary>
/// Resets weights for multiple tiles to default.
/// </summary>
public void ResetTileWeights(IEnumerable<Point> points, bool recalculate = true)
{
SetTileWeights(points, 1, recalculate);
}
private bool IsAreaFree(int x, int y, int w, int h, int requiredWeight)
@@ -648,6 +695,7 @@ public class GridFiller
if (removedRectangles.Count == 0)
{
applyChanges();
// Even if no existing zone was removed, edits may have opened space that needs fresh zoning.
FillGrid(left, top, right, bottom);
return;
}
@@ -657,6 +705,8 @@ public class GridFiller
int fillRight = Math.Max(right, removedRectangles.Max(r => r.Right));
int fillBottom = Math.Max(bottom, removedRectangles.Max(r => r.Bottom));
// Expanding fill bounds to include removed rectangle extents avoids edge artifacts
// where old zone boundaries would otherwise leave suboptimal splits near the edit area.
applyChanges();
FillGrid(fillLeft, fillTop, fillRight, fillBottom);
}
+6
View File
@@ -5,8 +5,14 @@ using MonoGame.Extended.Input;
namespace LargeGridPathfinding;
/// <summary>
/// Input extension helpers for game controls.
/// </summary>
internal static class InputHelper
{
/// <summary>
/// Converts WASD keyboard input into a movement vector.
/// </summary>
public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState)
{
Vector2 movementDirection = Vector2.Zero;
@@ -15,6 +15,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding;
/// <summary>
/// Main game host that handles UI, map editing, rendering, and background path updates.
/// </summary>
public class LargeGridPathfindingGame : Game
{
private enum BrushMode
@@ -42,10 +45,24 @@ public class LargeGridPathfindingGame : Game
PenalizeStretchedRectangles
}
/// <summary>
/// Supported map presets for startup generation.
/// </summary>
private enum StartupMapPreset
{
/// <summary>
/// Procedural room-like map with random walls and doors.
/// </summary>
Rooms,
/// <summary>
/// No generated obstacles.
/// </summary>
Empty,
/// <summary>
/// Dense 3x3 room lattice with deterministic offset doors for heavy zone fragmentation.
/// </summary>
WorstCase
}
@@ -78,7 +95,6 @@ public class LargeGridPathfindingGame : Game
private Point? fillSelectionCurrent;
private bool batchProcessingScheduled;
private bool startupMenuActive = true;
private readonly bool startupInitializationStarted;
private StartupMenuItem startupSelectedItem;
private int startupGridWidth = 1000;
private int startupGridHeight = 1000;
@@ -216,21 +232,27 @@ public class LargeGridPathfindingGame : Game
case StartupMenuItem.GridWidth:
startupGridWidth = Math.Clamp(startupGridWidth + (direction * 100), 500, 30000);
break;
case StartupMenuItem.GridHeight:
startupGridHeight = Math.Clamp(startupGridHeight + (direction * 100), 500, 30000);
break;
case StartupMenuItem.AgentCount:
startupAgentCount = Math.Clamp(startupAgentCount + (direction * 100), 100, 200000);
break;
case StartupMenuItem.MapPreset:
startupMapPreset = (StartupMapPreset)Math.Clamp((int)startupMapPreset + direction, (int)StartupMapPreset.Rooms, (int)StartupMapPreset.WorstCase);
break;
case StartupMenuItem.ObstacleDivisor:
startupObstacleDivisor = Math.Clamp(startupObstacleDivisor + direction, 1, 50);
break;
case StartupMenuItem.PathRandomization:
startupPathRandomization = direction > 0 || (direction >= 0 && startupPathRandomization);
break;
case StartupMenuItem.PenalizeStretchedRectangles:
startupPenalizeStretchedRectangles = direction > 0 || (direction >= 0 && startupPenalizeStretchedRectangles);
break;
@@ -515,7 +537,7 @@ public class LargeGridPathfindingGame : Game
{
uiSpriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);
if (startupMenuActive && !startupInitializationStarted)
if (startupMenuActive)
{
uiSpriteBatch.DrawString(uiFont, "Startup Configuration", new Vector2(40, 30), Color.Black);
uiSpriteBatch.DrawString(uiFont, "Arrow Up/Down: Select Arrow Left/Right: Change Enter: Start", new Vector2(40, 60), Color.Black);
@@ -748,6 +770,9 @@ public class LargeGridPathfindingGame : Game
base.Draw(gameTime);
}
/// <summary>
/// Builds the initial grid and starts the path update worker using the startup configuration.
/// </summary>
private void StartInitializationTask(int width, int height, int agentCount, StartupMapPreset mapPreset, int obstacleDivisor, bool pathRandomization, bool penalizeStretchedRectangles)
{
_ = Task.Run(() =>
@@ -769,7 +794,10 @@ public class LargeGridPathfindingGame : Game
int obstacleIterations = Math.Max(0, (width + height) / Math.Max(1, obstacleDivisor));
int minObstacleSize = 5;
int minDimension = Math.Min(width, height);
// Using a sub-linear exponent keeps small maps close to previous behavior,
// but prevents obstacle size from exploding on huge maps (e.g. 10k x 10k).
int scaledByDimension = (int)Math.Round(Math.Pow(minDimension, 0.4) * 2.1);
// This upper cap prevents very long walls on thin maps where one dimension is small.
int maxByDimension = Math.Max(minObstacleSize + 1, minDimension / 4);
int maxObstacleSize = Math.Max(minObstacleSize + 1, Math.Min(scaledByDimension, maxByDimension));
@@ -832,6 +860,9 @@ public class LargeGridPathfindingGame : Game
int local = x % pitch;
int baseX = x - local;
bool oddSegment = (baseX / pitch % 2) == 1;
// We intentionally alternate door offsets between left and right side of each room segment.
// If we always place centered doors, many transitions become symmetric and produce fewer
// distinct rectangular splits. Offsetting increases boundary complexity while staying connected.
int offset = ((rowIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // left/right offset
bool isDoor = local == offset;
@@ -862,6 +893,8 @@ public class LargeGridPathfindingGame : Game
int local = y % pitch;
int baseY = y - local;
bool oddSegment = (baseY / pitch % 2) == 1;
// Same idea vertically: alternating top/bottom door offsets avoids repetitive straight corridors
// and keeps the whole lattice navigable with a high number of zone boundaries.
int offset = ((colIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // top/bottom offset
bool isDoor = local == offset;
@@ -912,7 +945,9 @@ public class LargeGridPathfindingGame : Game
});
}
// Handles pathfinding calculations in a separate task
/// <summary>
/// Handles graph/path refresh work on a dedicated long-running task.
/// </summary>
private void StartUpdatePathTask()
{
_ = Task.Factory.StartNew(async () =>
@@ -1100,7 +1135,7 @@ public class LargeGridPathfindingGame : Game
if (resetWeightPoints.Length > 0)
{
affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(resetWeightPoints, 1));
affectedZones.UnionWith(gridFiller.ResetTileWeightsWithAffected(resetWeightPoints));
}
if (placeObstacleRectangles.Length > 0)
+26 -3
View File
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding;
/// <summary>
/// Builds a zone adjacency graph and computes paths across rectangular zones.
/// </summary>
public class Pathfinder
{
private readonly ConcurrentDictionary<int, Rectangle> rectanglesSource;
@@ -19,11 +22,17 @@ public class Pathfinder
private Dictionary<int, Rectangle> rectangleMap;
private Dictionary<int, List<int>> adjacencyList;
/// <summary>
/// Returns the current zone adjacency list.
/// </summary>
public Dictionary<int, List<int>> GetAdjacencyList()
{
return adjacencyList;
}
/// <summary>
/// Returns the zone label at a grid point, or 0 when out of bounds.
/// </summary>
public int GetGridValue(Point gridPoint)
{
if (gridPoint.X < 0 || gridPoint.Y < 0 || gridPoint.X >= grid.GetLength(1) || gridPoint.Y >= grid.GetLength(0))
@@ -34,6 +43,9 @@ public class Pathfinder
return grid[gridPoint.Y, gridPoint.X];
}
/// <summary>
/// Initializes a pathfinder over a mutable grid/rectangle source.
/// </summary>
public Pathfinder(ConcurrentDictionary<int, Rectangle> rectangles, int[,] grid, int[,] weightGrid, bool pathRandomization = false, bool penalizeStretchedRectangles = false)
{
rectanglesSource = rectangles;
@@ -45,6 +57,9 @@ public class Pathfinder
adjacencyList = [];
}
/// <summary>
/// Finds a path between two grid points as coordinate waypoints.
/// </summary>
public List<Vector2>? FindPath(Point startPoint, Point goalPoint)
{
if (!IsInBounds(startPoint.X, startPoint.Y) || !IsInBounds(goalPoint.X, goalPoint.Y))
@@ -115,6 +130,9 @@ public class Pathfinder
return null;
}
/// <summary>
/// Rebuilds the full adjacency graph from current rectangles.
/// </summary>
public void BuildGraph()
{
rectangleMap = new Dictionary<int, Rectangle>(rectanglesSource);
@@ -131,6 +149,8 @@ public class Pathfinder
{
if (!rectangleMap.TryGetValue(rectangleLabel, out Rectangle rectangle))
{
// Returning a very large cost keeps stale nodes from being attractive
// if they slip through due to transient graph inconsistencies.
return int.MaxValue / 4;
}
@@ -247,7 +267,7 @@ public class Pathfinder
graph[id] = [];
}
// For small graphs, use sequential algorithm (less overhead)
// For small graphs, sequential processing is usually faster than parallel setup/synchronization.
if (count < 50)
{
for (int i = 0; i < count; i++)
@@ -273,7 +293,7 @@ public class Pathfinder
}
else
{
// For larger graphs, use parallelization with partitioning
// For larger graphs, parallel pair-checking pays off despite synchronization overhead.
object[] locks = new object[count];
for (int i = 0; i < count; i++)
{
@@ -296,7 +316,7 @@ public class Pathfinder
{
if (AreRectanglesAdjacent(rect1, rect2))
{
// Use per-rectangle locks to minimize contention
// Per-index locks reduce contention compared to one global graph lock.
lock (locks[i])
{
graph[id1].Add(id2);
@@ -314,6 +334,9 @@ public class Pathfinder
return graph;
}
/// <summary>
/// Incrementally refreshes affected parts of the graph after local grid edits.
/// </summary>
public void IncrementalUpdateGraph(HashSet<int> affectedZones)
{
if (affectedZones.Count == 0)
@@ -3,20 +3,32 @@ using System.Collections.Generic;
namespace LargeGridPathfinding;
/// <summary>
/// Tracks named progress entries for long-running operations.
/// </summary>
internal class ProgressTracker
{
private readonly List<ProgressData> progresses = [];
/// <summary>
/// Adds a determinate progress entry.
/// </summary>
public ProgressData AddProgress(string name, out IProgress<float> progress)
{
return AddProgress(name, false, false, out progress);
}
/// <summary>
/// Adds an optionally indeterminate progress entry.
/// </summary>
public ProgressData AddProgress(string name, bool indeterminate, out IProgress<float> progress)
{
return AddProgress(name, indeterminate, false, out progress);
}
/// <summary>
/// Adds a progress entry with full control over removal behavior.
/// </summary>
public ProgressData AddProgress(string name, bool indeterminate, bool disableAutoRemove, out IProgress<float> progress)
{
CustomProgress<float> newProgress = new CustomProgress<float>();
@@ -33,6 +45,9 @@ internal class ProgressTracker
return progressData;
}
/// <summary>
/// Removes a previously added progress entry.
/// </summary>
public void RemoveProgress(ProgressData progressData)
{
lock (progresses)
@@ -41,6 +56,9 @@ internal class ProgressTracker
}
}
/// <summary>
/// Returns active progress entries.
/// </summary>
public IReadOnlyList<ProgressData> GetProgresses()
{
return progresses;
@@ -50,12 +68,16 @@ internal class ProgressTracker
{
progressData.Progress = progress;
// Auto-removal keeps the HUD uncluttered once tasks finish.
if (progress >= 1 && !progressData.DoNotRemove)
{
RemoveProgress(progressData);
}
}
/// <summary>
/// Immutable metadata with mutable progress value for one tracked task.
/// </summary>
public class ProgressData(string name, bool indeterminate, bool doNotRemove)
{
public string Name { get; } = name;
@@ -64,6 +86,9 @@ internal class ProgressTracker
public bool DoNotRemove { get; } = doNotRemove;
}
/// <summary>
/// Minimal IProgress implementation exposing ProgressChanged events.
/// </summary>
public class CustomProgress<T>() : IProgress<T>
{
public EventHandler<T>? ProgressChanged { get; set; }