mirror of
https://github.com/Stone-Red-Code/LargeGridPathfinding.git
synced 2026-09-04 09:06:12 +02:00
Add xml docs
This commit is contained in:
@@ -5,15 +5,33 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace LargeGridPathfinding;
|
namespace LargeGridPathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a moving entity that follows a precomputed grid path.
|
||||||
|
/// </summary>
|
||||||
internal class Agent(Vector2 position)
|
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;
|
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));
|
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; }
|
public Vector2 NextPosition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current target destination.
|
||||||
|
/// </summary>
|
||||||
public Point? Destination { get; set; }
|
public Point? Destination { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the active path.
|
||||||
|
/// </summary>
|
||||||
public List<Vector2>? Path { get; set; }
|
public List<Vector2>? Path { get; set; }
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace LargeGridPathfinding;
|
namespace LargeGridPathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maintains grid labels/weights and generates rectangular walkable zones.
|
||||||
|
/// </summary>
|
||||||
public class GridFiller
|
public class GridFiller
|
||||||
{
|
{
|
||||||
private const int RecalculationRadius = 24;
|
private const int RecalculationRadius = 24;
|
||||||
@@ -21,6 +24,9 @@ public class GridFiller
|
|||||||
public int Width { get; }
|
public int Width { get; }
|
||||||
public int Height { get; }
|
public int Height { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an empty grid with default tile weights.
|
||||||
|
/// </summary>
|
||||||
public GridFiller(int width, int height)
|
public GridFiller(int width, int height)
|
||||||
{
|
{
|
||||||
Width = width;
|
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)
|
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)
|
lock (mutationLock)
|
||||||
@@ -185,16 +194,25 @@ public class GridFiller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places a single obstacle rectangle.
|
||||||
|
/// </summary>
|
||||||
public void PlaceObstacle(Rectangle rectangle)
|
public void PlaceObstacle(Rectangle rectangle)
|
||||||
{
|
{
|
||||||
PlaceObstacles([rectangle]);
|
PlaceObstacles([rectangle]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places obstacle rectangles and optionally recalculates affected zones.
|
||||||
|
/// </summary>
|
||||||
public void PlaceObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
public void PlaceObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
||||||
{
|
{
|
||||||
_ = PlaceObstaclesWithAffected(rectangles, recalculate);
|
_ = PlaceObstaclesWithAffected(rectangles, recalculate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places obstacles and returns changed zone labels.
|
||||||
|
/// </summary>
|
||||||
public HashSet<int> PlaceObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
public HashSet<int> PlaceObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
||||||
{
|
{
|
||||||
lock (mutationLock)
|
lock (mutationLock)
|
||||||
@@ -266,7 +284,7 @@ public class GridFiller
|
|||||||
|
|
||||||
Debug.WriteLine("Placed obstacle");
|
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];
|
HashSet<int> affected = [.. zonesBefore];
|
||||||
affected.SymmetricExceptWith(zonesAfter); // zones removed OR added
|
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)
|
public void RemoveObstacle(Rectangle rectangle)
|
||||||
{
|
{
|
||||||
RemoveObstacles([rectangle]);
|
RemoveObstacles([rectangle]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes obstacle rectangles and optionally recalculates affected zones.
|
||||||
|
/// </summary>
|
||||||
public void RemoveObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
public void RemoveObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
||||||
{
|
{
|
||||||
_ = RemoveObstaclesWithAffected(rectangles, recalculate);
|
_ = RemoveObstaclesWithAffected(rectangles, recalculate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes obstacles and returns changed zone labels.
|
||||||
|
/// </summary>
|
||||||
public HashSet<int> RemoveObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
public HashSet<int> RemoveObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
|
||||||
{
|
{
|
||||||
lock (mutationLock)
|
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)
|
public void SetTileWeight(int x, int y, int weight)
|
||||||
{
|
{
|
||||||
SetTileWeights([new Point(x, y)], weight);
|
SetTileWeights([new Point(x, y)], weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets a single tile weight to default.
|
||||||
|
/// </summary>
|
||||||
public void ResetTileWeight(int x, int y)
|
public void ResetTileWeight(int x, int y)
|
||||||
{
|
{
|
||||||
ResetTileWeights([new Point(x, 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)
|
public void SetTileWeights(IEnumerable<Point> points, int weight, bool recalculate = true)
|
||||||
{
|
{
|
||||||
_ = SetTileWeightsWithAffected(points, weight, recalculate);
|
_ = 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)
|
public HashSet<int> SetTileWeightsWithAffected(IEnumerable<Point> points, int weight, bool recalculate = true)
|
||||||
{
|
{
|
||||||
lock (mutationLock)
|
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)
|
private bool IsAreaFree(int x, int y, int w, int h, int requiredWeight)
|
||||||
@@ -648,6 +695,7 @@ public class GridFiller
|
|||||||
if (removedRectangles.Count == 0)
|
if (removedRectangles.Count == 0)
|
||||||
{
|
{
|
||||||
applyChanges();
|
applyChanges();
|
||||||
|
// Even if no existing zone was removed, edits may have opened space that needs fresh zoning.
|
||||||
FillGrid(left, top, right, bottom);
|
FillGrid(left, top, right, bottom);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -657,6 +705,8 @@ public class GridFiller
|
|||||||
int fillRight = Math.Max(right, removedRectangles.Max(r => r.Right));
|
int fillRight = Math.Max(right, removedRectangles.Max(r => r.Right));
|
||||||
int fillBottom = Math.Max(bottom, removedRectangles.Max(r => r.Bottom));
|
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();
|
applyChanges();
|
||||||
FillGrid(fillLeft, fillTop, fillRight, fillBottom);
|
FillGrid(fillLeft, fillTop, fillRight, fillBottom);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ using MonoGame.Extended.Input;
|
|||||||
|
|
||||||
namespace LargeGridPathfinding;
|
namespace LargeGridPathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input extension helpers for game controls.
|
||||||
|
/// </summary>
|
||||||
internal static class InputHelper
|
internal static class InputHelper
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts WASD keyboard input into a movement vector.
|
||||||
|
/// </summary>
|
||||||
public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState)
|
public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState)
|
||||||
{
|
{
|
||||||
Vector2 movementDirection = Vector2.Zero;
|
Vector2 movementDirection = Vector2.Zero;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace LargeGridPathfinding;
|
namespace LargeGridPathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main game host that handles UI, map editing, rendering, and background path updates.
|
||||||
|
/// </summary>
|
||||||
public class LargeGridPathfindingGame : Game
|
public class LargeGridPathfindingGame : Game
|
||||||
{
|
{
|
||||||
private enum BrushMode
|
private enum BrushMode
|
||||||
@@ -42,10 +45,24 @@ public class LargeGridPathfindingGame : Game
|
|||||||
PenalizeStretchedRectangles
|
PenalizeStretchedRectangles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported map presets for startup generation.
|
||||||
|
/// </summary>
|
||||||
private enum StartupMapPreset
|
private enum StartupMapPreset
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Procedural room-like map with random walls and doors.
|
||||||
|
/// </summary>
|
||||||
Rooms,
|
Rooms,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No generated obstacles.
|
||||||
|
/// </summary>
|
||||||
Empty,
|
Empty,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dense 3x3 room lattice with deterministic offset doors for heavy zone fragmentation.
|
||||||
|
/// </summary>
|
||||||
WorstCase
|
WorstCase
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +95,6 @@ public class LargeGridPathfindingGame : Game
|
|||||||
private Point? fillSelectionCurrent;
|
private Point? fillSelectionCurrent;
|
||||||
private bool batchProcessingScheduled;
|
private bool batchProcessingScheduled;
|
||||||
private bool startupMenuActive = true;
|
private bool startupMenuActive = true;
|
||||||
private readonly bool startupInitializationStarted;
|
|
||||||
private StartupMenuItem startupSelectedItem;
|
private StartupMenuItem startupSelectedItem;
|
||||||
private int startupGridWidth = 1000;
|
private int startupGridWidth = 1000;
|
||||||
private int startupGridHeight = 1000;
|
private int startupGridHeight = 1000;
|
||||||
@@ -216,21 +232,27 @@ public class LargeGridPathfindingGame : Game
|
|||||||
case StartupMenuItem.GridWidth:
|
case StartupMenuItem.GridWidth:
|
||||||
startupGridWidth = Math.Clamp(startupGridWidth + (direction * 100), 500, 30000);
|
startupGridWidth = Math.Clamp(startupGridWidth + (direction * 100), 500, 30000);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StartupMenuItem.GridHeight:
|
case StartupMenuItem.GridHeight:
|
||||||
startupGridHeight = Math.Clamp(startupGridHeight + (direction * 100), 500, 30000);
|
startupGridHeight = Math.Clamp(startupGridHeight + (direction * 100), 500, 30000);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StartupMenuItem.AgentCount:
|
case StartupMenuItem.AgentCount:
|
||||||
startupAgentCount = Math.Clamp(startupAgentCount + (direction * 100), 100, 200000);
|
startupAgentCount = Math.Clamp(startupAgentCount + (direction * 100), 100, 200000);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StartupMenuItem.MapPreset:
|
case StartupMenuItem.MapPreset:
|
||||||
startupMapPreset = (StartupMapPreset)Math.Clamp((int)startupMapPreset + direction, (int)StartupMapPreset.Rooms, (int)StartupMapPreset.WorstCase);
|
startupMapPreset = (StartupMapPreset)Math.Clamp((int)startupMapPreset + direction, (int)StartupMapPreset.Rooms, (int)StartupMapPreset.WorstCase);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StartupMenuItem.ObstacleDivisor:
|
case StartupMenuItem.ObstacleDivisor:
|
||||||
startupObstacleDivisor = Math.Clamp(startupObstacleDivisor + direction, 1, 50);
|
startupObstacleDivisor = Math.Clamp(startupObstacleDivisor + direction, 1, 50);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StartupMenuItem.PathRandomization:
|
case StartupMenuItem.PathRandomization:
|
||||||
startupPathRandomization = direction > 0 || (direction >= 0 && startupPathRandomization);
|
startupPathRandomization = direction > 0 || (direction >= 0 && startupPathRandomization);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case StartupMenuItem.PenalizeStretchedRectangles:
|
case StartupMenuItem.PenalizeStretchedRectangles:
|
||||||
startupPenalizeStretchedRectangles = direction > 0 || (direction >= 0 && startupPenalizeStretchedRectangles);
|
startupPenalizeStretchedRectangles = direction > 0 || (direction >= 0 && startupPenalizeStretchedRectangles);
|
||||||
break;
|
break;
|
||||||
@@ -515,7 +537,7 @@ public class LargeGridPathfindingGame : Game
|
|||||||
{
|
{
|
||||||
uiSpriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);
|
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, "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);
|
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);
|
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)
|
private void StartInitializationTask(int width, int height, int agentCount, StartupMapPreset mapPreset, int obstacleDivisor, bool pathRandomization, bool penalizeStretchedRectangles)
|
||||||
{
|
{
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
@@ -769,7 +794,10 @@ public class LargeGridPathfindingGame : Game
|
|||||||
int obstacleIterations = Math.Max(0, (width + height) / Math.Max(1, obstacleDivisor));
|
int obstacleIterations = Math.Max(0, (width + height) / Math.Max(1, obstacleDivisor));
|
||||||
int minObstacleSize = 5;
|
int minObstacleSize = 5;
|
||||||
int minDimension = Math.Min(width, height);
|
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);
|
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 maxByDimension = Math.Max(minObstacleSize + 1, minDimension / 4);
|
||||||
int maxObstacleSize = Math.Max(minObstacleSize + 1, Math.Min(scaledByDimension, maxByDimension));
|
int maxObstacleSize = Math.Max(minObstacleSize + 1, Math.Min(scaledByDimension, maxByDimension));
|
||||||
|
|
||||||
@@ -832,6 +860,9 @@ public class LargeGridPathfindingGame : Game
|
|||||||
int local = x % pitch;
|
int local = x % pitch;
|
||||||
int baseX = x - local;
|
int baseX = x - local;
|
||||||
bool oddSegment = (baseX / pitch % 2) == 1;
|
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
|
int offset = ((rowIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // left/right offset
|
||||||
bool isDoor = local == offset;
|
bool isDoor = local == offset;
|
||||||
|
|
||||||
@@ -862,6 +893,8 @@ public class LargeGridPathfindingGame : Game
|
|||||||
int local = y % pitch;
|
int local = y % pitch;
|
||||||
int baseY = y - local;
|
int baseY = y - local;
|
||||||
bool oddSegment = (baseY / pitch % 2) == 1;
|
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
|
int offset = ((colIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // top/bottom offset
|
||||||
bool isDoor = local == 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()
|
private void StartUpdatePathTask()
|
||||||
{
|
{
|
||||||
_ = Task.Factory.StartNew(async () =>
|
_ = Task.Factory.StartNew(async () =>
|
||||||
@@ -1100,7 +1135,7 @@ public class LargeGridPathfindingGame : Game
|
|||||||
|
|
||||||
if (resetWeightPoints.Length > 0)
|
if (resetWeightPoints.Length > 0)
|
||||||
{
|
{
|
||||||
affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(resetWeightPoints, 1));
|
affectedZones.UnionWith(gridFiller.ResetTileWeightsWithAffected(resetWeightPoints));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (placeObstacleRectangles.Length > 0)
|
if (placeObstacleRectangles.Length > 0)
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace LargeGridPathfinding;
|
namespace LargeGridPathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a zone adjacency graph and computes paths across rectangular zones.
|
||||||
|
/// </summary>
|
||||||
public class Pathfinder
|
public class Pathfinder
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<int, Rectangle> rectanglesSource;
|
private readonly ConcurrentDictionary<int, Rectangle> rectanglesSource;
|
||||||
@@ -19,11 +22,17 @@ public class Pathfinder
|
|||||||
private Dictionary<int, Rectangle> rectangleMap;
|
private Dictionary<int, Rectangle> rectangleMap;
|
||||||
private Dictionary<int, List<int>> adjacencyList;
|
private Dictionary<int, List<int>> adjacencyList;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the current zone adjacency list.
|
||||||
|
/// </summary>
|
||||||
public Dictionary<int, List<int>> GetAdjacencyList()
|
public Dictionary<int, List<int>> GetAdjacencyList()
|
||||||
{
|
{
|
||||||
return adjacencyList;
|
return adjacencyList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the zone label at a grid point, or 0 when out of bounds.
|
||||||
|
/// </summary>
|
||||||
public int GetGridValue(Point gridPoint)
|
public int GetGridValue(Point gridPoint)
|
||||||
{
|
{
|
||||||
if (gridPoint.X < 0 || gridPoint.Y < 0 || gridPoint.X >= grid.GetLength(1) || gridPoint.Y >= grid.GetLength(0))
|
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];
|
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)
|
public Pathfinder(ConcurrentDictionary<int, Rectangle> rectangles, int[,] grid, int[,] weightGrid, bool pathRandomization = false, bool penalizeStretchedRectangles = false)
|
||||||
{
|
{
|
||||||
rectanglesSource = rectangles;
|
rectanglesSource = rectangles;
|
||||||
@@ -45,6 +57,9 @@ public class Pathfinder
|
|||||||
adjacencyList = [];
|
adjacencyList = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a path between two grid points as coordinate waypoints.
|
||||||
|
/// </summary>
|
||||||
public List<Vector2>? FindPath(Point startPoint, Point goalPoint)
|
public List<Vector2>? FindPath(Point startPoint, Point goalPoint)
|
||||||
{
|
{
|
||||||
if (!IsInBounds(startPoint.X, startPoint.Y) || !IsInBounds(goalPoint.X, goalPoint.Y))
|
if (!IsInBounds(startPoint.X, startPoint.Y) || !IsInBounds(goalPoint.X, goalPoint.Y))
|
||||||
@@ -115,6 +130,9 @@ public class Pathfinder
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rebuilds the full adjacency graph from current rectangles.
|
||||||
|
/// </summary>
|
||||||
public void BuildGraph()
|
public void BuildGraph()
|
||||||
{
|
{
|
||||||
rectangleMap = new Dictionary<int, Rectangle>(rectanglesSource);
|
rectangleMap = new Dictionary<int, Rectangle>(rectanglesSource);
|
||||||
@@ -131,6 +149,8 @@ public class Pathfinder
|
|||||||
{
|
{
|
||||||
if (!rectangleMap.TryGetValue(rectangleLabel, out Rectangle rectangle))
|
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;
|
return int.MaxValue / 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +267,7 @@ public class Pathfinder
|
|||||||
graph[id] = [];
|
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)
|
if (count < 50)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
@@ -273,7 +293,7 @@ public class Pathfinder
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// For larger graphs, use parallelization with partitioning
|
// For larger graphs, parallel pair-checking pays off despite synchronization overhead.
|
||||||
object[] locks = new object[count];
|
object[] locks = new object[count];
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
@@ -296,7 +316,7 @@ public class Pathfinder
|
|||||||
{
|
{
|
||||||
if (AreRectanglesAdjacent(rect1, rect2))
|
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])
|
lock (locks[i])
|
||||||
{
|
{
|
||||||
graph[id1].Add(id2);
|
graph[id1].Add(id2);
|
||||||
@@ -314,6 +334,9 @@ public class Pathfinder
|
|||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Incrementally refreshes affected parts of the graph after local grid edits.
|
||||||
|
/// </summary>
|
||||||
public void IncrementalUpdateGraph(HashSet<int> affectedZones)
|
public void IncrementalUpdateGraph(HashSet<int> affectedZones)
|
||||||
{
|
{
|
||||||
if (affectedZones.Count == 0)
|
if (affectedZones.Count == 0)
|
||||||
|
|||||||
@@ -3,20 +3,32 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace LargeGridPathfinding;
|
namespace LargeGridPathfinding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks named progress entries for long-running operations.
|
||||||
|
/// </summary>
|
||||||
internal class ProgressTracker
|
internal class ProgressTracker
|
||||||
{
|
{
|
||||||
private readonly List<ProgressData> progresses = [];
|
private readonly List<ProgressData> progresses = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a determinate progress entry.
|
||||||
|
/// </summary>
|
||||||
public ProgressData AddProgress(string name, out IProgress<float> progress)
|
public ProgressData AddProgress(string name, out IProgress<float> progress)
|
||||||
{
|
{
|
||||||
return AddProgress(name, false, false, out 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)
|
public ProgressData AddProgress(string name, bool indeterminate, out IProgress<float> progress)
|
||||||
{
|
{
|
||||||
return AddProgress(name, indeterminate, false, out 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)
|
public ProgressData AddProgress(string name, bool indeterminate, bool disableAutoRemove, out IProgress<float> progress)
|
||||||
{
|
{
|
||||||
CustomProgress<float> newProgress = new CustomProgress<float>();
|
CustomProgress<float> newProgress = new CustomProgress<float>();
|
||||||
@@ -33,6 +45,9 @@ internal class ProgressTracker
|
|||||||
return progressData;
|
return progressData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a previously added progress entry.
|
||||||
|
/// </summary>
|
||||||
public void RemoveProgress(ProgressData progressData)
|
public void RemoveProgress(ProgressData progressData)
|
||||||
{
|
{
|
||||||
lock (progresses)
|
lock (progresses)
|
||||||
@@ -41,6 +56,9 @@ internal class ProgressTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns active progress entries.
|
||||||
|
/// </summary>
|
||||||
public IReadOnlyList<ProgressData> GetProgresses()
|
public IReadOnlyList<ProgressData> GetProgresses()
|
||||||
{
|
{
|
||||||
return progresses;
|
return progresses;
|
||||||
@@ -50,12 +68,16 @@ internal class ProgressTracker
|
|||||||
{
|
{
|
||||||
progressData.Progress = progress;
|
progressData.Progress = progress;
|
||||||
|
|
||||||
|
// Auto-removal keeps the HUD uncluttered once tasks finish.
|
||||||
if (progress >= 1 && !progressData.DoNotRemove)
|
if (progress >= 1 && !progressData.DoNotRemove)
|
||||||
{
|
{
|
||||||
RemoveProgress(progressData);
|
RemoveProgress(progressData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable metadata with mutable progress value for one tracked task.
|
||||||
|
/// </summary>
|
||||||
public class ProgressData(string name, bool indeterminate, bool doNotRemove)
|
public class ProgressData(string name, bool indeterminate, bool doNotRemove)
|
||||||
{
|
{
|
||||||
public string Name { get; } = name;
|
public string Name { get; } = name;
|
||||||
@@ -64,6 +86,9 @@ internal class ProgressTracker
|
|||||||
public bool DoNotRemove { get; } = doNotRemove;
|
public bool DoNotRemove { get; } = doNotRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal IProgress implementation exposing ProgressChanged events.
|
||||||
|
/// </summary>
|
||||||
public class CustomProgress<T>() : IProgress<T>
|
public class CustomProgress<T>() : IProgress<T>
|
||||||
{
|
{
|
||||||
public EventHandler<T>? ProgressChanged { get; set; }
|
public EventHandler<T>? ProgressChanged { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user