From 899244732379a7597260b776306bd7e584efa0fb Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Fri, 14 Feb 2025 01:17:55 +0100 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 398 +++++++++++ LICENSE | 21 + README.md | 2 + src/LargeGridPathfinding.sln | 22 + .../.config/dotnet-tools.json | 36 + src/LargeGridPathfinding/Agent.cs | 19 + src/LargeGridPathfinding/Content/Content.mgcb | 22 + .../Content/File.spritefont | 60 ++ .../Content/File1.spritefont | 60 ++ .../Content/ManoloMono.ttf | Bin 0 -> 9232 bytes src/LargeGridPathfinding/GridFiller.cs | 297 ++++++++ src/LargeGridPathfinding/Icon.bmp | Bin 0 -> 262282 bytes src/LargeGridPathfinding/Icon.ico | Bin 0 -> 147541 bytes src/LargeGridPathfinding/InputHelper.cs | 31 + .../LargeGridPathfinding.csproj | 35 + .../LargeGridPathfindingGame.cs | 652 ++++++++++++++++++ src/LargeGridPathfinding/Pathfinder.cs | 231 +++++++ src/LargeGridPathfinding/Program.cs | 2 + src/LargeGridPathfinding/ProgressTracker.cs | 79 +++ src/LargeGridPathfinding/app.manifest | 43 ++ 21 files changed, 2012 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/LargeGridPathfinding.sln create mode 100644 src/LargeGridPathfinding/.config/dotnet-tools.json create mode 100644 src/LargeGridPathfinding/Agent.cs create mode 100644 src/LargeGridPathfinding/Content/Content.mgcb create mode 100644 src/LargeGridPathfinding/Content/File.spritefont create mode 100644 src/LargeGridPathfinding/Content/File1.spritefont create mode 100644 src/LargeGridPathfinding/Content/ManoloMono.ttf create mode 100644 src/LargeGridPathfinding/GridFiller.cs create mode 100644 src/LargeGridPathfinding/Icon.bmp create mode 100644 src/LargeGridPathfinding/Icon.ico create mode 100644 src/LargeGridPathfinding/InputHelper.cs create mode 100644 src/LargeGridPathfinding/LargeGridPathfinding.csproj create mode 100644 src/LargeGridPathfinding/LargeGridPathfindingGame.cs create mode 100644 src/LargeGridPathfinding/Pathfinder.cs create mode 100644 src/LargeGridPathfinding/Program.cs create mode 100644 src/LargeGridPathfinding/ProgressTracker.cs create mode 100644 src/LargeGridPathfinding/app.manifest diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a30d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ccfb086 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Stone_Red + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..08c3d21 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# LargeGridPathfinding + A zone-based pathfinding algorithm that efficiently handles concurrent navigation of numerous agents across large, grid-based maps diff --git a/src/LargeGridPathfinding.sln b/src/LargeGridPathfinding.sln new file mode 100644 index 0000000..d61d4f2 --- /dev/null +++ b/src/LargeGridPathfinding.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LargeGridPathfinding", "LargeGridPathfinding\LargeGridPathfinding.csproj", "{4F58A5EA-B389-4AFE-979C-0F0B0AAAD86C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4F58A5EA-B389-4AFE-979C-0F0B0AAAD86C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F58A5EA-B389-4AFE-979C-0F0B0AAAD86C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F58A5EA-B389-4AFE-979C-0F0B0AAAD86C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F58A5EA-B389-4AFE-979C-0F0B0AAAD86C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/LargeGridPathfinding/.config/dotnet-tools.json b/src/LargeGridPathfinding/.config/dotnet-tools.json new file mode 100644 index 0000000..84ff03d --- /dev/null +++ b/src/LargeGridPathfinding/.config/dotnet-tools.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-mgcb": { + "version": "3.8.2.1105", + "commands": [ + "mgcb" + ] + }, + "dotnet-mgcb-editor": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor" + ] + }, + "dotnet-mgcb-editor-linux": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor-linux" + ] + }, + "dotnet-mgcb-editor-windows": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor-windows" + ] + }, + "dotnet-mgcb-editor-mac": { + "version": "3.8.2.1105", + "commands": [ + "mgcb-editor-mac" + ] + } + } +} \ No newline at end of file diff --git a/src/LargeGridPathfinding/Agent.cs b/src/LargeGridPathfinding/Agent.cs new file mode 100644 index 0000000..aa0b962 --- /dev/null +++ b/src/LargeGridPathfinding/Agent.cs @@ -0,0 +1,19 @@ +using Microsoft.Xna.Framework; + +using System; +using System.Collections.Generic; + +namespace LargeGridPathfinding; + +internal class Agent(Vector2 position) +{ + public Vector2 Position { get; set; } = position; + + public Point GridPosition => new Point((int)Math.Round(Position.X), (int)Math.Round(Position.Y)); + + public Vector2 NextPosition { get; set; } + + public Point? Destination { get; set; } + + public List? Path { get; set; } +} \ No newline at end of file diff --git a/src/LargeGridPathfinding/Content/Content.mgcb b/src/LargeGridPathfinding/Content/Content.mgcb new file mode 100644 index 0000000..9f2147c --- /dev/null +++ b/src/LargeGridPathfinding/Content/Content.mgcb @@ -0,0 +1,22 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + +#begin File1.spritefont +/importer:FontDescriptionImporter +/processor:FontDescriptionProcessor +/processorParam:PremultiplyAlpha=True +/processorParam:TextureFormat=Compressed +/build:File1.spritefont;ManoloMono.spritefont + diff --git a/src/LargeGridPathfinding/Content/File.spritefont b/src/LargeGridPathfinding/Content/File.spritefont new file mode 100644 index 0000000..bd33ecf --- /dev/null +++ b/src/LargeGridPathfinding/Content/File.spritefont @@ -0,0 +1,60 @@ + + + + + + + Arial + + + 12 + + + 0 + + + true + + + + + + + + + + + + ~ + + + + diff --git a/src/LargeGridPathfinding/Content/File1.spritefont b/src/LargeGridPathfinding/Content/File1.spritefont new file mode 100644 index 0000000..ae017fb --- /dev/null +++ b/src/LargeGridPathfinding/Content/File1.spritefont @@ -0,0 +1,60 @@ + + + + + + + ManoloMono + + + 12 + + + 0 + + + true + + + + + + + + + + + + ~ + + + + diff --git a/src/LargeGridPathfinding/Content/ManoloMono.ttf b/src/LargeGridPathfinding/Content/ManoloMono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ade15238cc0899a687caa07a9623c5725108ce4c GIT binary patch literal 9232 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 ux%RCwAhedx_G@^ft=cVqjlKqHmLr9Jg PlacedRectangles { get; } + public int Width { get; } + public int Height { get; } + + public GridFiller(int width, int height, List obstacles) + { + Width = width; + Height = height; + Grid = new int[height, width]; + PlacedRectangles = []; + + foreach (Rectangle rectangle in obstacles) + { + PlaceObstacle(rectangle); + } + } + + public void FillGrid(int? x1 = null, int? y1 = null, int? x2 = null, int? y2 = null, bool fillAll = false, IProgress? totalProgress = null, IProgress? calculatingCandidatesProgress = null, IProgress? placingCandidatesProgress = null) + { + x1 ??= 0; + y1 ??= 0; + x2 ??= Width; + y2 ??= Height; + + if (fillAll) + { + PlacedRectangles.Clear(); + for (int y = y1.Value; y < y2; y++) + { + for (int x = x1.Value; x < x2; x++) + { + if (Grid[y, x] > 0) + { + Grid[y, x] = 0; + } + } + } + } + + // Collect all valid rectangle placements + ConcurrentBag<(int x, int y, int w, int h)> candidates = []; + + int cells = Grid.GetLength(0) * Grid.GetLength(1); + int areaPlaced = 0; + int maxRectangleSize = Math.Max((Grid.GetLength(0) + Grid.GetLength(1)) / 20, 100); + int reportInterval = cells / 100; + + do + { + candidates.Clear(); + + Debug.WriteLine("Calculating candidates..."); + + // Calculate all possible rectangle placements + _ = Parallel.For(y1.Value, y2.Value, (y, loopState) => + { + for (int x = x1.Value; x < x2; x++) + { + if (Grid[y, x] == 0) + { + (int w, int h) = GetMaxRectangleSize(x, y, maxRectangleSize, maxRectangleSize); + + if (w > 0 && h > 0) + { + candidates.Add((x, y, w, h)); + } + + if (w >= maxRectangleSize && h >= maxRectangleSize) + { + loopState.Break(); + return; + } + + if (candidates.Count % reportInterval == 0) + { + calculatingCandidatesProgress?.Report((float)candidates.Count / (cells - areaPlaced)); + Debug.WriteLine($"Candidates Progress: {candidates.Count} / {cells - areaPlaced} ({(float)candidates.Count / (cells - areaPlaced):P2})"); + } + } + } + }); + + Debug.WriteLine($"Candidates: {candidates.Count}"); + Debug.WriteLine("Sorting candidates..."); + + // Sort by area (largest first) + List<(int x, int y, int w, int h)> sortedCandidates = [.. candidates]; + sortedCandidates.Sort((a, b) => (b.w * b.h).CompareTo(a.w * a.h)); + + Debug.WriteLine("Placing rectangles..."); + + int placed = 0; + int reportIntervalPlacing = Math.Max(sortedCandidates.Count / 10, 1); + + // Place rectangles, ensuring no overlap + foreach ((int x, int y, int w, int h) in sortedCandidates) + { + // Check if area is still free + if (IsAreaFree(x, y, w, h)) + { + Rectangle rectangle = new Rectangle(x, y, w, h); + PlaceRectangle(rectangle); + areaPlaced += w * h; + } + + placed++; + + if (placed % reportIntervalPlacing == 0) + { + totalProgress?.Report((float)areaPlaced / cells); + placingCandidatesProgress?.Report((float)placed / (sortedCandidates.Count + 1)); + Debug.WriteLine($"Placed Progress: {placed} / {sortedCandidates.Count} ({(float)placed / sortedCandidates.Count:P2})"); + } + } + + totalProgress?.Report((float)areaPlaced / cells); + } while (!candidates.IsEmpty); + + totalProgress?.Report(1); + calculatingCandidatesProgress?.Report(1); + placingCandidatesProgress?.Report(1); + } + + public void PlaceObstacle(Rectangle rectangle) + { + // Check area around the rectangle to update all adjacent rectangles + + int y = int.Clamp(rectangle.Y - 1, 0, Height); + int x = int.Clamp(rectangle.X - 1, 0, Width); + int h = int.Clamp(rectangle.Y + rectangle.Height + 2, 0, Height); + int w = int.Clamp(rectangle.X + rectangle.Width + 2, 0, Width); + + List removedRectangles = [new Rectangle(x, y, w - x, h - y)]; + + int obstacle = currentObstacleLabel--; + + for (int dy = y; dy < h; dy++) + { + for (int dx = x; dx < w; dx++) + { + if (Grid[dy, dx] > 0 && PlacedRectangles.ContainsKey(Grid[dy, dx])) + { + removedRectangles.Add(RemoveRectangle(Grid[dy, dx])); + } + + if (Grid[dy, dx] >= 0 && dy < rectangle.Y + rectangle.Height && dx < rectangle.X + rectangle.Width && dy >= rectangle.Y && dx >= rectangle.X) + { + Grid[dy, dx] = obstacle; + } + } + } + + if (removedRectangles.Count == 1) + { + return; + } + + int minStartX = removedRectangles.Min(r => r.X); + int minStartY = removedRectangles.Min(r => r.Y); + int maxEndX = removedRectangles.Max(r => r.X + r.Width); + int maxEndY = removedRectangles.Max(r => r.Y + r.Height); + + FillGrid(minStartX, minStartY, maxEndX, maxEndY); + + Debug.WriteLine("Placed obstacle"); + Debug.WriteLine($"Placed obstacle {rectangle.X}, {rectangle.Y}, {rectangle.Width}, {rectangle.Height}"); + Debug.WriteLine($"minX: {minStartX}, minY: {minStartY}, maxX: {maxEndX}, maxY: {maxEndY}"); + } + + public void RemoveObstacle(Rectangle rectangle) + { + // Check area around the rectangle to update all adjacent rectangles + + int y = int.Clamp(rectangle.Y - 1, 0, Height); + int x = int.Clamp(rectangle.X - 1, 0, Width); + int h = int.Clamp(rectangle.Y + rectangle.Height + 2, 0, Height); + int w = int.Clamp(rectangle.X + rectangle.Width + 2, 0, Width); + + List removedRectangles = [new Rectangle(x, y, w - x, h - y)]; + + for (int dy = y; dy < h; dy++) + { + for (int dx = x; dx < w; dx++) + { + if (Grid[dy, dx] > 0) + { + removedRectangles.Add(RemoveRectangle(Grid[dy, dx])); + } + + if (Grid[dy, dx] < 0 && dy < rectangle.Y + rectangle.Height && dx < rectangle.X + rectangle.Width && dy >= rectangle.Y && dx >= rectangle.X) + { + Grid[dy, dx] = 0; + } + } + } + + int minStartX = removedRectangles.Min(r => r.X); + int minStartY = removedRectangles.Min(r => r.Y); + int maxEndX = removedRectangles.Max(r => r.X + r.Width); + int maxEndY = removedRectangles.Max(r => r.Y + r.Height); + + FillGrid(minStartX, minStartY, maxEndX, maxEndY); + + Debug.WriteLine("Removed obstacle"); + Debug.WriteLine($"Placed obstacle {rectangle.X}, {rectangle.Y}, {rectangle.Width}, {rectangle.Height}"); + Debug.WriteLine($"minX: {minStartX}, minY: {minStartY}, maxX: {maxEndX}, maxY: {maxEndY}"); + } + + // Helper method to check if a rectangle can still be placed + private bool IsAreaFree(int x, int y, int w, int h) + { + for (int dy = 0; dy < h; dy++) + { + for (int dx = 0; dx < w; dx++) + { + if (Grid[y + dy, x + dx] != 0) // Not empty + { + return false; + } + } + } + return true; + } + + private (int, int) GetMaxRectangleSize(int startX, int startY, int limitW, int limitH) + { + int maxWidth = 0, maxHeight = 0; + + for (int x = startX; x < Width && maxWidth < limitW && Grid[startY, x] == 0; x++) + { + maxWidth++; + } + + for (int h = 1; h <= Math.Min(Height - startY, limitH); h++) + { + for (int x = startX; x < startX + maxWidth; x++) + { + if (Grid[startY + h - 1, x] != 0) + { + return (maxWidth, h - 1); + } + } + maxHeight = h; + } + + return (maxWidth, maxHeight); + } + + private void PlaceRectangle(Rectangle rectangle) + { + int label = currentLabel++; + + for (int dy = 0; dy < rectangle.Height; dy++) + { + for (int dx = 0; dx < rectangle.Width; dx++) + { + Grid[rectangle.Y + dy, rectangle.X + dx] = label; + } + } + + PlacedRectangles[label] = rectangle; + } + + private Rectangle RemoveRectangle(int label) + { + if (!PlacedRectangles.TryRemove(label, out Rectangle rectangle)) + { + return Rectangle.Empty; + } + + for (int dy = 0; dy < rectangle.Height; dy++) + { + for (int dx = 0; dx < rectangle.Width; dx++) + { + Grid[rectangle.Y + dy, rectangle.X + dx] = 0; + } + } + + return rectangle; + } +} \ No newline at end of file diff --git a/src/LargeGridPathfinding/Icon.bmp b/src/LargeGridPathfinding/Icon.bmp new file mode 100644 index 0000000000000000000000000000000000000000..2b481653e241818d2894e4ef33234eaff9aad701 GIT binary patch literal 262282 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 RJoIeLznU7AhJ`o~_y6Yy51#-4 literal 0 HcmV?d00001 diff --git a/src/LargeGridPathfinding/InputHelper.cs b/src/LargeGridPathfinding/InputHelper.cs new file mode 100644 index 0000000..4849499 --- /dev/null +++ b/src/LargeGridPathfinding/InputHelper.cs @@ -0,0 +1,31 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +using MonoGame.Extended.Input; + +namespace LargeGridPathfinding; + +internal static class InputHelper +{ + public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState) + { + Vector2 movementDirection = Vector2.Zero; + if (keyboardState.IsKeyDown(Keys.S)) + { + movementDirection += Vector2.UnitY; + } + if (keyboardState.IsKeyDown(Keys.W)) + { + movementDirection -= Vector2.UnitY; + } + if (keyboardState.IsKeyDown(Keys.A)) + { + movementDirection -= Vector2.UnitX; + } + if (keyboardState.IsKeyDown(Keys.D)) + { + movementDirection += Vector2.UnitX; + } + return movementDirection; + } +} \ No newline at end of file diff --git a/src/LargeGridPathfinding/LargeGridPathfinding.csproj b/src/LargeGridPathfinding/LargeGridPathfinding.csproj new file mode 100644 index 0000000..3ba61c7 --- /dev/null +++ b/src/LargeGridPathfinding/LargeGridPathfinding.csproj @@ -0,0 +1,35 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + enable + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + \ No newline at end of file diff --git a/src/LargeGridPathfinding/LargeGridPathfindingGame.cs b/src/LargeGridPathfinding/LargeGridPathfindingGame.cs new file mode 100644 index 0000000..7a958f3 --- /dev/null +++ b/src/LargeGridPathfinding/LargeGridPathfindingGame.cs @@ -0,0 +1,652 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +using MonoGame.Extended; +using MonoGame.Extended.Input; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace LargeGridPathfinding; + +public class LargeGridPathfindingGame : Game +{ + private readonly ConcurrentDictionary temporaryIndicators = []; + private readonly ProgressTracker progressTracker = new ProgressTracker(); + private readonly List agents = []; + private SpriteBatch spriteBatch = null!; + private SpriteBatch uiSpriteBatch = null!; + private SpriteFont uiFont = null!; + private OrthographicCamera camera = null!; + private GridFiller gridFiller = null!; + private Pathfinder pathfinder = null!; + private bool gridChanged; + private bool inputBlocked = true; + private bool showZones = true; + private bool showGrid = false; + private bool showPaths = false; + private Vector2? previousMousePosition; + + public LargeGridPathfindingGame() + { + _ = new GraphicsDeviceManager(this) + { + PreferredBackBufferWidth = 1920, + PreferredBackBufferHeight = 1080 + }; + + Content.RootDirectory = "Content"; + IsMouseVisible = true; + + Window.Title = "Large Grid Pathfinding"; + Window.AllowUserResizing = true; + } + + protected override void Initialize() + { + camera = new OrthographicCamera(GraphicsDevice) + { + MinimumZoom = 0.1f, + MaximumZoom = 2, + Zoom = 0.5f, + }; + + StartInitializationTask(); + + base.Initialize(); + } + + protected override void LoadContent() + { + spriteBatch = new SpriteBatch(GraphicsDevice); + uiSpriteBatch = new SpriteBatch(GraphicsDevice); + uiFont = Content.Load("ManoloMono"); + } + + protected override void Update(GameTime gameTime) + { + if (!IsActive) + { + return; + } + + if (pathfinder is not null) + { + // Update agent positions + foreach (Agent agent in agents) + { + if (agent.Path is null || agent.Path.Count < 2) + { + continue; + } + + if (Vector2.Distance(agent.Position, agent.NextPosition) < 0.1) + { + if (agent.NextPosition == agent.Path[^1]) + { + agent.Path = null; + agent.Destination = null; + } + else + { + agent.NextPosition = agent.Path[agent.Path.LastIndexOf(agent.NextPosition) + 1]; + } + } + else + { + float distance = Vector2.Distance(agent.Position, agent.NextPosition); + float lerpFactor = Math.Min(10 / distance * gameTime.GetElapsedSeconds(), 1); + + Vector2 nextPosition = Vector2.Lerp(agent.Position, agent.NextPosition, lerpFactor); + + int newX = (int)Math.Round(nextPosition.X); + int newY = (int)Math.Round(nextPosition.Y); + + int[,] grid = gridFiller.Grid; + + if (grid[newY, newX] < 0) + { + agent.Path = null; + continue; + } + + agent.Position = nextPosition; + } + } + } + + MouseExtended.Update(); + KeyboardExtended.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); + } + + if (inputBlocked) + { + return; + } + + // Toggle debug options + + if (keyboardState.WasKeyPressed(Keys.Z)) + { + showZones = !showZones; + } + + if (keyboardState.WasKeyPressed(Keys.G)) + { + showGrid = !showGrid; + } + + if (keyboardState.WasKeyPressed(Keys.P)) + { + showPaths = !showPaths; + } + + if (keyboardState.WasKeyPressed(Keys.F)) + { + Debug.WriteLine("Filling grid..."); + + _ = progressTracker.AddProgress("Filling grid", out IProgress fillingGridProgressReporter); + _ = progressTracker.AddProgress("Calculating candidates", out IProgress calculatingCandidatesProgressReporter); + _ = progressTracker.AddProgress("Placing zones", out IProgress placingCandidatesProgressReporter); + + bool showZonesBefore = showZones; + bool showGridBefore = showGrid; + bool showPathsBefore = showPaths; + showZones = true; + showGrid = false; + showPaths = false; + inputBlocked = true; + + _ = Task.Run(() => + { + gridFiller.FillGrid(fillAll: true, totalProgress: fillingGridProgressReporter, calculatingCandidatesProgress: calculatingCandidatesProgressReporter, placingCandidatesProgress: placingCandidatesProgressReporter); + gridChanged = true; + inputBlocked = false; + showZones = showZonesBefore; + showGrid = showGridBefore; + showPaths = showPathsBefore; + }); + } + + // Place or remove obstacles via mouse input + + if (mouseState.IsButtonDown(MouseButton.Left)) + { + Vector2 mousePosition = camera.ScreenToWorld(mouseState.Position.ToVector2()); + Vector2 gridPosition = new Vector2((int)mousePosition.X / 10, (int)mousePosition.Y / 10); + + if (gridPosition.X < 0 || gridPosition.X >= gridFiller.Width || gridPosition.Y < 0 || gridPosition.Y >= gridFiller.Height || gridFiller.Grid[(int)gridPosition.Y, (int)gridPosition.X] < 0 || temporaryIndicators.ContainsKey(gridPosition)) + { + return; + } + + Vector2? previousGridPosition = null; + if (previousMousePosition is not null) + { + previousGridPosition = new Vector2((int)previousMousePosition.Value.X / 10, (int)previousMousePosition.Value.Y / 10); + } + + // Place temporary indicators to show the obstacle is scheduled for placement + temporaryIndicators[gridPosition] = Color.Red; + + previousMousePosition = mousePosition; + + _ = Task.Run(() => + { + if (previousGridPosition is not null) + { + List interpolatedPositions = []; + + // Interpolate between previous and current mouse position to place obstacles in a straight line to prevent gaps + float distance = Vector2.Distance(previousGridPosition.Value, gridPosition); + float lerpSteps = distance * 10; + + for (float t = 0; t <= 1; t += 1 / lerpSteps) + { + Vector2 interpolatedPosition = Vector2.Lerp(previousGridPosition.Value, gridPosition, t); + interpolatedPosition.Round(); + interpolatedPositions.Add(interpolatedPosition); + temporaryIndicators[interpolatedPosition] = Color.Red; + } + + foreach (Vector2 interpolatedPosition in interpolatedPositions.Distinct()) + { + gridFiller.PlaceObstacle(new Rectangle((int)interpolatedPosition.X, (int)interpolatedPosition.Y, 1, 1)); + _ = temporaryIndicators.TryRemove(interpolatedPosition, out _); + } + } + + gridFiller.PlaceObstacle(new Rectangle((int)gridPosition.X, (int)gridPosition.Y, 1, 1)); + gridChanged = true; + _ = temporaryIndicators.TryRemove(gridPosition, out _); + }); + } + else if (mouseState.IsButtonDown(MouseButton.Right)) + { + Vector2 mousePosition = camera.ScreenToWorld(mouseState.Position.ToVector2()); + Vector2 gridPosition = new Vector2((int)mousePosition.X / 10, (int)mousePosition.Y / 10); + + if (gridPosition.X < 0 || gridPosition.X >= gridFiller.Width || gridPosition.Y < 0 || gridPosition.Y >= gridFiller.Height || gridFiller.Grid[(int)gridPosition.Y, (int)gridPosition.X] > 0 || temporaryIndicators.ContainsKey(gridPosition)) + { + return; + } + + Vector2? previousGridPosition = null; + if (previousMousePosition is not null) + { + previousGridPosition = new Vector2((int)previousMousePosition.Value.X / 10, (int)previousMousePosition.Value.Y / 10); + } + + // Place temporary indicators to show the obstacle is scheduled for removal + temporaryIndicators[gridPosition] = Color.Yellow; + + previousMousePosition = mousePosition; + + _ = Task.Run(() => + { + if (previousGridPosition is not null) + { + List interpolatedPositions = []; + + // Interpolate between previous and current mouse position to place obstacles in a straight line to prevent gaps + + float distance = Vector2.Distance(previousGridPosition.Value, gridPosition); + float lerpSteps = distance * 10; + + for (float t = 0; t <= 1; t += 1 / lerpSteps) + { + Vector2 interpolatedPosition = Vector2.Lerp(previousGridPosition.Value, gridPosition, t); + interpolatedPosition.Round(); + interpolatedPositions.Add(interpolatedPosition); + temporaryIndicators[interpolatedPosition] = Color.Yellow; + } + + foreach (Vector2 interpolatedPosition in interpolatedPositions.Distinct()) + { + gridFiller.RemoveObstacle(new Rectangle((int)interpolatedPosition.X, (int)interpolatedPosition.Y, 1, 1)); + _ = temporaryIndicators.TryRemove(interpolatedPosition, out _); + } + } + + gridFiller.RemoveObstacle(new Rectangle((int)gridPosition.X, (int)gridPosition.Y, 1, 1)); + gridChanged = true; + _ = temporaryIndicators.TryRemove(gridPosition, out _); + }); + } + else + { + previousMousePosition = null; + } + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + if (gridFiller is null) + { + return; + } + + GraphicsDevice.Clear(Color.CornflowerBlue); + + 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(gridFiller.Height, (int)(camera.BoundingRectangle.Bottom / 10) + 2); + int xEnd = Math.Min(gridFiller.Width, (int)(camera.BoundingRectangle.Right / 10) + 2); + + int[,] grid = gridFiller.Grid; + Rectangle cellRect = new Rectangle(0, 0, 10, 10); + + 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 + ]; + + // Draw grid cells + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp, transformMatrix: transformMatrix); + + spriteBatch.FillRectangle(new Rectangle(0, 0, gridFiller.Width * 10, gridFiller.Height * 10), Color.White, layerDepth: 0.4f); + + if (showZones) + { + Dictionary rectangles = new Dictionary(gridFiller.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, 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]; + + 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 (showGrid) + { + spriteBatch.DrawRectangle(cellRect, Color.Black, layerDepth: 0.2f); + } + } + } + spriteBatch.End(); + + // Draw paths in a separate batch if needed + if (showPaths) + { + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp, transformMatrix: transformMatrix); + foreach (List? path in agents.Select(a => a.Path)) + { + if (path is null) + { + 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); + + if ((start.X < camera.BoundingRectangle.Left - 10 && end.X < camera.BoundingRectangle.Left - 10) || + (start.X > camera.BoundingRectangle.Right + 10 && end.X > camera.BoundingRectangle.Right + 10) || + (start.Y < camera.BoundingRectangle.Top - 10 && end.Y < camera.BoundingRectangle.Top - 10) || + (start.Y > camera.BoundingRectangle.Bottom + 10 && end.Y > camera.BoundingRectangle.Bottom + 10)) + { + continue; + } + + spriteBatch.DrawLine(start, end, Color.Black, 2f, layerDepth: 0.1f); + spriteBatch.DrawCircle(start, 5, 10, i == 0 ? Color.Green : Color.Black, 2f, layerDepth: 0.1f); + spriteBatch.DrawCircle(end, 5, 10, i == path.Count - 2 ? Color.Red : Color.Black, 2f, layerDepth: 0.1f); + } + } + spriteBatch.End(); + } + + // Draw agents + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp, transformMatrix: transformMatrix); + + foreach (Vector2 currentPosition in agents.Select(a => a.Position)) + { + if (currentPosition.X < xStart || currentPosition.X > xEnd || currentPosition.Y < yStart || currentPosition.Y > yEnd) + { + continue; + } + + spriteBatch.DrawCircle((currentPosition + new Vector2(0.5f, 0.5f)) * 10, 5, 10, Color.Blue, 2f, layerDepth: 0.1f); + } + + spriteBatch.End(); + + // Draw UI elements + 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: {gridFiller.Width}x{gridFiller.Height}", new Vector2(10, 30), Color.Black); + uiSpriteBatch.DrawString(uiFont, $"Zones: {gridFiller.PlacedRectangles.Count}", new Vector2(10, 50), Color.Black); + uiSpriteBatch.DrawString(uiFont, $"Agents: {agents.Count}", new Vector2(10, 70), Color.Black); + + IReadOnlyList 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(10, 90 + (i * 20)), Color.Black); + } + + uiSpriteBatch.End(); + + base.Draw(gameTime); + } + + private void StartInitializationTask() + { + _ = Task.Run(() => + { + // Configuration options + int width = 1000; + int height = 1000; + int agentCount = 10000; + + bool pathRandomization = true; // Randomize path costs to prevent agents from following the same path + bool penalizeStretchedRectangles = true; // (EXPERIMENTAL) Penalize paths that go through stretched rectangles to prevent too many agents in a small area + + for (int i = 0; i < agentCount; i++) + { + int x = Random.Shared.Next(0, width - 1); + int y = Random.Shared.Next(0, height - 1); + + agents.Add(new(new Vector2(x, y))); + } + + List obstacles = []; + + Debug.WriteLine("Generating random obstacles..."); + + // Generate room/building like obstacles with walls and doors + + for (int i = 0; i < (width + height) / 2; i++) + { + int x = Random.Shared.Next(0, width - 1); + int y = Random.Shared.Next(0, height - 1); + int w = Random.Shared.Next(5, (width + height) / 100); + int h = Random.Shared.Next(5, (width + height) / 100); + + Rectangle wall1 = new(x, y, w, 1); // Top wall + Rectangle wall2 = new(x, y, 1, h); // Left wall + Rectangle wall3 = new(x + w - 1, y, 1, h); // Right wall + Rectangle wall4 = new(x, y + h - 1, w, 1); // Bottom wall + + int doorSide = Random.Shared.Next(4); // Choose a random side for the door + + if (doorSide == 0) // Bottom side + { + obstacles.Add(wall1); + obstacles.Add(wall2); + obstacles.Add(wall3); + + int doorX = x + Random.Shared.Next(1, w - 2); + Rectangle doorWall1 = new(x, y + h - 1, doorX - x, 1); + Rectangle doorWall2 = new(doorX + 2, y + h - 1, x + w - (doorX + 2), 1); + + obstacles.Add(doorWall1); + obstacles.Add(doorWall2); + } + else if (doorSide == 1) // Top side + { + obstacles.Add(wall2); + obstacles.Add(wall3); + obstacles.Add(wall4); + + int doorX = x + Random.Shared.Next(1, w - 2); + Rectangle doorWall1 = new(x, y, doorX - x, 1); + Rectangle doorWall2 = new(doorX + 2, y, x + w - (doorX + 2), 1); + + obstacles.Add(doorWall1); + obstacles.Add(doorWall2); + } + else if (doorSide == 2) // Left side + { + obstacles.Add(wall1); + obstacles.Add(wall3); + obstacles.Add(wall4); + + int doorY = y + Random.Shared.Next(1, h - 2); + Rectangle doorWall1 = new(x, y, 1, doorY - y); + Rectangle doorWall2 = new(x, doorY + 2, 1, y + h - (doorY + 2)); + + obstacles.Add(doorWall1); + obstacles.Add(doorWall2); + } + else if (doorSide == 3) // Right side + { + obstacles.Add(wall1); + obstacles.Add(wall2); + obstacles.Add(wall4); + + int doorY = y + Random.Shared.Next(1, h - 2); + Rectangle doorWall1 = new(x + w - 1, y, 1, doorY - y); + Rectangle doorWall2 = new(x + w - 1, doorY + 2, 1, y + h - (doorY + 2)); + + obstacles.Add(doorWall1); + obstacles.Add(doorWall2); + } + } + + Debug.WriteLine("Initializing grid..."); + + ProgressTracker.ProgressData progressData = progressTracker.AddProgress("Initializing grid", true, out _); + gridFiller = new GridFiller(width, height, obstacles); + progressTracker.RemoveProgress(progressData); + + Debug.WriteLine("Filling grid..."); + + _ = progressTracker.AddProgress("Filling grid", out IProgress fillingGridProgressReporter); + _ = progressTracker.AddProgress("Calculating candidates", out IProgress calculatingCandidatesProgressReporter); + _ = progressTracker.AddProgress("Placing zones", out IProgress placingCandidatesProgressReporter); + + gridFiller.FillGrid(totalProgress: fillingGridProgressReporter, calculatingCandidatesProgress: calculatingCandidatesProgressReporter, placingCandidatesProgress: placingCandidatesProgressReporter); + + pathfinder = new Pathfinder(gridFiller.PlacedRectangles, pathRandomization, penalizeStretchedRectangles); + gridChanged = true; + inputBlocked = false; + showZones = false; + showPaths = false; + + StartUpdatePathTask(); + }); + } + + // Handles pathfinding calculations in a separate task + private void StartUpdatePathTask() + { + _ = Task.Factory.StartNew(async () => + { + while (true) + { + try + { + if (gridChanged) + { + gridChanged = false; + ProgressTracker.ProgressData progressDataBuildGraph = progressTracker.AddProgress("Rebuilding graph", true, out _); + pathfinder.RebuildGraph(); + progressTracker.RemoveProgress(progressDataBuildGraph); + } + + Agent[] agentsRequirePath = agents.Where(a => a.Path is null).ToArray(); + + if (agentsRequirePath.Length != 0) + { + ProgressTracker.ProgressData progressDataPaths = progressTracker.AddProgress($"Calculating {agentsRequirePath.Length} paths", out IProgress progress); + + int pathsCalculated = 0; + int reportInterval = Math.Max(1, agentsRequirePath.Length / 10); + + // Calculate paths for agents that require a new path + _ = Parallel.For(0, agentsRequirePath.Length, i => + { + Agent agent = agentsRequirePath[i]; + + agent.Path = CalculatePath(agent.GridPosition, agent.Destination); + agent.Path ??= CalculatePath(); + agent.Position = agent.Path?[0] ?? agent.Position; + agent.NextPosition = agent.Path?[1] ?? agent.Position; + agent.Destination = agent.Path?.LastOrDefault().ToPoint(); + + int localPathsCalculated = Interlocked.Increment(ref pathsCalculated); + + if (localPathsCalculated % reportInterval == 0) + { + progress.Report((float)localPathsCalculated / agentsRequirePath.Length); + } + }); + + progressTracker.RemoveProgress(progressDataPaths); + } + + await Task.Delay(100); + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + }, TaskCreationOptions.LongRunning); + } + + private List? CalculatePath(Point? start = null, Point? goal = null) + { + int width = gridFiller.Width; + int height = gridFiller.Height; + int startX = Random.Shared.Next(0, width - 1); + int startY = Random.Shared.Next(0, height - 1); + int goalX = Random.Shared.Next(0, width - 1); + int goalY = Random.Shared.Next(0, height - 1); + + start ??= new Point(startX, startY); + goal ??= new Point(goalX, goalY); + + int[,] grid = gridFiller.Grid; + + List? path = pathfinder.FindPath(ref grid[start.Value.Y, start.Value.X], ref grid[goal.Value.Y, goal.Value.X]); + + if (path is not null) + { + // Add start and goal positions to the path because the pathfinder only returns transitions between rectangles + path.Insert(0, start.Value.ToVector2()); + path.Add(goal.Value.ToVector2()); + } + + return path; + } +} \ No newline at end of file diff --git a/src/LargeGridPathfinding/Pathfinder.cs b/src/LargeGridPathfinding/Pathfinder.cs new file mode 100644 index 0000000..786a42a --- /dev/null +++ b/src/LargeGridPathfinding/Pathfinder.cs @@ -0,0 +1,231 @@ +using Microsoft.Xna.Framework; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace LargeGridPathfinding; + +internal class Pathfinder +{ + private readonly ConcurrentDictionary rectanglesSource; + private readonly bool pathRandomization; + private readonly bool penalizeStretchedRectangles; + private Dictionary rectangleMap; + private Dictionary> adjacencyList; + + public Pathfinder(ConcurrentDictionary rectangles, bool pathRandomization = false, bool penalizeStretchedRectangles = false) + { + rectanglesSource = rectangles; + this.pathRandomization = pathRandomization; + this.penalizeStretchedRectangles = penalizeStretchedRectangles; + rectangleMap = new Dictionary(rectangles); + adjacencyList = BuildGraph(); + } + + public List? FindPath(ref int start, ref int goal) + { + PriorityQueue<(int node, List path, int cost), int> queue = new PriorityQueue<(int node, List path, int cost), int>(); + HashSet visited = []; + queue.Enqueue((start, new List { start }, 0), 0); + + if (!rectangleMap.ContainsKey(start) || !rectangleMap.ContainsKey(goal)) + { + Debug.WriteLine($"{(rectangleMap.ContainsKey(start) ? "Goal" : "Start")} rectangle not found."); + return null; + } + + while (queue.Count > 0) + { + (int node, List path, int cost) = queue.Dequeue(); + + if (node == goal) + { + // Convert rectangle path to coordinate transitions + List coordinatePath = []; + + for (int i = 0; i < path.Count - 1; i++) + { + (Vector2 entry, Vector2 exit) = GetTransitionPoints(path[i], path[i + 1], i + 2 < path.Count ? path[i + 2] : null); + coordinatePath.Add(entry); + coordinatePath.Add(exit); + } + + return coordinatePath; + } + + if (visited.Contains(node)) + { + continue; + } + + _ = visited.Add(node); + + foreach (int neighbor in adjacencyList.TryGetValue(node, out List? neighbors) ? neighbors : []) + { + if (!visited.Contains(neighbor)) + { + List newPath = new List(path) { neighbor }; + + int newCost = cost + 1; + + if (penalizeStretchedRectangles) + { + newCost += Math.Max(rectangleMap[neighbor].Width, rectangleMap[neighbor].Height) / Math.Min(rectangleMap[neighbor].Width, rectangleMap[neighbor].Height); + } + + if (pathRandomization) + { + newCost += Random.Shared.Next(-1, 2); + } + + queue.Enqueue((neighbor, newPath, newCost), newCost); + } + } + } + + return null; + } + + public void RebuildGraph() + { + rectangleMap = new Dictionary(rectanglesSource); + adjacencyList.Clear(); + adjacencyList = BuildGraph(); + } + + private static bool AreRectanglesAdjacent(Rectangle r1, Rectangle r2) + { + bool adjacentHorizontally = (r1.X + r1.Width == r2.X || r2.X + r2.Width == r1.X) && r1.Y < r2.Y + r2.Height && r1.Y + r1.Height > r2.Y; + bool adjacentVertically = (r1.Y + r1.Height == r2.Y || r2.Y + r2.Height == r1.Y) && r1.X < r2.X + r2.Width && r1.X + r1.Width > r2.X; + return adjacentHorizontally || adjacentVertically; + } + + private Vector2 GetRectangleClosestEdge(Rectangle rect, Vector2 point) + { + if (pathRandomization) + { + float randomX = Random.Shared.Next(rect.Left, rect.Right); + float randomY = Random.Shared.Next(rect.Top, rect.Bottom); + return new Vector2(randomX, randomY); + } + + // Clamp the point to the closest edge of the rectangle + float closestX = Math.Clamp(point.X, rect.Left, rect.Right - 1); + float closestY = Math.Clamp(point.Y, rect.Top, rect.Bottom - 1); + + // If point is inside the rectangle, push it to the nearest edge + if (point.X >= rect.Left && point.X < rect.Right && + point.Y >= rect.Top && point.Y < rect.Bottom) + { + float leftDistance = Math.Abs(point.X - rect.Left); + float rightDistance = Math.Abs(point.X - (rect.Right - 1)); + float topDistance = Math.Abs(point.Y - rect.Top); + float bottomDistance = Math.Abs(point.Y - (rect.Bottom - 1)); + + float minDistance = Math.Min(Math.Min(leftDistance, rightDistance), Math.Min(topDistance, bottomDistance)); + + if (minDistance == leftDistance) + { + closestX = rect.Left; + } + else if (minDistance == rightDistance) + { + closestX = rect.Right - 1; + } + else if (minDistance == topDistance) + { + closestY = rect.Top; + } + else if (minDistance == bottomDistance) + { + closestY = rect.Bottom - 1; + } + } + + return new Vector2(closestX, closestY); + } + + // Build a graph where each rectangle is a node and each edge represents an adjacency + private Dictionary> BuildGraph() + { + Dictionary> graph = []; + + foreach (KeyValuePair rect1 in rectangleMap) + { + if (!graph.ContainsKey(rect1.Key)) + { + graph[rect1.Key] = []; + } + + foreach (KeyValuePair rect2 in rectangleMap) + { + if (rect1.Key != rect2.Key && AreRectanglesAdjacent(rect1.Value, rect2.Value)) + { + graph[rect1.Key].Add(rect2.Key); + } + } + } + + return graph; + } + + // Find the transition points between two rectangles + private (Vector2 entry, Vector2 exit) GetTransitionPoints(int from, int to, int? next = null) + { + Rectangle r1 = rectangleMap[from]; + Rectangle r2 = rectangleMap[to]; + + // Find all possible valid transition points + List<(Vector2 entry, Vector2 exit)> possibleTransitions = []; + + if (r1.Right == r2.Left) + { + for (int y = Math.Max(r1.Top, r2.Top); y < Math.Min(r1.Bottom, r2.Bottom); y++) + { + possibleTransitions.Add((new Vector2(r1.Right - 1, y), new Vector2(r2.Left, y))); + } + } + else if (r2.Right == r1.Left) + { + for (int y = Math.Max(r1.Top, r2.Top); y < Math.Min(r1.Bottom, r2.Bottom); y++) + { + possibleTransitions.Add((new Vector2(r1.Left, y), new Vector2(r2.Right - 1, y))); + } + } + else if (r1.Bottom == r2.Top) + { + for (int x = Math.Max(r1.Left, r2.Left); x < Math.Min(r1.Right, r2.Right); x++) + { + possibleTransitions.Add((new Vector2(x, r1.Bottom - 1), new Vector2(x, r2.Top))); + } + } + else if (r2.Bottom == r1.Top) + { + for (int x = Math.Max(r1.Left, r2.Left); x < Math.Min(r1.Right, r2.Right); x++) + { + possibleTransitions.Add((new Vector2(x, r1.Top), new Vector2(x, r2.Bottom - 1))); + } + } + + if (possibleTransitions.Count == 0) + { + return (new Vector2(-1, -1), new Vector2(-1, -1)); + } + + // If there's a next rectangle, optimize the transition to minimize the distance + if (next is not null && rectangleMap.TryGetValue(next.Value, out Rectangle r3)) + { + (Vector2 entry, Vector2 exit) bestTransition = possibleTransitions + .OrderBy(t => Vector2.DistanceSquared(t.exit, GetRectangleClosestEdge(r3, t.exit))) + .First(); + + return bestTransition; + } + + // Default: Pick the middle transition point + return possibleTransitions[possibleTransitions.Count / 2]; + } +} \ No newline at end of file diff --git a/src/LargeGridPathfinding/Program.cs b/src/LargeGridPathfinding/Program.cs new file mode 100644 index 0000000..c75454c --- /dev/null +++ b/src/LargeGridPathfinding/Program.cs @@ -0,0 +1,2 @@ +using LargeGridPathfinding.LargeGridPathfindingGame game = new LargeGridPathfinding.LargeGridPathfindingGame(); +game.Run(); \ No newline at end of file diff --git a/src/LargeGridPathfinding/ProgressTracker.cs b/src/LargeGridPathfinding/ProgressTracker.cs new file mode 100644 index 0000000..7875ba8 --- /dev/null +++ b/src/LargeGridPathfinding/ProgressTracker.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; + +namespace LargeGridPathfinding; + +internal class ProgressTracker +{ + private readonly List progresses = []; + + public ProgressData AddProgress(string name, out IProgress progress) + { + return AddProgress(name, false, false, out progress); + } + + public ProgressData AddProgress(string name, bool indeterminate, out IProgress progress) + { + return AddProgress(name, indeterminate, false, out progress); + } + + public ProgressData AddProgress(string name, bool indeterminate, bool disableAutoRemove, out IProgress progress) + { + CustomProgress newProgress = new CustomProgress(); + ProgressData progressData = new ProgressData(name, indeterminate, disableAutoRemove); + + newProgress.ProgressChanged += (_, progress) => Progress_ProgressChanged(progressData, progress); + + lock (progresses) + { + progresses.Add(progressData); + } + + progress = newProgress; + return progressData; + } + + public void RemoveProgress(ProgressData progressData) + { + lock (progresses) + { + if (!progressData.DoNotRemove) + { + _ = progresses.Remove(progressData); + } + } + } + + public IReadOnlyList GetProgresses() + { + return progresses; + } + + private void Progress_ProgressChanged(ProgressData progressData, float progress) + { + progressData.Progress = progress; + + if (progress >= 1) + { + RemoveProgress(progressData); + } + } + + public class ProgressData(string name, bool indeterminate, bool doNotRemove) + { + public string Name { get; } = name; + public float Progress { get; set; } = 0; + public bool Indeterminate { get; } = indeterminate; + public bool DoNotRemove { get; } = doNotRemove; + } + + public class CustomProgress() : IProgress + { + public EventHandler? ProgressChanged { get; set; } + + public void Report(T value) + { + ProgressChanged?.Invoke(this, value); + } + } +} \ No newline at end of file diff --git a/src/LargeGridPathfinding/app.manifest b/src/LargeGridPathfinding/app.manifest new file mode 100644 index 0000000..64a6e2a --- /dev/null +++ b/src/LargeGridPathfinding/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + +