mirror of
https://github.com/Stone-Red-Code/LargeGridPathfinding.git
synced 2026-09-04 09:06:12 +02:00
Refactor to use ECS
This commit is contained in:
@@ -1,42 +0,0 @@
|
|||||||
using Microsoft.Xna.Framework;
|
|
||||||
|
|
||||||
using System;
|
|
||||||
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; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Index of the current target waypoint within Path.
|
|
||||||
/// </summary>
|
|
||||||
public int PathIndex { get; set; }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Components;
|
||||||
|
|
||||||
|
public class AgentComponent
|
||||||
|
{
|
||||||
|
public Vector2 Position { get; set; }
|
||||||
|
public Vector2 NextPosition { get; set; }
|
||||||
|
public Point? Destination { get; set; }
|
||||||
|
public List<Vector2>? Path { get; set; }
|
||||||
|
public int PathIndex { get; set; }
|
||||||
|
public Point GridPosition => new((int)Position.X, (int)Position.Y);
|
||||||
|
}
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MonoGame.Extended" Version="4.0.4" />
|
<PackageReference Include="MonoGame.Extended" Version="6.0.1" />
|
||||||
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.2.1105" />
|
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.4.1" />
|
||||||
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.2.1105" />
|
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.4.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
|
<Target Name="RestoreDotnetTools" BeforeTargets="Restore">
|
||||||
<Message Text="Restoring dotnet tools" Importance="High" />
|
<Message Text="Restoring dotnet tools" Importance="High" />
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ namespace LargeGridPathfinding;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks named progress entries for long-running operations.
|
/// Tracks named progress entries for long-running operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ProgressTracker
|
public class ProgressTracker
|
||||||
{
|
{
|
||||||
private readonly List<ProgressData> progresses = [];
|
private readonly List<ProgressData> progresses = [];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
using LargeGridPathfinding.Components;
|
||||||
|
using LargeGridPathfinding.Services;
|
||||||
|
using LargeGridPathfinding.Systems;
|
||||||
|
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
using MonoGame.Extended;
|
||||||
|
using MonoGame.Extended.ECS;
|
||||||
|
using MonoGame.Extended.Input;
|
||||||
|
using MonoGame.Extended.Screens;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Screens;
|
||||||
|
|
||||||
|
public class GameplayScreen : GameScreen
|
||||||
|
{
|
||||||
|
private readonly ProgressTracker progressTracker;
|
||||||
|
private readonly int startupGridWidth;
|
||||||
|
private readonly int startupGridHeight;
|
||||||
|
private readonly int startupAgentCount;
|
||||||
|
private readonly int startupObstacleDivisor;
|
||||||
|
private readonly bool startupPathRandomization;
|
||||||
|
private readonly bool startupPenalizeStretchedRectangles;
|
||||||
|
private readonly StartupMenuScreen.StartupMapPreset startupMapPreset;
|
||||||
|
|
||||||
|
private SpriteBatch spriteBatch = null!;
|
||||||
|
private SpriteBatch uiSpriteBatch = null!;
|
||||||
|
private SpriteFont uiFont = null!;
|
||||||
|
private OrthographicCamera camera = null!;
|
||||||
|
private GridService? gridService;
|
||||||
|
private PathfindingService? pathfindingService;
|
||||||
|
private List<AgentComponent> agents = [];
|
||||||
|
private World? world;
|
||||||
|
private GridEditSystem? gridEditSystem;
|
||||||
|
private readonly ConcurrentDictionary<Vector2, Color> temporaryIndicators = [];
|
||||||
|
private bool inputBlocked = true;
|
||||||
|
private bool showZones = true;
|
||||||
|
private bool showGrid;
|
||||||
|
private bool showPaths;
|
||||||
|
private bool initializationComplete;
|
||||||
|
private bool worldBuilt;
|
||||||
|
|
||||||
|
public GameplayScreen(
|
||||||
|
Game game,
|
||||||
|
ProgressTracker progressTracker,
|
||||||
|
int gridWidth,
|
||||||
|
int gridHeight,
|
||||||
|
int agentCount,
|
||||||
|
StartupMenuScreen.StartupMapPreset mapPreset,
|
||||||
|
int obstacleDivisor,
|
||||||
|
bool pathRandomization,
|
||||||
|
bool penalizeStretchedRectangles)
|
||||||
|
: base(game)
|
||||||
|
{
|
||||||
|
this.progressTracker = progressTracker;
|
||||||
|
startupGridWidth = gridWidth;
|
||||||
|
startupGridHeight = gridHeight;
|
||||||
|
startupAgentCount = agentCount;
|
||||||
|
startupMapPreset = mapPreset;
|
||||||
|
startupObstacleDivisor = obstacleDivisor;
|
||||||
|
startupPathRandomization = pathRandomization;
|
||||||
|
startupPenalizeStretchedRectangles = penalizeStretchedRectangles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
camera = new OrthographicCamera(GraphicsDevice)
|
||||||
|
{
|
||||||
|
MinimumZoom = 0.1f,
|
||||||
|
MaximumZoom = 2,
|
||||||
|
Zoom = 0.5f,
|
||||||
|
};
|
||||||
|
|
||||||
|
base.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void LoadContent()
|
||||||
|
{
|
||||||
|
base.LoadContent();
|
||||||
|
spriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
uiSpriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
uiFont = Content.Load<SpriteFont>("ManoloMono");
|
||||||
|
|
||||||
|
StartInitializationTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(GameTime gameTime)
|
||||||
|
{
|
||||||
|
if (!Game.IsActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initializationComplete)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!worldBuilt)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain pending path results from background worker
|
||||||
|
if (pathfindingService is not null)
|
||||||
|
{
|
||||||
|
while (pathfindingService.PendingPathResults.TryDequeue(out (AgentComponent agent, List<Vector2>? path) result))
|
||||||
|
{
|
||||||
|
AgentComponent agent = result.agent;
|
||||||
|
List<Vector2>? path = result.path;
|
||||||
|
agent.Path = path;
|
||||||
|
if (path?.Count > 0)
|
||||||
|
{
|
||||||
|
agent.Position = path[0];
|
||||||
|
agent.NextPosition = path.Count > 1 ? path[1] : path[0];
|
||||||
|
agent.PathIndex = path.Count > 1 ? 1 : 0;
|
||||||
|
agent.Destination = new Point((int)path[^1].X, (int)path[^1].Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process pending grid operations in background
|
||||||
|
ProcessPendingOperations();
|
||||||
|
|
||||||
|
// Camera movement
|
||||||
|
KeyboardExtended.Update();
|
||||||
|
MouseExtended.Update();
|
||||||
|
|
||||||
|
MouseStateExtended mouseState = MouseExtended.GetState();
|
||||||
|
KeyboardStateExtended keyboardState = KeyboardExtended.GetState();
|
||||||
|
|
||||||
|
float movementSpeed = (float)Math.Pow(200, 2 - camera.Zoom);
|
||||||
|
movementSpeed = Math.Clamp(movementSpeed, 1000, 20000);
|
||||||
|
camera.Move(keyboardState.GetMovementDirection() * movementSpeed * gameTime.GetElapsedSeconds());
|
||||||
|
|
||||||
|
if (mouseState.DeltaScrollWheelValue < 0)
|
||||||
|
{
|
||||||
|
camera.ZoomIn(0.1f);
|
||||||
|
}
|
||||||
|
else if (mouseState.DeltaScrollWheelValue > 0)
|
||||||
|
{
|
||||||
|
camera.ZoomOut(0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run ECS world update (calls all registered IUpdateSystems)
|
||||||
|
world!.Update(gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
GraphicsDevice.Clear(Color.CornflowerBlue);
|
||||||
|
|
||||||
|
if (!initializationComplete || gridService is null)
|
||||||
|
{
|
||||||
|
uiSpriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);
|
||||||
|
uiSpriteBatch.DrawString(uiFont, "Initializing simulation...", new Vector2(40, 30), Color.Black);
|
||||||
|
|
||||||
|
IReadOnlyList<ProgressTracker.ProgressData> progresses = progressTracker.GetProgresses();
|
||||||
|
for (int i = 0; i < progresses.Count; i++)
|
||||||
|
{
|
||||||
|
ProgressTracker.ProgressData progressData = progresses[i];
|
||||||
|
string progress = progressData.Indeterminate ? "..." : progressData.Progress.ToString("P0");
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"{progressData.Name}: {progress}", new Vector2(40, 300 + (i * 24)), Color.Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
uiSpriteBatch.End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run ECS world draw (calls all registered IDrawSystems)
|
||||||
|
if (worldBuilt)
|
||||||
|
{
|
||||||
|
world!.Draw(gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw UI overlay
|
||||||
|
uiSpriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);
|
||||||
|
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"FPS: {1 / gameTime.GetElapsedSeconds():0}", new Vector2(10, 10), Color.Black);
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"Grid: {gridService.Width}x{gridService.Height}", new Vector2(10, 30), Color.Black);
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"Zones: {gridService.Filler.PlacedRectangles.Count}", new Vector2(10, 50), Color.Black);
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"Agents: {agents.Count}", new Vector2(10, 70), Color.Black);
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"Brush mode: {gridEditSystem?.CurrentBrushMode ?? GridEditSystem.BrushMode.Obstacle} [O]", new Vector2(10, 90), Color.Black);
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"Weight: {gridEditSystem?.PaintWeight ?? 0} [Keys 1-9, Weight mode]", new Vector2(10, 110), Color.Black);
|
||||||
|
uiSpriteBatch.DrawString(uiFont, "Area fill: hold Shift + left click twice", new Vector2(10, 130), Color.Black);
|
||||||
|
|
||||||
|
IReadOnlyList<ProgressTracker.ProgressData> currentProgresses = progressTracker.GetProgresses();
|
||||||
|
for (int i = 0; i < currentProgresses.Count; i++)
|
||||||
|
{
|
||||||
|
ProgressTracker.ProgressData progressData = currentProgresses[i];
|
||||||
|
string progress = progressData.Indeterminate ? "..." : progressData.Progress.ToString("P0");
|
||||||
|
uiSpriteBatch.DrawString(uiFont, $"{progressData.Name}: {progress}", new Vector2(10, 150 + (i * 20)), Color.Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
uiSpriteBatch.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildECSWorld()
|
||||||
|
{
|
||||||
|
if (gridService is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentMovementSystem agentMovementSystem = new AgentMovementSystem(gridService, camera, agents);
|
||||||
|
|
||||||
|
gridEditSystem = new GridEditSystem(
|
||||||
|
gridService,
|
||||||
|
camera,
|
||||||
|
temporaryIndicators,
|
||||||
|
progressTracker,
|
||||||
|
() => inputBlocked,
|
||||||
|
v => inputBlocked = v,
|
||||||
|
() => showZones,
|
||||||
|
v => showZones = v,
|
||||||
|
() => showGrid,
|
||||||
|
v => showGrid = v,
|
||||||
|
() => showPaths,
|
||||||
|
v => showPaths = v
|
||||||
|
);
|
||||||
|
|
||||||
|
GridRenderSystem gridRenderSystem = new GridRenderSystem(
|
||||||
|
spriteBatch,
|
||||||
|
gridService,
|
||||||
|
camera,
|
||||||
|
temporaryIndicators,
|
||||||
|
() => showZones,
|
||||||
|
() => showGrid,
|
||||||
|
() => gridEditSystem.FillSelectionStart,
|
||||||
|
() => gridEditSystem.FillSelectionCurrent
|
||||||
|
);
|
||||||
|
|
||||||
|
PathRenderSystem pathRenderSystem = new PathRenderSystem(
|
||||||
|
spriteBatch,
|
||||||
|
camera,
|
||||||
|
() => showPaths,
|
||||||
|
() => agentMovementSystem.SectorsX,
|
||||||
|
() => agentMovementSystem.SectorsY,
|
||||||
|
() => agentMovementSystem.AgentSectors
|
||||||
|
);
|
||||||
|
|
||||||
|
AgentRenderSystem agentRenderSystem = new AgentRenderSystem(
|
||||||
|
spriteBatch,
|
||||||
|
camera,
|
||||||
|
() => agentMovementSystem.SectorsX,
|
||||||
|
() => agentMovementSystem.SectorsY,
|
||||||
|
() => agentMovementSystem.AgentSectors
|
||||||
|
);
|
||||||
|
|
||||||
|
world = new WorldBuilder()
|
||||||
|
.AddSystem(agentMovementSystem)
|
||||||
|
.AddSystem(gridEditSystem)
|
||||||
|
.AddSystem(gridRenderSystem)
|
||||||
|
.AddSystem(pathRenderSystem)
|
||||||
|
.AddSystem(agentRenderSystem)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Create ECS entities for each agent
|
||||||
|
foreach (AgentComponent agent in agents)
|
||||||
|
{
|
||||||
|
world.CreateEntity().Attach(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
worldBuilt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartInitializationTask()
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
List<AgentComponent> agentList = new(startupAgentCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < startupAgentCount; i++)
|
||||||
|
{
|
||||||
|
int x = Random.Shared.Next(0, startupGridWidth - 1);
|
||||||
|
int y = Random.Shared.Next(0, startupGridHeight - 1);
|
||||||
|
agentList.Add(new AgentComponent { Position = new Vector2(x, y) });
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Rectangle> obstacles = [];
|
||||||
|
|
||||||
|
if (startupMapPreset == StartupMenuScreen.StartupMapPreset.Rooms)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Generating random obstacles...");
|
||||||
|
|
||||||
|
int obstacleIterations = Math.Max(0, (startupGridWidth + startupGridHeight) / Math.Max(1, startupObstacleDivisor));
|
||||||
|
int minObstacleSize = 5;
|
||||||
|
int minDimension = Math.Min(startupGridWidth, startupGridHeight);
|
||||||
|
int scaledByDimension = (int)Math.Round(Math.Pow(minDimension, 0.4) * 2.1);
|
||||||
|
int maxByDimension = Math.Max(minObstacleSize + 1, minDimension / 4);
|
||||||
|
int maxObstacleSize = Math.Max(minObstacleSize + 1, Math.Min(scaledByDimension, maxByDimension));
|
||||||
|
|
||||||
|
for (int i = 0; i < obstacleIterations; i++)
|
||||||
|
{
|
||||||
|
int x = Random.Shared.Next(0, startupGridWidth - 1);
|
||||||
|
int y = Random.Shared.Next(0, startupGridHeight - 1);
|
||||||
|
int w = Random.Shared.Next(minObstacleSize, maxObstacleSize);
|
||||||
|
int h = Random.Shared.Next(minObstacleSize, maxObstacleSize);
|
||||||
|
|
||||||
|
Rectangle wall1 = new(x, y, w, 1);
|
||||||
|
Rectangle wall2 = new(x, y, 1, h);
|
||||||
|
Rectangle wall3 = new(x + w - 1, y, 1, h);
|
||||||
|
Rectangle wall4 = new(x, y + h - 1, w, 1);
|
||||||
|
|
||||||
|
int doorSide = Random.Shared.Next(4);
|
||||||
|
if (doorSide == 0)
|
||||||
|
{
|
||||||
|
obstacles.Add(wall1); obstacles.Add(wall2); obstacles.Add(wall3);
|
||||||
|
int doorX = x + Random.Shared.Next(1, w - 2);
|
||||||
|
obstacles.Add(new Rectangle(x, y + h - 1, doorX - x, 1));
|
||||||
|
obstacles.Add(new Rectangle(doorX + 2, y + h - 1, x + w - (doorX + 2), 1));
|
||||||
|
}
|
||||||
|
else if (doorSide == 1)
|
||||||
|
{
|
||||||
|
obstacles.Add(wall2); obstacles.Add(wall3); obstacles.Add(wall4);
|
||||||
|
int doorX = x + Random.Shared.Next(1, w - 2);
|
||||||
|
obstacles.Add(new Rectangle(x, y, doorX - x, 1));
|
||||||
|
obstacles.Add(new Rectangle(doorX + 2, y, x + w - (doorX + 2), 1));
|
||||||
|
}
|
||||||
|
else if (doorSide == 2)
|
||||||
|
{
|
||||||
|
obstacles.Add(wall1); obstacles.Add(wall3); obstacles.Add(wall4);
|
||||||
|
int doorY = y + Random.Shared.Next(1, h - 2);
|
||||||
|
obstacles.Add(new Rectangle(x, y, 1, doorY - y));
|
||||||
|
obstacles.Add(new Rectangle(x, doorY + 2, 1, y + h - (doorY + 2)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
obstacles.Add(wall1); obstacles.Add(wall2); obstacles.Add(wall4);
|
||||||
|
int doorY = y + Random.Shared.Next(1, h - 2);
|
||||||
|
obstacles.Add(new Rectangle(x + w - 1, y, 1, doorY - y));
|
||||||
|
obstacles.Add(new Rectangle(x + w - 1, doorY + 2, 1, y + h - (doorY + 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (startupMapPreset == StartupMenuScreen.StartupMapPreset.WorstCase)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Generating connected worst-case 3x3 rooms with offset doors...");
|
||||||
|
const int roomSize = 3;
|
||||||
|
const int pitch = roomSize + 1;
|
||||||
|
|
||||||
|
for (int y = pitch; y < startupGridHeight; y += pitch)
|
||||||
|
{
|
||||||
|
int runStart = 0;
|
||||||
|
int rowIndex = y / pitch;
|
||||||
|
|
||||||
|
for (int x = 0; x < startupGridWidth; x++)
|
||||||
|
{
|
||||||
|
int local = x % pitch;
|
||||||
|
int baseX = x - local;
|
||||||
|
bool oddSegment = (baseX / pitch % 2) == 1;
|
||||||
|
int offset = ((rowIndex % 2 == 0) ^ oddSegment) ? 1 : 3;
|
||||||
|
bool isDoor = local == offset;
|
||||||
|
|
||||||
|
if (isDoor)
|
||||||
|
{
|
||||||
|
if (x > runStart)
|
||||||
|
{
|
||||||
|
obstacles.Add(new Rectangle(runStart, y, x - runStart, 1));
|
||||||
|
}
|
||||||
|
runStart = x + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runStart < startupGridWidth)
|
||||||
|
{
|
||||||
|
obstacles.Add(new Rectangle(runStart, y, startupGridWidth - runStart, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int x = pitch; x < startupGridWidth; x += pitch)
|
||||||
|
{
|
||||||
|
int runStart = 0;
|
||||||
|
int colIndex = x / pitch;
|
||||||
|
|
||||||
|
for (int y = 0; y < startupGridHeight; y++)
|
||||||
|
{
|
||||||
|
int local = y % pitch;
|
||||||
|
int baseY = y - local;
|
||||||
|
bool oddSegment = (baseY / pitch % 2) == 1;
|
||||||
|
int offset = ((colIndex % 2 == 0) ^ oddSegment) ? 1 : 3;
|
||||||
|
bool isDoor = local == offset;
|
||||||
|
|
||||||
|
if (isDoor)
|
||||||
|
{
|
||||||
|
if (y > runStart)
|
||||||
|
{
|
||||||
|
obstacles.Add(new Rectangle(x, runStart, 1, y - runStart));
|
||||||
|
}
|
||||||
|
runStart = y + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runStart < startupGridHeight)
|
||||||
|
{
|
||||||
|
obstacles.Add(new Rectangle(x, runStart, 1, startupGridHeight - runStart));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Using empty map preset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine("Initializing grid...");
|
||||||
|
|
||||||
|
ProgressTracker.ProgressData progressData = progressTracker.AddProgress("Initializing grid", true, out _);
|
||||||
|
GridFiller gf = new GridFiller(startupGridWidth, startupGridHeight);
|
||||||
|
gf.PlaceObstacles(obstacles, false);
|
||||||
|
progressTracker.RemoveProgress(progressData);
|
||||||
|
|
||||||
|
Debug.WriteLine("Filling grid...");
|
||||||
|
|
||||||
|
_ = progressTracker.AddProgress("Filling grid", out IProgress<float> fillingGridProgressReporter);
|
||||||
|
_ = progressTracker.AddProgress("Calculating candidates", out IProgress<float> calculatingCandidatesProgressReporter);
|
||||||
|
_ = progressTracker.AddProgress("Placing zones", out IProgress<float> placingCandidatesProgressReporter);
|
||||||
|
|
||||||
|
gf.FillGrid(totalProgress: fillingGridProgressReporter, calculatingCandidatesProgress: calculatingCandidatesProgressReporter, placingCandidatesProgress: placingCandidatesProgressReporter);
|
||||||
|
|
||||||
|
GridService gs = new GridService(gf);
|
||||||
|
Pathfinder pf = new Pathfinder(gf.PlacedRectangles, gf.Grid, gf.WeightGrid, startupPathRandomization, startupPenalizeStretchedRectangles);
|
||||||
|
PathfindingService pfs = new PathfindingService(pf, gs, agentList, progressTracker);
|
||||||
|
|
||||||
|
gridService = gs;
|
||||||
|
pathfindingService = pfs;
|
||||||
|
agents = agentList;
|
||||||
|
gs.GridChanged = true;
|
||||||
|
inputBlocked = false;
|
||||||
|
showZones = false;
|
||||||
|
showPaths = false;
|
||||||
|
|
||||||
|
initializationComplete = true;
|
||||||
|
|
||||||
|
// Build the ECS world (must be on main thread — deferred to next Update)
|
||||||
|
BuildECSWorld();
|
||||||
|
|
||||||
|
pfs.Start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessPendingOperations()
|
||||||
|
{
|
||||||
|
if (gridService is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gridService.TryDequeueBatch(out GridService.PendingOperationEntry[]? batch))
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
gridService.ProcessOperationBatch(batch);
|
||||||
|
|
||||||
|
foreach (GridService.PendingOperationEntry entry in batch)
|
||||||
|
{
|
||||||
|
_ = temporaryIndicators.TryRemove(entry.Point.ToVector2(), out _);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
|
||||||
|
using MonoGame.Extended.Input;
|
||||||
|
using MonoGame.Extended.Screens;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Screens;
|
||||||
|
|
||||||
|
public class StartupMenuScreen : GameScreen
|
||||||
|
{
|
||||||
|
private enum StartupMenuItem
|
||||||
|
{
|
||||||
|
GridWidth,
|
||||||
|
GridHeight,
|
||||||
|
AgentCount,
|
||||||
|
MapPreset,
|
||||||
|
ObstacleDivisor,
|
||||||
|
PathRandomization,
|
||||||
|
PenalizeStretchedRectangles
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StartupMapPreset
|
||||||
|
{
|
||||||
|
Rooms,
|
||||||
|
Empty,
|
||||||
|
WorstCase
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpriteFont uiFont = null!;
|
||||||
|
private readonly ProgressTracker progressTracker = new();
|
||||||
|
private StartupMenuItem startupSelectedItem;
|
||||||
|
private int startupGridWidth = 1000;
|
||||||
|
private int startupGridHeight = 1000;
|
||||||
|
private int startupAgentCount = 1000;
|
||||||
|
private StartupMapPreset startupMapPreset = StartupMapPreset.Rooms;
|
||||||
|
private int startupObstacleDivisor = 2;
|
||||||
|
private bool startupPathRandomization;
|
||||||
|
private bool startupPenalizeStretchedRectangles;
|
||||||
|
private bool initialized;
|
||||||
|
|
||||||
|
public int GridWidth => startupGridWidth;
|
||||||
|
public int GridHeight => startupGridHeight;
|
||||||
|
public int AgentCount => startupAgentCount;
|
||||||
|
public int ObstacleDivisor => startupObstacleDivisor;
|
||||||
|
public bool PathRandomization => startupPathRandomization;
|
||||||
|
public bool PenalizeStretchedRectangles => startupPenalizeStretchedRectangles;
|
||||||
|
public ProgressTracker ProgressTracker => progressTracker;
|
||||||
|
public bool Initialized => initialized;
|
||||||
|
|
||||||
|
public StartupMapPreset MapPreset => startupMapPreset;
|
||||||
|
|
||||||
|
public StartupMenuScreen(Game game) : base(game)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void LoadContent()
|
||||||
|
{
|
||||||
|
base.LoadContent();
|
||||||
|
uiFont = Content.Load<SpriteFont>("ManoloMono");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(GameTime gameTime)
|
||||||
|
{
|
||||||
|
KeyboardExtended.Update();
|
||||||
|
|
||||||
|
KeyboardStateExtended keyboardState = KeyboardExtended.GetState();
|
||||||
|
bool shiftPressed = keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift);
|
||||||
|
bool ctrlPressed = keyboardState.IsKeyDown(Keys.LeftControl) || keyboardState.IsKeyDown(Keys.RightControl);
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.Up))
|
||||||
|
{
|
||||||
|
startupSelectedItem = (StartupMenuItem)Math.Max(0, (int)startupSelectedItem - 1);
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.Down))
|
||||||
|
{
|
||||||
|
startupSelectedItem = (StartupMenuItem)Math.Min((int)StartupMenuItem.PenalizeStretchedRectangles, (int)startupSelectedItem + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int direction = 0;
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.Left))
|
||||||
|
{
|
||||||
|
direction = shiftPressed ? ctrlPressed ? -100 : -10 : -1;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.Right))
|
||||||
|
{
|
||||||
|
direction = shiftPressed ? ctrlPressed ? 100 : 10 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction != 0)
|
||||||
|
{
|
||||||
|
switch (startupSelectedItem)
|
||||||
|
{
|
||||||
|
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 * 1000), 100, 1000000);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.Enter))
|
||||||
|
{
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
GraphicsDevice.Clear(Color.CornflowerBlue);
|
||||||
|
|
||||||
|
var spriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);
|
||||||
|
|
||||||
|
spriteBatch.DrawString(uiFont, "Startup Configuration", new Vector2(40, 30), Color.Black);
|
||||||
|
spriteBatch.DrawString(uiFont, "Arrow Up/Down: Select Arrow Left/Right: Change Enter: Start", new Vector2(40, 60), Color.Black);
|
||||||
|
|
||||||
|
string[] lines =
|
||||||
|
[
|
||||||
|
$"Grid Width: {startupGridWidth}",
|
||||||
|
$"Grid Height: {startupGridHeight}",
|
||||||
|
$"Agent Count: {startupAgentCount}",
|
||||||
|
$"Map Preset: {startupMapPreset}",
|
||||||
|
$"Obstacle Divisor: {startupObstacleDivisor} (lower = more obstacles)",
|
||||||
|
$"Path Randomization: {startupPathRandomization}",
|
||||||
|
$"Penalize Stretched Rectangles: {startupPenalizeStretchedRectangles}"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
Color color = i == (int)startupSelectedItem ? Color.DarkBlue : Color.Black;
|
||||||
|
string prefix = i == (int)startupSelectedItem ? "# " : " ";
|
||||||
|
spriteBatch.DrawString(uiFont, $"{prefix}{lines[i]}", new Vector2(40, 100 + (i * 28)), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<ProgressTracker.ProgressData> startupProgresses = progressTracker.GetProgresses();
|
||||||
|
for (int i = 0; i < startupProgresses.Count; i++)
|
||||||
|
{
|
||||||
|
ProgressTracker.ProgressData progressData = startupProgresses[i];
|
||||||
|
string progress = progressData.Indeterminate ? "..." : progressData.Progress.ToString("P0");
|
||||||
|
spriteBatch.DrawString(uiFont, $"{progressData.Name}: {progress}", new Vector2(40, 300 + (i * 24)), Color.Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteBatch.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Services;
|
||||||
|
|
||||||
|
public class GridService
|
||||||
|
{
|
||||||
|
private readonly GridFiller gridFiller;
|
||||||
|
private readonly object pendingOperationsLock = new();
|
||||||
|
private readonly Dictionary<Point, PendingOperation> pendingOperations = [];
|
||||||
|
private readonly HashSet<int> affectedZones = [];
|
||||||
|
private readonly object affectedZonesLock = new();
|
||||||
|
private bool batchProcessingScheduled;
|
||||||
|
private bool gridChanged;
|
||||||
|
|
||||||
|
public GridFiller Filler => gridFiller;
|
||||||
|
public int[,] Grid => gridFiller.Grid;
|
||||||
|
public int[,] WeightGrid => gridFiller.WeightGrid;
|
||||||
|
public int Width => gridFiller.Width;
|
||||||
|
public int Height => gridFiller.Height;
|
||||||
|
public bool GridChanged
|
||||||
|
{
|
||||||
|
get => gridChanged;
|
||||||
|
set => gridChanged = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<int> AffectedZones => affectedZones;
|
||||||
|
|
||||||
|
public GridService(GridFiller gridFiller)
|
||||||
|
{
|
||||||
|
this.gridFiller = gridFiller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueOperations(IEnumerable<Point> points, PendingOperation operation)
|
||||||
|
{
|
||||||
|
lock (pendingOperationsLock)
|
||||||
|
{
|
||||||
|
foreach (Point point in points)
|
||||||
|
{
|
||||||
|
pendingOperations[point] = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchProcessingScheduled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
batchProcessingScheduled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryDequeueBatch(out PendingOperationEntry[] batch)
|
||||||
|
{
|
||||||
|
lock (pendingOperationsLock)
|
||||||
|
{
|
||||||
|
if (pendingOperations.Count == 0)
|
||||||
|
{
|
||||||
|
batchProcessingScheduled = false;
|
||||||
|
batch = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch = [.. pendingOperations.Select(kvp => new PendingOperationEntry(kvp.Key, kvp.Value))];
|
||||||
|
pendingOperations.Clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessOperationBatch(PendingOperationEntry[] batch)
|
||||||
|
{
|
||||||
|
IGrouping<int, Point>[] weightGroups = [.. batch
|
||||||
|
.Where(op => op.Operation.Kind == PendingOperationKind.SetWeight)
|
||||||
|
.GroupBy(op => op.Operation.Weight, op => op.Point)];
|
||||||
|
|
||||||
|
Point[] resetWeightPoints = [.. batch
|
||||||
|
.Where(op => op.Operation.Kind == PendingOperationKind.ResetWeight)
|
||||||
|
.Select(op => op.Point)];
|
||||||
|
|
||||||
|
Rectangle[] placeObstacleRectangles = [.. batch
|
||||||
|
.Where(op => op.Operation.Kind == PendingOperationKind.PlaceObstacle)
|
||||||
|
.Select(op => new Rectangle(op.Point.X, op.Point.Y, 1, 1))];
|
||||||
|
|
||||||
|
Rectangle[] removeObstacleRectangles = [.. batch
|
||||||
|
.Where(op => op.Operation.Kind == PendingOperationKind.RemoveObstacle)
|
||||||
|
.Select(op => new Rectangle(op.Point.X, op.Point.Y, 1, 1))];
|
||||||
|
|
||||||
|
lock (affectedZonesLock)
|
||||||
|
{
|
||||||
|
foreach (IGrouping<int, Point> weightGroup in weightGroups)
|
||||||
|
{
|
||||||
|
affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(weightGroup, weightGroup.Key));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetWeightPoints.Length > 0)
|
||||||
|
{
|
||||||
|
affectedZones.UnionWith(gridFiller.ResetTileWeightsWithAffected(resetWeightPoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeObstacleRectangles.Length > 0)
|
||||||
|
{
|
||||||
|
affectedZones.UnionWith(gridFiller.PlaceObstaclesWithAffected(placeObstacleRectangles));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeObstacleRectangles.Length > 0)
|
||||||
|
{
|
||||||
|
affectedZones.UnionWith(gridFiller.RemoveObstaclesWithAffected(removeObstacleRectangles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gridChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<int> TakeAffectedZones()
|
||||||
|
{
|
||||||
|
lock (affectedZonesLock)
|
||||||
|
{
|
||||||
|
if (affectedZones.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<int> result = [.. affectedZones];
|
||||||
|
affectedZones.Clear();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct PendingOperation(PendingOperationKind Kind, int Weight);
|
||||||
|
|
||||||
|
public readonly record struct PendingOperationEntry(Point Point, PendingOperation Operation);
|
||||||
|
|
||||||
|
public enum PendingOperationKind
|
||||||
|
{
|
||||||
|
SetWeight,
|
||||||
|
ResetWeight,
|
||||||
|
PlaceObstacle,
|
||||||
|
RemoveObstacle
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using LargeGridPathfinding.Components;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Services;
|
||||||
|
|
||||||
|
public class PathfindingService
|
||||||
|
{
|
||||||
|
private readonly Pathfinder pathfinder;
|
||||||
|
private readonly GridService gridService;
|
||||||
|
private readonly List<AgentComponent> agents;
|
||||||
|
private readonly ConcurrentQueue<(AgentComponent agent, List<Vector2>? path)> pendingPathResults = new();
|
||||||
|
private readonly ProgressTracker progressTracker;
|
||||||
|
private CancellationTokenSource? cts;
|
||||||
|
private Task? workerTask;
|
||||||
|
|
||||||
|
public ConcurrentQueue<(AgentComponent agent, List<Vector2>? path)> PendingPathResults => pendingPathResults;
|
||||||
|
public Pathfinder Pathfinder => pathfinder;
|
||||||
|
|
||||||
|
public PathfindingService(Pathfinder pathfinder, GridService gridService, List<AgentComponent> agents, ProgressTracker progressTracker)
|
||||||
|
{
|
||||||
|
this.pathfinder = pathfinder;
|
||||||
|
this.gridService = gridService;
|
||||||
|
this.agents = agents;
|
||||||
|
this.progressTracker = progressTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
workerTask = Task.Factory.StartNew(() => WorkerLoop(cts.Token), TaskCreationOptions.LongRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
cts?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Vector2>? CalculatePath(Point? start = null, Point? goal = null, int maxNodes = int.MaxValue)
|
||||||
|
{
|
||||||
|
int width = gridService.Width;
|
||||||
|
int height = gridService.Height;
|
||||||
|
int[,] g = gridService.Grid;
|
||||||
|
|
||||||
|
if (start is null)
|
||||||
|
{
|
||||||
|
start = RandomPoint(width, height, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goal is null)
|
||||||
|
{
|
||||||
|
goal = RandomPoint(width, height, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathfinder.FindPath(start.Value, goal.Value, maxNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Point RandomPoint(int width, int height, int[,] grid)
|
||||||
|
{
|
||||||
|
Point p;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
p = new Point(Random.Shared.Next(0, width - 1), Random.Shared.Next(0, height - 1));
|
||||||
|
}
|
||||||
|
while (grid[p.Y, p.X] <= 0);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WorkerLoop(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HashSet<int> localAffectedZones = gridService.TakeAffectedZones();
|
||||||
|
|
||||||
|
if (localAffectedZones.Count > 0)
|
||||||
|
{
|
||||||
|
gridService.GridChanged = false;
|
||||||
|
ProgressTracker.ProgressData progressDataUpdateGraph = progressTracker.AddProgress("Updating graph", true, out _);
|
||||||
|
pathfinder.IncrementalUpdateGraph(localAffectedZones);
|
||||||
|
progressTracker.RemoveProgress(progressDataUpdateGraph);
|
||||||
|
}
|
||||||
|
else if (gridService.GridChanged)
|
||||||
|
{
|
||||||
|
gridService.GridChanged = false;
|
||||||
|
ProgressTracker.ProgressData progressDataBuildGraph = progressTracker.AddProgress("Building graph", true, out _);
|
||||||
|
pathfinder.BuildGraph();
|
||||||
|
progressTracker.RemoveProgress(progressDataBuildGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AgentComponent> agentsRequirePathList = new(agents.Count);
|
||||||
|
|
||||||
|
foreach (AgentComponent agent in agents)
|
||||||
|
{
|
||||||
|
if (agent.Path is null)
|
||||||
|
{
|
||||||
|
agentsRequirePathList.Add(agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentsRequirePathList.Count != 0)
|
||||||
|
{
|
||||||
|
int agentCount = agentsRequirePathList.Count;
|
||||||
|
int batchSize = Math.Max(64, agentCount / (Environment.ProcessorCount * 4));
|
||||||
|
|
||||||
|
ProgressTracker.ProgressData progressDataPaths = progressTracker.AddProgress($"Calculating {agentCount} paths", out IProgress<float> progress);
|
||||||
|
|
||||||
|
ParallelOptions parallelOptions = new()
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = Environment.ProcessorCount
|
||||||
|
};
|
||||||
|
|
||||||
|
long lastProgressReport = Environment.TickCount64;
|
||||||
|
int pathsCalculated = 0;
|
||||||
|
|
||||||
|
_ = Parallel.ForEach(Partitioner.Create(0, agentCount, batchSize), parallelOptions, range =>
|
||||||
|
{
|
||||||
|
for (int i = range.Item1; i < range.Item2; i++)
|
||||||
|
{
|
||||||
|
AgentComponent agent = agentsRequirePathList[i];
|
||||||
|
List<Vector2>? path = CalculatePath(agent.GridPosition, agent.Destination, maxNodes: int.MaxValue) ?? CalculatePath(maxNodes: int.MaxValue);
|
||||||
|
pendingPathResults.Enqueue((agent, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
int local = Interlocked.Add(ref pathsCalculated, range.Item2 - range.Item1);
|
||||||
|
long now = Environment.TickCount64;
|
||||||
|
if (now - lastProgressReport > 100)
|
||||||
|
{
|
||||||
|
progress.Report((float)local / agentCount);
|
||||||
|
_ = Interlocked.Exchange(ref lastProgressReport, now);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
progress.Report(1.0f);
|
||||||
|
progressTracker.RemoveProgress(progressDataPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentsRequirePathList.Count < 1000)
|
||||||
|
{
|
||||||
|
Task.Delay(100).Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.WriteLine(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
using MonoGame.Extended;
|
||||||
|
using MonoGame.Extended.ECS;
|
||||||
|
using MonoGame.Extended.ECS.Systems;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using LargeGridPathfinding.Components;
|
||||||
|
using LargeGridPathfinding.Services;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Systems;
|
||||||
|
|
||||||
|
public class AgentMovementSystem : EntityUpdateSystem
|
||||||
|
{
|
||||||
|
private const int SectorShift = 7;
|
||||||
|
private const int SectorSize = 1 << SectorShift;
|
||||||
|
|
||||||
|
private readonly GridService gridService;
|
||||||
|
private readonly OrthographicCamera camera;
|
||||||
|
private readonly List<AgentComponent> agentList;
|
||||||
|
private ComponentMapper<AgentComponent> agentMapper = null!;
|
||||||
|
private int sectorsX;
|
||||||
|
private int sectorsY;
|
||||||
|
private List<AgentComponent>[,] agentSectors = null!;
|
||||||
|
private int frameCount;
|
||||||
|
|
||||||
|
public int SectorsX => sectorsX;
|
||||||
|
public int SectorsY => sectorsY;
|
||||||
|
public List<AgentComponent>[,] AgentSectors => agentSectors;
|
||||||
|
|
||||||
|
public AgentMovementSystem(GridService gridService, OrthographicCamera camera, List<AgentComponent> agentList)
|
||||||
|
: base(Aspect.All(typeof(AgentComponent)))
|
||||||
|
{
|
||||||
|
this.gridService = gridService;
|
||||||
|
this.camera = camera;
|
||||||
|
this.agentList = agentList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(IComponentMapperService mapperService)
|
||||||
|
{
|
||||||
|
agentMapper = mapperService.GetMapper<AgentComponent>();
|
||||||
|
RebuildSectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(GameTime gameTime)
|
||||||
|
{
|
||||||
|
int[,] grid = gridService.Grid;
|
||||||
|
float dt = gameTime.GetElapsedSeconds();
|
||||||
|
float step = 10f * dt;
|
||||||
|
float sqrStep = step * step;
|
||||||
|
|
||||||
|
frameCount++;
|
||||||
|
|
||||||
|
if ((frameCount & 127) == 0)
|
||||||
|
{
|
||||||
|
RebuildSectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
RectangleF viewBounds = camera.BoundingRectangle;
|
||||||
|
float margin = 5f;
|
||||||
|
float cellViewLeft = (viewBounds.Left / 10f) - margin;
|
||||||
|
float cellViewRight = (viewBounds.Right / 10f) + margin;
|
||||||
|
float cellViewTop = (viewBounds.Top / 10f) - margin;
|
||||||
|
float cellViewBottom = (viewBounds.Bottom / 10f) + margin;
|
||||||
|
|
||||||
|
int minSX = Math.Max(0, (int)(cellViewLeft / SectorSize) - 1);
|
||||||
|
int maxSX = Math.Min(sectorsX - 1, (int)(cellViewRight / SectorSize) + 1);
|
||||||
|
int minSY = Math.Max(0, (int)(cellViewTop / SectorSize) - 1);
|
||||||
|
int maxSY = Math.Min(sectorsY - 1, (int)(cellViewBottom / SectorSize) + 1);
|
||||||
|
|
||||||
|
for (int sx = minSX; sx <= maxSX; sx++)
|
||||||
|
{
|
||||||
|
for (int sy = minSY; sy <= maxSY; sy++)
|
||||||
|
{
|
||||||
|
List<AgentComponent>? sector = agentSectors[sx, sy];
|
||||||
|
if (sector is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (AgentComponent agent in sector)
|
||||||
|
{
|
||||||
|
List<Vector2>? path = agent.Path;
|
||||||
|
if (path is null || agent.PathIndex >= path.Count)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 currentPos = agent.Position;
|
||||||
|
bool inView = currentPos.X >= cellViewLeft && currentPos.X <= cellViewRight
|
||||||
|
&& currentPos.Y >= cellViewTop && currentPos.Y <= cellViewBottom;
|
||||||
|
|
||||||
|
Vector2 targetPos = agent.NextPosition;
|
||||||
|
float dx = targetPos.X - currentPos.X;
|
||||||
|
float dy = targetPos.Y - currentPos.Y;
|
||||||
|
float sqrDist = (dx * dx) + (dy * dy);
|
||||||
|
|
||||||
|
if (sqrDist < 0.01f)
|
||||||
|
{
|
||||||
|
if (agent.PathIndex == path.Count - 1)
|
||||||
|
{
|
||||||
|
agent.Path = null;
|
||||||
|
agent.Destination = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
agent.NextPosition = path[++agent.PathIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Vector2 nextPosition;
|
||||||
|
if (sqrDist <= sqrStep)
|
||||||
|
{
|
||||||
|
nextPosition = targetPos;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float t = step / MathF.Sqrt(sqrDist);
|
||||||
|
nextPosition = new Vector2(currentPos.X + (dx * t), currentPos.Y + (dy * t));
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.Position = nextPosition;
|
||||||
|
|
||||||
|
if (inView)
|
||||||
|
{
|
||||||
|
int newX = (int)(nextPosition.X + 0.5f);
|
||||||
|
int newY = (int)(nextPosition.Y + 0.5f);
|
||||||
|
|
||||||
|
if (grid[newY, newX] < 0)
|
||||||
|
{
|
||||||
|
agent.Position = currentPos;
|
||||||
|
agent.Path = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalAgents = agentList.Count;
|
||||||
|
int offScreenBudget = Math.Max(1000, totalAgents / 1000);
|
||||||
|
float catchUpStep = step * (totalAgents / (float)offScreenBudget);
|
||||||
|
int startIndex = frameCount * offScreenBudget % totalAgents;
|
||||||
|
|
||||||
|
for (int i = 0; i < offScreenBudget; i++)
|
||||||
|
{
|
||||||
|
AgentComponent a = agentList[(startIndex + i) % totalAgents];
|
||||||
|
|
||||||
|
List<Vector2>? p = a.Path;
|
||||||
|
if (p is null || a.PathIndex >= p.Count)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 pos = a.Position;
|
||||||
|
if (pos.X >= cellViewLeft && pos.X <= cellViewRight
|
||||||
|
&& pos.Y >= cellViewTop && pos.Y <= cellViewBottom)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float remaining = catchUpStep;
|
||||||
|
|
||||||
|
while (remaining > 0.01f)
|
||||||
|
{
|
||||||
|
Vector2 currentPos = a.Position;
|
||||||
|
Vector2 targetPos = a.NextPosition;
|
||||||
|
float ddx = targetPos.X - currentPos.X;
|
||||||
|
float ddy = targetPos.Y - currentPos.Y;
|
||||||
|
float sqrDist = (ddx * ddx) + (ddy * ddy);
|
||||||
|
|
||||||
|
if (sqrDist < 0.01f)
|
||||||
|
{
|
||||||
|
if (a.PathIndex == p.Count - 1)
|
||||||
|
{
|
||||||
|
a.Path = null;
|
||||||
|
a.Destination = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.NextPosition = p[++a.PathIndex];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sqrDist <= remaining * remaining)
|
||||||
|
{
|
||||||
|
a.Position = targetPos;
|
||||||
|
remaining -= MathF.Sqrt(sqrDist);
|
||||||
|
|
||||||
|
if (a.PathIndex == p.Count - 1)
|
||||||
|
{
|
||||||
|
a.Path = null;
|
||||||
|
a.Destination = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.NextPosition = p[++a.PathIndex];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float t = remaining / MathF.Sqrt(sqrDist);
|
||||||
|
a.Position = new Vector2(currentPos.X + (ddx * t), currentPos.Y + (ddy * t));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RebuildSectors()
|
||||||
|
{
|
||||||
|
int sx = (gridService.Width + SectorSize - 1) / SectorSize;
|
||||||
|
int sy = (gridService.Height + SectorSize - 1) / SectorSize;
|
||||||
|
|
||||||
|
List<AgentComponent>[,] newSectors = new List<AgentComponent>[sx, sy];
|
||||||
|
|
||||||
|
foreach (AgentComponent agent in agentList)
|
||||||
|
{
|
||||||
|
int x = Math.Clamp((int)(agent.Position.X / SectorSize), 0, sx - 1);
|
||||||
|
int y = Math.Clamp((int)(agent.Position.Y / SectorSize), 0, sy - 1);
|
||||||
|
(newSectors[x, y] ??= []).Add(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
sectorsX = sx;
|
||||||
|
sectorsY = sy;
|
||||||
|
agentSectors = newSectors;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
using MonoGame.Extended;
|
||||||
|
using MonoGame.Extended.ECS;
|
||||||
|
using MonoGame.Extended.ECS.Systems;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using LargeGridPathfinding.Components;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Systems;
|
||||||
|
|
||||||
|
public class AgentRenderSystem : EntityDrawSystem
|
||||||
|
{
|
||||||
|
private const int SectorSize = 1 << 7;
|
||||||
|
|
||||||
|
private readonly SpriteBatch spriteBatch;
|
||||||
|
private readonly OrthographicCamera camera;
|
||||||
|
private readonly Func<int> getSectorsX;
|
||||||
|
private readonly Func<int> getSectorsY;
|
||||||
|
private readonly Func<List<AgentComponent>[,]> getAgentSectors;
|
||||||
|
private ComponentMapper<AgentComponent> agentMapper = null!;
|
||||||
|
|
||||||
|
public AgentRenderSystem(
|
||||||
|
SpriteBatch spriteBatch,
|
||||||
|
OrthographicCamera camera,
|
||||||
|
Func<int> getSectorsX,
|
||||||
|
Func<int> getSectorsY,
|
||||||
|
Func<List<AgentComponent>[,]> getAgentSectors)
|
||||||
|
: base(Aspect.All(typeof(AgentComponent)))
|
||||||
|
{
|
||||||
|
this.spriteBatch = spriteBatch;
|
||||||
|
this.camera = camera;
|
||||||
|
this.getSectorsX = getSectorsX;
|
||||||
|
this.getSectorsY = getSectorsY;
|
||||||
|
this.getAgentSectors = getAgentSectors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(IComponentMapperService mapperService)
|
||||||
|
{
|
||||||
|
agentMapper = mapperService.GetMapper<AgentComponent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
Matrix transformMatrix = camera.GetViewMatrix();
|
||||||
|
|
||||||
|
int yStart = Math.Max(0, (int)(camera.BoundingRectangle.Top / 10) - 2);
|
||||||
|
int xStart = Math.Max(0, (int)(camera.BoundingRectangle.Left / 10) - 2);
|
||||||
|
int yEnd = Math.Min((int)(camera.BoundingRectangle.Bottom / 10) + 2, 1000000);
|
||||||
|
int xEnd = Math.Min((int)(camera.BoundingRectangle.Right / 10) + 2, 1000000);
|
||||||
|
|
||||||
|
int sectorsX = getSectorsX();
|
||||||
|
int sectorsY = getSectorsY();
|
||||||
|
var agentSectors = getAgentSectors();
|
||||||
|
|
||||||
|
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp, transformMatrix: transformMatrix);
|
||||||
|
|
||||||
|
int drawMinSX = Math.Max(0, (xStart / SectorSize) - 1);
|
||||||
|
int drawMaxSX = Math.Min(sectorsX - 1, (xEnd / SectorSize) + 1);
|
||||||
|
int drawMinSY = Math.Max(0, (yStart / SectorSize) - 1);
|
||||||
|
int drawMaxSY = Math.Min(sectorsY - 1, (yEnd / SectorSize) + 1);
|
||||||
|
|
||||||
|
for (int sx = drawMinSX; sx <= drawMaxSX; sx++)
|
||||||
|
{
|
||||||
|
for (int sy = drawMinSY; sy <= drawMaxSY; sy++)
|
||||||
|
{
|
||||||
|
List<AgentComponent>? sector = agentSectors[sx, sy];
|
||||||
|
if (sector is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (AgentComponent agent in sector)
|
||||||
|
{
|
||||||
|
Vector2 pos = agent.Position;
|
||||||
|
if (pos.X >= xStart && pos.X <= xEnd && pos.Y >= yStart && pos.Y <= yEnd)
|
||||||
|
{
|
||||||
|
spriteBatch.DrawCircle((pos + new Vector2(0.5f, 0.5f)) * 10, 5, 10, Color.Blue, 2f, layerDepth: 0.1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteBatch.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
|
||||||
|
using MonoGame.Extended;
|
||||||
|
using MonoGame.Extended.ECS;
|
||||||
|
using MonoGame.Extended.ECS.Systems;
|
||||||
|
using MonoGame.Extended.Input;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using LargeGridPathfinding.Services;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Systems;
|
||||||
|
|
||||||
|
public class GridEditSystem : IUpdateSystem
|
||||||
|
{
|
||||||
|
public enum BrushMode
|
||||||
|
{
|
||||||
|
Weight,
|
||||||
|
Obstacle
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly GridService gridService;
|
||||||
|
private readonly OrthographicCamera camera;
|
||||||
|
private readonly ConcurrentDictionary<Vector2, Color> temporaryIndicators;
|
||||||
|
private readonly ProgressTracker progressTracker;
|
||||||
|
private readonly Func<bool> isInputBlocked;
|
||||||
|
private readonly Action<bool> setInputBlocked;
|
||||||
|
private readonly Func<bool> getShowZones;
|
||||||
|
private readonly Action<bool> setShowZones;
|
||||||
|
private readonly Func<bool> getShowGrid;
|
||||||
|
private readonly Action<bool> setShowGrid;
|
||||||
|
private readonly Func<bool> getShowPaths;
|
||||||
|
private readonly Action<bool> setShowPaths;
|
||||||
|
|
||||||
|
private BrushMode brushMode = BrushMode.Obstacle;
|
||||||
|
private int paintWeight = 5;
|
||||||
|
private Vector2? previousMousePosition;
|
||||||
|
private bool wasShiftLeftDown;
|
||||||
|
private bool wasShiftRightDown;
|
||||||
|
private Point? fillSelectionStart;
|
||||||
|
private Point? fillSelectionCurrent;
|
||||||
|
|
||||||
|
public BrushMode CurrentBrushMode => brushMode;
|
||||||
|
public int PaintWeight => paintWeight;
|
||||||
|
public Point? FillSelectionStart => fillSelectionStart;
|
||||||
|
public Point? FillSelectionCurrent => fillSelectionCurrent;
|
||||||
|
|
||||||
|
public GridEditSystem(
|
||||||
|
GridService gridService,
|
||||||
|
OrthographicCamera camera,
|
||||||
|
ConcurrentDictionary<Vector2, Color> temporaryIndicators,
|
||||||
|
ProgressTracker progressTracker,
|
||||||
|
Func<bool> isInputBlocked,
|
||||||
|
Action<bool> setInputBlocked,
|
||||||
|
Func<bool> getShowZones,
|
||||||
|
Action<bool> setShowZones,
|
||||||
|
Func<bool> getShowGrid,
|
||||||
|
Action<bool> setShowGrid,
|
||||||
|
Func<bool> getShowPaths,
|
||||||
|
Action<bool> setShowPaths)
|
||||||
|
{
|
||||||
|
this.gridService = gridService;
|
||||||
|
this.camera = camera;
|
||||||
|
this.temporaryIndicators = temporaryIndicators;
|
||||||
|
this.progressTracker = progressTracker;
|
||||||
|
this.isInputBlocked = isInputBlocked;
|
||||||
|
this.setInputBlocked = setInputBlocked;
|
||||||
|
this.getShowZones = getShowZones;
|
||||||
|
this.setShowZones = setShowZones;
|
||||||
|
this.getShowGrid = getShowGrid;
|
||||||
|
this.setShowGrid = setShowGrid;
|
||||||
|
this.getShowPaths = getShowPaths;
|
||||||
|
this.setShowPaths = setShowPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(World world) { }
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
|
||||||
|
public void Update(GameTime gameTime)
|
||||||
|
{
|
||||||
|
MouseStateExtended mouseState = MouseExtended.GetState();
|
||||||
|
KeyboardStateExtended keyboardState = KeyboardExtended.GetState();
|
||||||
|
bool shiftPressed = keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift);
|
||||||
|
bool shiftLeftDown = shiftPressed && mouseState.IsButtonDown(MouseButton.Left);
|
||||||
|
bool shiftLeftClicked = shiftLeftDown && !wasShiftLeftDown;
|
||||||
|
bool shiftRightDown = shiftPressed && mouseState.IsButtonDown(MouseButton.Right);
|
||||||
|
bool shiftRightClicked = shiftRightDown && !wasShiftRightDown;
|
||||||
|
wasShiftLeftDown = shiftLeftDown;
|
||||||
|
wasShiftRightDown = shiftRightDown;
|
||||||
|
|
||||||
|
if (isInputBlocked())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.Z))
|
||||||
|
{
|
||||||
|
setShowZones(!getShowZones());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.G))
|
||||||
|
{
|
||||||
|
setShowGrid(!getShowGrid());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.P))
|
||||||
|
{
|
||||||
|
setShowPaths(!getShowPaths());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.D1) || keyboardState.WasKeyPressed(Keys.NumPad1))
|
||||||
|
{
|
||||||
|
paintWeight = 1;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D2) || keyboardState.WasKeyPressed(Keys.NumPad2))
|
||||||
|
{
|
||||||
|
paintWeight = 2;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D3) || keyboardState.WasKeyPressed(Keys.NumPad3))
|
||||||
|
{
|
||||||
|
paintWeight = 3;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D4) || keyboardState.WasKeyPressed(Keys.NumPad4))
|
||||||
|
{
|
||||||
|
paintWeight = 4;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D5) || keyboardState.WasKeyPressed(Keys.NumPad5))
|
||||||
|
{
|
||||||
|
paintWeight = 5;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D6) || keyboardState.WasKeyPressed(Keys.NumPad6))
|
||||||
|
{
|
||||||
|
paintWeight = 6;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D7) || keyboardState.WasKeyPressed(Keys.NumPad7))
|
||||||
|
{
|
||||||
|
paintWeight = 7;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D8) || keyboardState.WasKeyPressed(Keys.NumPad8))
|
||||||
|
{
|
||||||
|
paintWeight = 8;
|
||||||
|
}
|
||||||
|
else if (keyboardState.WasKeyPressed(Keys.D9) || keyboardState.WasKeyPressed(Keys.NumPad9))
|
||||||
|
{
|
||||||
|
paintWeight = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.O))
|
||||||
|
{
|
||||||
|
brushMode = brushMode == BrushMode.Weight ? BrushMode.Obstacle : BrushMode.Weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboardState.WasKeyPressed(Keys.F))
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Filling grid...");
|
||||||
|
|
||||||
|
_ = progressTracker.AddProgress("Filling grid", out IProgress<float> fillingGridProgressReporter);
|
||||||
|
_ = progressTracker.AddProgress("Calculating candidates", out IProgress<float> calculatingCandidatesProgressReporter);
|
||||||
|
_ = progressTracker.AddProgress("Placing zones", out IProgress<float> placingCandidatesProgressReporter);
|
||||||
|
|
||||||
|
bool showZonesBefore = getShowZones();
|
||||||
|
bool showGridBefore = getShowGrid();
|
||||||
|
bool showPathsBefore = getShowPaths();
|
||||||
|
setShowZones(true);
|
||||||
|
setShowGrid(false);
|
||||||
|
setShowPaths(false);
|
||||||
|
setInputBlocked(true);
|
||||||
|
|
||||||
|
_ = System.Threading.Tasks.Task.Run(() =>
|
||||||
|
{
|
||||||
|
gridService.Filler.FillGrid(fillAll: true, totalProgress: fillingGridProgressReporter, calculatingCandidatesProgress: calculatingCandidatesProgressReporter, placingCandidatesProgress: placingCandidatesProgressReporter);
|
||||||
|
gridService.GridChanged = true;
|
||||||
|
setInputBlocked(false);
|
||||||
|
setShowZones(showZonesBefore);
|
||||||
|
setShowGrid(showGridBefore);
|
||||||
|
setShowPaths(showPathsBefore);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shiftPressed)
|
||||||
|
{
|
||||||
|
if (fillSelectionStart.HasValue && TryGetMouseGridPoint(mouseState, out Point hoverGridPoint))
|
||||||
|
{
|
||||||
|
fillSelectionCurrent = hoverGridPoint;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fillSelectionCurrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((shiftLeftClicked || shiftRightClicked) && TryGetMouseGridPoint(mouseState, out Point clickedGridPoint))
|
||||||
|
{
|
||||||
|
if (!fillSelectionStart.HasValue)
|
||||||
|
{
|
||||||
|
fillSelectionStart = clickedGridPoint;
|
||||||
|
fillSelectionCurrent = clickedGridPoint;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<Point> selectionPoints = GetRectanglePoints(fillSelectionStart.Value, clickedGridPoint);
|
||||||
|
Color indicatorColor = brushMode == BrushMode.Weight ? Color.Orange : Color.Red;
|
||||||
|
|
||||||
|
foreach (Point point in selectionPoints)
|
||||||
|
{
|
||||||
|
temporaryIndicators[point.ToVector2()] = indicatorColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
GridService.PendingOperation operation;
|
||||||
|
if (brushMode == BrushMode.Weight && shiftLeftClicked)
|
||||||
|
{
|
||||||
|
operation = new GridService.PendingOperation(GridService.PendingOperationKind.SetWeight, paintWeight);
|
||||||
|
}
|
||||||
|
else if (brushMode == BrushMode.Weight && shiftRightClicked)
|
||||||
|
{
|
||||||
|
operation = new GridService.PendingOperation(GridService.PendingOperationKind.ResetWeight, 0);
|
||||||
|
}
|
||||||
|
else if (brushMode == BrushMode.Obstacle && shiftLeftClicked)
|
||||||
|
{
|
||||||
|
operation = new GridService.PendingOperation(GridService.PendingOperationKind.PlaceObstacle, 0);
|
||||||
|
}
|
||||||
|
else if (brushMode == BrushMode.Obstacle && shiftRightClicked)
|
||||||
|
{
|
||||||
|
operation = new GridService.PendingOperation(GridService.PendingOperationKind.RemoveObstacle, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Invalid brush mode or mouse button state.");
|
||||||
|
}
|
||||||
|
|
||||||
|
gridService.EnqueueOperations(selectionPoints, operation);
|
||||||
|
fillSelectionStart = null;
|
||||||
|
fillSelectionCurrent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousMousePosition = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fillSelectionStart = null;
|
||||||
|
fillSelectionCurrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouseState.IsButtonDown(MouseButton.Left))
|
||||||
|
{
|
||||||
|
Vector2 mousePosition = camera.ScreenToWorld(mouseState.Position.ToVector2());
|
||||||
|
Vector2 gridPosition = new((int)mousePosition.X / 10, (int)mousePosition.Y / 10);
|
||||||
|
|
||||||
|
if (gridPosition.X < 0 || gridPosition.X >= gridService.Width || gridPosition.Y < 0 || gridPosition.Y >= gridService.Height || temporaryIndicators.ContainsKey(gridPosition))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cellValue = gridService.Grid[(int)gridPosition.Y, (int)gridPosition.X];
|
||||||
|
if ((brushMode == BrushMode.Weight && cellValue < 0) || (brushMode == BrushMode.Obstacle && cellValue < 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2? previousGridPosition = null;
|
||||||
|
if (previousMousePosition is not null)
|
||||||
|
{
|
||||||
|
previousGridPosition = new Vector2((int)previousMousePosition.Value.X / 10, (int)previousMousePosition.Value.Y / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color indicatorColor = brushMode == BrushMode.Weight ? Color.Orange : Color.Red;
|
||||||
|
BrushMode currentBrushMode = brushMode;
|
||||||
|
int currentPaintWeight = paintWeight;
|
||||||
|
previousMousePosition = mousePosition;
|
||||||
|
|
||||||
|
List<Point> brushPoints = GetBrushPoints(gridPosition, previousGridPosition);
|
||||||
|
foreach (Point point in brushPoints)
|
||||||
|
{
|
||||||
|
temporaryIndicators[point.ToVector2()] = indicatorColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
GridService.PendingOperation pendingOperation = currentBrushMode == BrushMode.Weight
|
||||||
|
? new GridService.PendingOperation(GridService.PendingOperationKind.SetWeight, currentPaintWeight)
|
||||||
|
: new GridService.PendingOperation(GridService.PendingOperationKind.PlaceObstacle, 0);
|
||||||
|
|
||||||
|
gridService.EnqueueOperations(brushPoints, pendingOperation);
|
||||||
|
}
|
||||||
|
else if (mouseState.IsButtonDown(MouseButton.Right))
|
||||||
|
{
|
||||||
|
Vector2 mousePosition = camera.ScreenToWorld(mouseState.Position.ToVector2());
|
||||||
|
Vector2 gridPosition = new((int)mousePosition.X / 10, (int)mousePosition.Y / 10);
|
||||||
|
|
||||||
|
if (gridPosition.X < 0 || gridPosition.X >= gridService.Width || gridPosition.Y < 0 || gridPosition.Y >= gridService.Height || temporaryIndicators.ContainsKey(gridPosition))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cellValue = gridService.Grid[(int)gridPosition.Y, (int)gridPosition.X];
|
||||||
|
if ((brushMode == BrushMode.Weight && cellValue < 0) || (brushMode == BrushMode.Obstacle && cellValue >= 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2? previousGridPosition = null;
|
||||||
|
if (previousMousePosition is not null)
|
||||||
|
{
|
||||||
|
previousGridPosition = new Vector2((int)previousMousePosition.Value.X / 10, (int)previousMousePosition.Value.Y / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color indicatorColor = brushMode == BrushMode.Weight ? Color.LightGray : Color.Yellow;
|
||||||
|
BrushMode currentBrushMode = brushMode;
|
||||||
|
previousMousePosition = mousePosition;
|
||||||
|
|
||||||
|
List<Point> brushPoints = GetBrushPoints(gridPosition, previousGridPosition);
|
||||||
|
foreach (Point point in brushPoints)
|
||||||
|
{
|
||||||
|
temporaryIndicators[point.ToVector2()] = indicatorColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
GridService.PendingOperation pendingOperation = currentBrushMode == BrushMode.Weight
|
||||||
|
? new GridService.PendingOperation(GridService.PendingOperationKind.ResetWeight, 0)
|
||||||
|
: new GridService.PendingOperation(GridService.PendingOperationKind.RemoveObstacle, 0);
|
||||||
|
|
||||||
|
gridService.EnqueueOperations(brushPoints, pendingOperation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
previousMousePosition = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetMouseGridPoint(MouseStateExtended mouseState, out Point gridPoint)
|
||||||
|
{
|
||||||
|
Vector2 mousePosition = camera.ScreenToWorld(mouseState.Position.ToVector2());
|
||||||
|
Point point = new((int)mousePosition.X / 10, (int)mousePosition.Y / 10);
|
||||||
|
|
||||||
|
if (point.X < 0 || point.X >= gridService.Width || point.Y < 0 || point.Y >= gridService.Height)
|
||||||
|
{
|
||||||
|
gridPoint = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridPoint = point;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Point> GetRectanglePoints(Point start, Point end)
|
||||||
|
{
|
||||||
|
int minX = Math.Min(start.X, end.X);
|
||||||
|
int minY = Math.Min(start.Y, end.Y);
|
||||||
|
int maxX = Math.Max(start.X, end.X);
|
||||||
|
int maxY = Math.Max(start.Y, end.Y);
|
||||||
|
|
||||||
|
List<Point> points = new((maxX - minX + 1) * (maxY - minY + 1));
|
||||||
|
|
||||||
|
for (int y = minY; y <= maxY; y++)
|
||||||
|
{
|
||||||
|
for (int x = minX; x <= maxX; x++)
|
||||||
|
{
|
||||||
|
points.Add(new Point(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Point> GetBrushPoints(Vector2 currentGridPosition, Vector2? previousGridPosition)
|
||||||
|
{
|
||||||
|
HashSet<Point> points = [];
|
||||||
|
Point currentPoint = new((int)currentGridPosition.X, (int)currentGridPosition.Y);
|
||||||
|
_ = points.Add(currentPoint);
|
||||||
|
|
||||||
|
if (previousGridPosition is null)
|
||||||
|
{
|
||||||
|
return [.. points];
|
||||||
|
}
|
||||||
|
|
||||||
|
float distance = Vector2.Distance(previousGridPosition.Value, currentGridPosition);
|
||||||
|
int steps = Math.Max(1, (int)Math.Ceiling(distance * 10));
|
||||||
|
|
||||||
|
for (int i = 0; i <= steps; i++)
|
||||||
|
{
|
||||||
|
float t = i / (float)steps;
|
||||||
|
Vector2 interpolatedPosition = Vector2.Lerp(previousGridPosition.Value, currentGridPosition, t);
|
||||||
|
Point interpolatedPoint = new((int)Math.Round(interpolatedPosition.X), (int)Math.Round(interpolatedPosition.Y));
|
||||||
|
_ = points.Add(interpolatedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. points];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
using MonoGame.Extended;
|
||||||
|
using MonoGame.Extended.ECS;
|
||||||
|
using MonoGame.Extended.ECS.Systems;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using LargeGridPathfinding.Services;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Systems;
|
||||||
|
|
||||||
|
public class GridRenderSystem : IDrawSystem
|
||||||
|
{
|
||||||
|
private const int SectorShift = 7;
|
||||||
|
private const int SectorSize = 1 << SectorShift;
|
||||||
|
|
||||||
|
private readonly SpriteBatch spriteBatch;
|
||||||
|
private readonly GridService gridService;
|
||||||
|
private readonly OrthographicCamera camera;
|
||||||
|
private readonly ConcurrentDictionary<Vector2, Color> temporaryIndicators;
|
||||||
|
private readonly Func<bool> getShowZones;
|
||||||
|
private readonly Func<bool> getShowGrid;
|
||||||
|
private readonly Func<Point?> getFillSelectionStart;
|
||||||
|
private readonly Func<Point?> getFillSelectionCurrent;
|
||||||
|
|
||||||
|
private static readonly Color[] ColorLookup =
|
||||||
|
[
|
||||||
|
Color.Blue, Color.Cyan, Color.Magenta, Color.Yellow, Color.Orange,
|
||||||
|
Color.DarkMagenta, Color.DarkCyan, Color.Tan, Color.RosyBrown,
|
||||||
|
Color.DarkKhaki, Color.DarkSalmon, Color.DarkSlateGray,
|
||||||
|
Color.DarkTurquoise, Color.DarkGoldenrod, Color.Aqua, Color.Aquamarine,
|
||||||
|
Color.Bisque, Color.DarkSlateBlue, Color.BlueViolet, Color.Brown,
|
||||||
|
Color.BurlyWood, Color.CadetBlue, Color.Chartreuse, Color.Chocolate,
|
||||||
|
Color.Coral, Color.CornflowerBlue, Color.Crimson, Color.DarkBlue
|
||||||
|
];
|
||||||
|
|
||||||
|
public GridRenderSystem(
|
||||||
|
SpriteBatch spriteBatch,
|
||||||
|
GridService gridService,
|
||||||
|
OrthographicCamera camera,
|
||||||
|
ConcurrentDictionary<Vector2, Color> temporaryIndicators,
|
||||||
|
Func<bool> getShowZones,
|
||||||
|
Func<bool> getShowGrid,
|
||||||
|
Func<Point?> getFillSelectionStart,
|
||||||
|
Func<Point?> getFillSelectionCurrent)
|
||||||
|
{
|
||||||
|
this.spriteBatch = spriteBatch;
|
||||||
|
this.gridService = gridService;
|
||||||
|
this.camera = camera;
|
||||||
|
this.temporaryIndicators = temporaryIndicators;
|
||||||
|
this.getShowZones = getShowZones;
|
||||||
|
this.getShowGrid = getShowGrid;
|
||||||
|
this.getFillSelectionStart = getFillSelectionStart;
|
||||||
|
this.getFillSelectionCurrent = getFillSelectionCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(World world) { }
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
|
||||||
|
public void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
Matrix transformMatrix = camera.GetViewMatrix();
|
||||||
|
|
||||||
|
int yStart = Math.Max(0, (int)(camera.BoundingRectangle.Top / 10) - 2);
|
||||||
|
int xStart = Math.Max(0, (int)(camera.BoundingRectangle.Left / 10) - 2);
|
||||||
|
int yEnd = Math.Min(gridService.Height, (int)(camera.BoundingRectangle.Bottom / 10) + 2);
|
||||||
|
int xEnd = Math.Min(gridService.Width, (int)(camera.BoundingRectangle.Right / 10) + 2);
|
||||||
|
|
||||||
|
int[,] grid = gridService.Grid;
|
||||||
|
Rectangle cellRect = new(0, 0, 10, 10);
|
||||||
|
|
||||||
|
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp, transformMatrix: transformMatrix);
|
||||||
|
|
||||||
|
spriteBatch.FillRectangle(new Rectangle(0, 0, gridService.Width * 10, gridService.Height * 10), Color.White, layerDepth: 0.4f);
|
||||||
|
|
||||||
|
if (getShowZones())
|
||||||
|
{
|
||||||
|
Dictionary<int, Rectangle> rectangles = new(gridService.Filler.PlacedRectangles);
|
||||||
|
|
||||||
|
foreach ((int label, Rectangle rectangle) in rectangles)
|
||||||
|
{
|
||||||
|
Color color = ColorLookup[label % 28];
|
||||||
|
spriteBatch.FillRectangle(new Rectangle(rectangle.X * 10, rectangle.Y * 10, rectangle.Width * 10, rectangle.Height * 10), Color.Lerp(color, Color.Gray, 0.55f), layerDepth: 0.2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = yStart; y < yEnd; y++)
|
||||||
|
{
|
||||||
|
for (int x = xStart; x < xEnd; x++)
|
||||||
|
{
|
||||||
|
cellRect.X = x * 10;
|
||||||
|
cellRect.Y = y * 10;
|
||||||
|
|
||||||
|
int cellValue = grid[y, x];
|
||||||
|
int cellWeight = gridService.WeightGrid[y, x];
|
||||||
|
|
||||||
|
if (cellWeight > 1 && cellValue >= 0)
|
||||||
|
{
|
||||||
|
float blendFactor = Math.Clamp((cellWeight - 1) / 8f, 0f, 1f);
|
||||||
|
Color weightColor = Color.Lerp(Color.LightYellow, Color.DarkOrange, blendFactor);
|
||||||
|
|
||||||
|
if (getShowZones())
|
||||||
|
{
|
||||||
|
spriteBatch.DrawRectangle(cellRect, weightColor * 0.75f, layerDepth: 0.25f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
spriteBatch.FillRectangle(cellRect, weightColor * 0.75f, layerDepth: 0.25f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellValue < 0)
|
||||||
|
{
|
||||||
|
spriteBatch.FillRectangle(cellRect, Color.Red, layerDepth: 0.3f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temporaryIndicators.TryGetValue(new Vector2(x, y), out Color indicatorColor))
|
||||||
|
{
|
||||||
|
spriteBatch.DrawCircle(cellRect.Center.ToVector2(), 5, 10, indicatorColor, 2f, layerDepth: 0.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getShowGrid())
|
||||||
|
{
|
||||||
|
spriteBatch.DrawRectangle(cellRect, Color.Black, layerDepth: 0.2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Point? fillSelectionStart = getFillSelectionStart();
|
||||||
|
if (fillSelectionStart.HasValue)
|
||||||
|
{
|
||||||
|
Point selectionEnd = getFillSelectionCurrent() ?? fillSelectionStart.Value;
|
||||||
|
int left = Math.Min(fillSelectionStart.Value.X, selectionEnd.X);
|
||||||
|
int top = Math.Min(fillSelectionStart.Value.Y, selectionEnd.Y);
|
||||||
|
int right = Math.Max(fillSelectionStart.Value.X, selectionEnd.X);
|
||||||
|
int bottom = Math.Max(fillSelectionStart.Value.Y, selectionEnd.Y);
|
||||||
|
|
||||||
|
Rectangle selectionRectangle = new(left * 10, top * 10, (right - left + 1) * 10, (bottom - top + 1) * 10);
|
||||||
|
spriteBatch.FillRectangle(selectionRectangle, Color.LightBlue * 0.2f, layerDepth: 0.24f);
|
||||||
|
spriteBatch.DrawRectangle(selectionRectangle, Color.Blue, 2f, layerDepth: 0.19f);
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteBatch.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
using MonoGame.Extended;
|
||||||
|
using MonoGame.Extended.ECS;
|
||||||
|
using MonoGame.Extended.ECS.Systems;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using LargeGridPathfinding.Components;
|
||||||
|
|
||||||
|
namespace LargeGridPathfinding.Systems;
|
||||||
|
|
||||||
|
public class PathRenderSystem : EntityDrawSystem
|
||||||
|
{
|
||||||
|
private const int SectorSize = 1 << 7;
|
||||||
|
|
||||||
|
private readonly SpriteBatch spriteBatch;
|
||||||
|
private readonly OrthographicCamera camera;
|
||||||
|
private readonly Func<bool> getShowPaths;
|
||||||
|
private readonly Func<int> getSectorsX;
|
||||||
|
private readonly Func<int> getSectorsY;
|
||||||
|
private readonly Func<List<AgentComponent>[,]> getAgentSectors;
|
||||||
|
private ComponentMapper<AgentComponent> agentMapper = null!;
|
||||||
|
|
||||||
|
public PathRenderSystem(
|
||||||
|
SpriteBatch spriteBatch,
|
||||||
|
OrthographicCamera camera,
|
||||||
|
Func<bool> getShowPaths,
|
||||||
|
Func<int> getSectorsX,
|
||||||
|
Func<int> getSectorsY,
|
||||||
|
Func<List<AgentComponent>[,]> getAgentSectors)
|
||||||
|
: base(Aspect.All(typeof(AgentComponent)))
|
||||||
|
{
|
||||||
|
this.spriteBatch = spriteBatch;
|
||||||
|
this.camera = camera;
|
||||||
|
this.getShowPaths = getShowPaths;
|
||||||
|
this.getSectorsX = getSectorsX;
|
||||||
|
this.getSectorsY = getSectorsY;
|
||||||
|
this.getAgentSectors = getAgentSectors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(IComponentMapperService mapperService)
|
||||||
|
{
|
||||||
|
agentMapper = mapperService.GetMapper<AgentComponent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
if (!getShowPaths())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix transformMatrix = camera.GetViewMatrix();
|
||||||
|
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp, transformMatrix: transformMatrix);
|
||||||
|
|
||||||
|
float viewLeft = camera.BoundingRectangle.Left - 10;
|
||||||
|
float viewRight = camera.BoundingRectangle.Right + 10;
|
||||||
|
float viewTop = camera.BoundingRectangle.Top - 10;
|
||||||
|
float viewBottom = camera.BoundingRectangle.Bottom + 10;
|
||||||
|
|
||||||
|
int sectorsX = getSectorsX();
|
||||||
|
int sectorsY = getSectorsY();
|
||||||
|
var agentSectors = getAgentSectors();
|
||||||
|
|
||||||
|
int pathMinSX = Math.Max(0, (int)(viewLeft / 10f / SectorSize) - 1);
|
||||||
|
int pathMaxSX = Math.Min(sectorsX - 1, (int)(viewRight / 10f / SectorSize) + 1);
|
||||||
|
int pathMinSY = Math.Max(0, (int)(viewTop / 10f / SectorSize) - 1);
|
||||||
|
int pathMaxSY = Math.Min(sectorsY - 1, (int)(viewBottom / 10f / SectorSize) + 1);
|
||||||
|
|
||||||
|
for (int sx = pathMinSX; sx <= pathMaxSX; sx++)
|
||||||
|
{
|
||||||
|
for (int sy = pathMinSY; sy <= pathMaxSY; sy++)
|
||||||
|
{
|
||||||
|
List<AgentComponent>? sector = agentSectors[sx, sy];
|
||||||
|
if (sector is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (AgentComponent agent in sector)
|
||||||
|
{
|
||||||
|
List<Vector2>? path = agent.Path;
|
||||||
|
if (path is null || path.Count < 2)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < path.Count - 1; i++)
|
||||||
|
{
|
||||||
|
Vector2 start = (path[i] * 10) + new Vector2(5, 5);
|
||||||
|
Vector2 end = (path[i + 1] * 10) + new Vector2(5, 5);
|
||||||
|
|
||||||
|
bool startInView = start.X >= viewLeft && start.X <= viewRight
|
||||||
|
&& start.Y >= viewTop && start.Y <= viewBottom;
|
||||||
|
bool endInView = end.X >= viewLeft && end.X <= viewRight
|
||||||
|
&& end.Y >= viewTop && end.Y <= viewBottom;
|
||||||
|
|
||||||
|
bool segmentVisible = !((start.X < viewLeft && end.X < viewLeft) ||
|
||||||
|
(start.X > viewRight && end.X > viewRight) ||
|
||||||
|
(start.Y < viewTop && end.Y < viewTop) ||
|
||||||
|
(start.Y > viewBottom && end.Y > viewBottom));
|
||||||
|
|
||||||
|
if (segmentVisible)
|
||||||
|
{
|
||||||
|
spriteBatch.DrawLine(start, end, Color.Gray, 2f, layerDepth: 0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startInView)
|
||||||
|
{
|
||||||
|
spriteBatch.DrawCircle(start, 5, 10, i == 0 ? Color.Green : Color.Gray, 2f, layerDepth: 0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == path.Count - 2 && endInView)
|
||||||
|
{
|
||||||
|
spriteBatch.DrawCircle(end, 5, 10, Color.Red, 2f, layerDepth: 0.1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteBatch.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user