From 3c5d137fef866f71e229aec3136f040b765ca3df Mon Sep 17 00:00:00 2001 From: sven-n Date: Fri, 6 Sep 2024 21:55:40 +0200 Subject: [PATCH] Made pathfinding on safezone possible --- src/GameLogic/GameMapTerrain.cs | 2 +- src/GameLogic/INpcIntelligence.cs | 8 ++++ src/GameLogic/ISupportWalk.cs | 8 ++++ src/GameLogic/NPC/BasicMonsterIntelligence.cs | 8 ++++ src/GameLogic/NPC/GuardIntelligence.cs | 8 ++++ src/GameLogic/NPC/Monster.cs | 16 ++++--- src/GameLogic/NPC/NullMonsterIntelligence.cs | 3 ++ src/GameLogic/NPC/TrapIntelligenceBase.cs | 3 ++ src/GameLogic/Player.cs | 3 ++ src/Pathfinding/BaseGridNetwork.cs | 26 ++++++++++- src/Pathfinding/FullGridNetwork.cs | 4 +- src/Pathfinding/INetwork.cs | 4 +- src/Pathfinding/IPathFinder.cs | 4 +- src/Pathfinding/PathFinder.cs | 8 ++-- .../PreCalculation/PreCalculatedPathFinder.cs | 2 +- .../PreCalculation/PreCalculator.cs | 2 +- src/Pathfinding/Readme.md | 15 +++++++ src/Pathfinding/ScopedGridNetwork.cs | 4 +- .../PathFinderTest.cs | 43 +++++++++++++++++-- 19 files changed, 148 insertions(+), 23 deletions(-) diff --git a/src/GameLogic/GameMapTerrain.cs b/src/GameLogic/GameMapTerrain.cs index 09239257d..44d559f38 100644 --- a/src/GameLogic/GameMapTerrain.cs +++ b/src/GameLogic/GameMapTerrain.cs @@ -91,7 +91,7 @@ public Point GetRandomCoordinate(Point point, byte maximumRadius) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UpdateAiGridValue(byte x, byte y) { - this.AIgrid[x, y] = Convert.ToByte(this.WalkMap[x, y] && !this.SafezoneMap[x, y]); + this.AIgrid[x, y] = (byte)((this.WalkMap[x, y] ? 1 : 0) | (this.SafezoneMap[x, y] ? 0b1000_0000 : 0)); } /// diff --git a/src/GameLogic/INpcIntelligence.cs b/src/GameLogic/INpcIntelligence.cs index bc5f67b3e..964bc803f 100644 --- a/src/GameLogic/INpcIntelligence.cs +++ b/src/GameLogic/INpcIntelligence.cs @@ -17,6 +17,14 @@ public interface INpcIntelligence /// NonPlayerCharacter Npc { get; set; } + /// + /// Gets or sets a value indicating whether this instance can walk on safezone. + /// + /// + /// true if this instance can walk on safezone; otherwise, false. + /// + bool CanWalkOnSafezone { get; } + /// /// Registers a hit from an attacker. /// diff --git a/src/GameLogic/ISupportWalk.cs b/src/GameLogic/ISupportWalk.cs index 35f4ca4bf..6dbf54702 100644 --- a/src/GameLogic/ISupportWalk.cs +++ b/src/GameLogic/ISupportWalk.cs @@ -11,6 +11,14 @@ namespace MUnique.OpenMU.GameLogic; /// public interface ISupportWalk : ILocateable { + /// + /// Gets or sets a value indicating whether this instance can walk on safezone. + /// + /// + /// true if this instance can walk on safezone; otherwise, false. + /// + bool CanWalkOnSafezone { get; } + /// /// Gets a value indicating whether this instance is walking. /// diff --git a/src/GameLogic/NPC/BasicMonsterIntelligence.cs b/src/GameLogic/NPC/BasicMonsterIntelligence.cs index b248728cf..cda2728fc 100644 --- a/src/GameLogic/NPC/BasicMonsterIntelligence.cs +++ b/src/GameLogic/NPC/BasicMonsterIntelligence.cs @@ -25,6 +25,14 @@ public class BasicMonsterIntelligence : INpcIntelligence, IDisposable this.Dispose(); } + /// + /// Gets or sets a value indicating whether this instance can walk on safezone. + /// + /// + /// true if this instance can walk on safezone; otherwise, false. + /// + public bool CanWalkOnSafezone { get; protected set; } + /// public NonPlayerCharacter Npc { diff --git a/src/GameLogic/NPC/GuardIntelligence.cs b/src/GameLogic/NPC/GuardIntelligence.cs index 8a3c5961f..0e9c1abc4 100644 --- a/src/GameLogic/NPC/GuardIntelligence.cs +++ b/src/GameLogic/NPC/GuardIntelligence.cs @@ -20,6 +20,14 @@ public sealed class GuardIntelligence : BasicMonsterIntelligence { private Point _spawnPoint; + /// + /// Initializes a new instance of the class. + /// + public GuardIntelligence() + { + this.CanWalkOnSafezone = true; + } + /// public override bool CanWalkOn(Point target) { diff --git a/src/GameLogic/NPC/Monster.cs b/src/GameLogic/NPC/Monster.cs index 8fd259119..2e9feff4e 100644 --- a/src/GameLogic/NPC/Monster.cs +++ b/src/GameLogic/NPC/Monster.cs @@ -74,11 +74,15 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map, /// public bool IsWalking => this.WalkTarget != default; + /// + public bool CanWalkOnSafezone => this._intelligence.CanWalkOnSafezone; + /// /// Gets the target by which this instance was summoned by. /// public Player? SummonedBy => (this._intelligence as SummonedMonsterIntelligence)?.Owner; + /// public Point WalkTarget => this._walker.CurrentTarget; /// @@ -130,7 +134,7 @@ public async ValueTask WalkToAsync(Point target) { pathFinder = await this._pathFinderPool.GetAsync().ConfigureAwait(false); pathFinder.ResetPathFinder(); - calculatedPath = pathFinder.FindPath(this.Position, target, this.CurrentMap.Terrain.AIgrid); + calculatedPath = pathFinder.FindPath(this.Position, target, this.CurrentMap.Terrain.AIgrid, this.CanWalkOnSafezone); if (calculatedPath is null) { return false; @@ -228,11 +232,13 @@ internal async ValueTask RandomMoveAsync() return; } - var moveByMaxX = Rand.NextInt(1, this.Definition.MoveRange + 1); - var moveByMaxY = Rand.NextInt(1, this.Definition.MoveRange + 1); + var moveByX = Rand.NextInt(-this.Definition.MoveRange, this.Definition.MoveRange + 1); + var moveByY = Rand.NextInt(-this.Definition.MoveRange, this.Definition.MoveRange + 1); - byte randx = (byte)Rand.NextInt(Math.Max(0, this.Position.X - moveByMaxX), Math.Min(0xFF, this.Position.X + moveByMaxX + 1)); - byte randy = (byte)Rand.NextInt(Math.Max(0, this.Position.Y - moveByMaxY), Math.Min(0xFF, this.Position.Y + moveByMaxY + 1)); + var newX = this.Position.X + moveByX; + var newY = this.Position.Y + moveByY; + byte randx = (byte)Math.Min(0xFF, Math.Max(0, newX)); + byte randy = (byte)Math.Min(0xFF, Math.Max(0, newY)); var target = new Point(randx, randy); if (this._intelligence.CanWalkOn(target)) diff --git a/src/GameLogic/NPC/NullMonsterIntelligence.cs b/src/GameLogic/NPC/NullMonsterIntelligence.cs index 6187b1dd5..99e4efd49 100644 --- a/src/GameLogic/NPC/NullMonsterIntelligence.cs +++ b/src/GameLogic/NPC/NullMonsterIntelligence.cs @@ -20,6 +20,9 @@ public NonPlayerCharacter Npc set => this._npc = value; } + /// + public bool CanWalkOnSafezone => false; + /// public void RegisterHit(IAttacker attacker) { diff --git a/src/GameLogic/NPC/TrapIntelligenceBase.cs b/src/GameLogic/NPC/TrapIntelligenceBase.cs index 4557be435..3c8f2da70 100644 --- a/src/GameLogic/NPC/TrapIntelligenceBase.cs +++ b/src/GameLogic/NPC/TrapIntelligenceBase.cs @@ -16,6 +16,9 @@ public abstract class TrapIntelligenceBase : INpcIntelligence, IDisposable private Timer? _aiTimer; private Trap? _trap; + /// + public bool CanWalkOnSafezone => false; + /// /// CanWalkOn? /// diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index 0487df5c8..8f9dadb02 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -124,6 +124,9 @@ public Player(IGameContext gameContext) /// public ILogger Logger { get; } + /// + public bool CanWalkOnSafezone => true; + /// public bool IsWalking => this._walker.CurrentTarget != default; diff --git a/src/Pathfinding/BaseGridNetwork.cs b/src/Pathfinding/BaseGridNetwork.cs index c2e5d5f12..30175fb50 100644 --- a/src/Pathfinding/BaseGridNetwork.cs +++ b/src/Pathfinding/BaseGridNetwork.cs @@ -14,6 +14,16 @@ public abstract class BaseGridNetwork : INetwork /// private const byte UnreachableGridNodeValue = 0; + /// + /// The bit flag which marks a safezone node. + /// + private const byte SafezoneBitFlag = 0b1000_0000; + + /// + /// The bit mask for the cost of a node. + /// + private const byte CostBitMask = 0b0111_1111; + private static readonly sbyte[,] DirectionOffsets = { { 0, -1 }, @@ -37,6 +47,11 @@ public abstract class BaseGridNetwork : INetwork /// private byte[,]? _grid; + /// + /// A flag, if safezone nodes should be included in the network. + /// + private bool _includeSafezone; + /// /// Initializes a new instance of the class. /// @@ -47,11 +62,12 @@ protected BaseGridNetwork(bool allowDiagonals) } /// - public virtual bool Prepare(Point start, Point end, byte[,] grid) + public virtual bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone) { this._grid = grid; this._gridWidth = (ushort)(grid.GetUpperBound(0) + 1); this._gridHeight = (ushort)(grid.GetUpperBound(1) + 1); + this._includeSafezone = includeSafezone; return true; } @@ -74,7 +90,13 @@ public IEnumerable GetPossibleNextNodes(Node node) newX = (byte)(node.X + DirectionOffsets[i, 0]); newY = (byte)(node.Y + DirectionOffsets[i, 1]); - if (newX >= this._gridWidth || newY >= this._gridHeight || grid[newX, newY] == UnreachableGridNodeValue) + if (!this._includeSafezone && (grid[newX, newY] & SafezoneBitFlag) > 0) + { + continue; + } + + var costToNode = grid[newX, newY] & CostBitMask; + if (newX >= this._gridWidth || newY >= this._gridHeight || costToNode == UnreachableGridNodeValue) { continue; } diff --git a/src/Pathfinding/FullGridNetwork.cs b/src/Pathfinding/FullGridNetwork.cs index bab9fc633..7c9be73d1 100644 --- a/src/Pathfinding/FullGridNetwork.cs +++ b/src/Pathfinding/FullGridNetwork.cs @@ -38,14 +38,14 @@ public override Node GetNodeAt(Point position) } /// - public override bool Prepare(Point start, Point end, byte[,] grid) + public override bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone) { foreach (var node in this._nodes.Where(n => n != null)) { node.Status = NodeStatus.Undefined; } - return base.Prepare(start, end, grid); + return base.Prepare(start, end, grid, includeSafezone); } private int GetIndexOfPoint(Point position) diff --git a/src/Pathfinding/INetwork.cs b/src/Pathfinding/INetwork.cs index aabc19a27..ecc8fe691 100644 --- a/src/Pathfinding/INetwork.cs +++ b/src/Pathfinding/INetwork.cs @@ -34,9 +34,11 @@ public interface INetwork /// The two-dimensional grid. /// For each coordinate it contains the cost of traveling to it from a neighbor coordinate. /// The value of 0 means, that the coordinate is unreachable, . + /// If the highest bit of a value is set, it means it's a coordinate of a safezone. /// + /// If set to true, safezone nodes should be included in the search. /// /// If the preparations were successful and the pathfinding can proceed. /// - bool Prepare(Point start, Point end, byte[,] grid); + bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone); } \ No newline at end of file diff --git a/src/Pathfinding/IPathFinder.cs b/src/Pathfinding/IPathFinder.cs index 8133f85d9..7d8688204 100644 --- a/src/Pathfinding/IPathFinder.cs +++ b/src/Pathfinding/IPathFinder.cs @@ -20,8 +20,10 @@ internal interface IPathFinder /// The two-dimensional grid of the terrain. /// For each coordinate it contains the cost of traveling to it from a neighbor coordinate. /// The value of 0 means, that the coordinate is unreachable, . + /// If the highest bit of a value is set, it means it's a coordinate of a safezone. /// + /// If set to true, safezone nodes should be included in the search. /// The optional cancellation token to cancel the operation. /// The path between start and end, including , but excluding . - IList? FindPath(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken = default); + IList? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Pathfinding/PathFinder.cs b/src/Pathfinding/PathFinder.cs index fed93fa66..2ff260ced 100644 --- a/src/Pathfinding/PathFinder.cs +++ b/src/Pathfinding/PathFinder.cs @@ -71,13 +71,13 @@ public PathFinder(INetwork network, IPriorityQueue openList) public IHeuristic Heuristic { get; set; } = new NoHeuristic(); /// - public IList? FindPath(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken = default) + public IList? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default) { CurrentSearches.Add(1); try { var stopwatch = Stopwatch.StartNew(); - var result = this.FindPathInner(start, end, terrain, cancellationToken); + var result = this.FindPathInner(start, end, terrain, includeSafezone, cancellationToken); var elapsedMs = (double)stopwatch.ElapsedTicks / TimeSpan.TicksPerMillisecond; if (result is null) { @@ -98,7 +98,7 @@ public PathFinder(INetwork network, IPriorityQueue openList) } } - private IList? FindPathInner(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken) + private IList? FindPathInner(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken) { if (this.MaximumDistanceExceeded(start, end)) { @@ -107,7 +107,7 @@ public PathFinder(INetwork network, IPriorityQueue openList) var pathFound = false; this._openList.Clear(); - if (!this._network.Prepare(start, end, terrain)) + if (!this._network.Prepare(start, end, terrain, includeSafezone)) { return null; } diff --git a/src/Pathfinding/PreCalculation/PreCalculatedPathFinder.cs b/src/Pathfinding/PreCalculation/PreCalculatedPathFinder.cs index 7fbc03823..ce74cac6f 100644 --- a/src/Pathfinding/PreCalculation/PreCalculatedPathFinder.cs +++ b/src/Pathfinding/PreCalculation/PreCalculatedPathFinder.cs @@ -23,7 +23,7 @@ public PreCalculatedPathFinder(IEnumerable pathInfos) } /// - public IList? FindPath(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken = default) + public IList? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default) { var result = new List(); Point nextStep; diff --git a/src/Pathfinding/PreCalculation/PreCalculator.cs b/src/Pathfinding/PreCalculation/PreCalculator.cs index a6d675c60..81072b982 100644 --- a/src/Pathfinding/PreCalculation/PreCalculator.cs +++ b/src/Pathfinding/PreCalculation/PreCalculator.cs @@ -63,7 +63,7 @@ private IEnumerable FindPaths(Point start, bool[,] map, byte[,] aiGrid continue; } - var nodes = pathFinder.FindPath(new Point(x, y), start, aiGrid); + var nodes = pathFinder.FindPath(new Point(x, y), start, aiGrid, false); if (nodes is { Count: > 0 }) { var firstNode = nodes[0]; diff --git a/src/Pathfinding/Readme.md b/src/Pathfinding/Readme.md index 65f1d5da8..8625ba1f3 100644 --- a/src/Pathfinding/Readme.md +++ b/src/Pathfinding/Readme.md @@ -15,6 +15,21 @@ IndexedLinkedList working faster under real circumstances. The reason is, that it's pretty hard to get the index fast under all conditions, because the expected open list lengths and estimated costs are always different. +## Scoped + +The implementation can be used in a scoped way, which means that the pathfinder +is only used for a scoped area of the map. This is useful if you want to calculate +paths very quickly and you know that the path is only needed in a small area. +You can read about that on my blog post: [Optimized Pathfinding](https://munique.net/optimizing-pathfinding/). + +## Safezones + +Safezones are areas on the map where usually no path should be calculated, except +for special NPCs like guards. By default, the pathfinder will not calculate paths +on the safezone tiles. You can change this behavior by passing the parameter +`includeSafezone`. The safezones are encoded into the grid cost values as the +highest bit. + ## Pre-Calculation In the sub-folder PreCalculation includes a pathfinder which makes use of diff --git a/src/Pathfinding/ScopedGridNetwork.cs b/src/Pathfinding/ScopedGridNetwork.cs index e8aec83bd..0f88a68b0 100644 --- a/src/Pathfinding/ScopedGridNetwork.cs +++ b/src/Pathfinding/ScopedGridNetwork.cs @@ -56,7 +56,7 @@ public ScopedGridNetwork(bool allowDiagonals = true, byte maximumSegmentSideLeng } /// - public override bool Prepare(Point start, Point end, byte[,] grid) + public override bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone) { var diffX = Math.Abs(end.X - start.X); var diffY = Math.Abs(end.Y - start.Y); @@ -96,7 +96,7 @@ public override bool Prepare(Point start, Point end, byte[,] grid) } } - return base.Prepare(start, end, grid); + return base.Prepare(start, end, grid, includeSafezone); byte GetOffset(byte avgValue, int gridSize) { diff --git a/tests/MUnique.OpenMU.Pathfinding.Tests/PathFinderTest.cs b/tests/MUnique.OpenMU.Pathfinding.Tests/PathFinderTest.cs index 1d4ca7627..4a3bb027b 100644 --- a/tests/MUnique.OpenMU.Pathfinding.Tests/PathFinderTest.cs +++ b/tests/MUnique.OpenMU.Pathfinding.Tests/PathFinderTest.cs @@ -30,6 +30,15 @@ public void SetUp() } } + // Safezone: + for (int x = 50; x < 100; x++) + { + for (int y = 50; y < 100; y++) + { + this._grid[x, y] = 0b1000_0001; + } + } + this._pathFinder = new PathFinder(new ScopedGridNetwork()); } @@ -41,7 +50,7 @@ public void TestStraightPath() { var start = new Point(110, 100); var end = new Point(115, 100); - var result = this._pathFinder.FindPath(start, end, this._grid); + var result = this._pathFinder.FindPath(start, end, this._grid, false); Assert.That(result, Is.Not.Null); var lastNode = result!.LastOrDefault(); Assert.That(lastNode, Is.Not.Null); @@ -49,6 +58,34 @@ public void TestStraightPath() Assert.That(lastNode.Y, Is.EqualTo(end.Y)); } + /// + /// Tests the straight path. + /// + [Test] + public void TestStraightPath_InSafezone() + { + var start = new Point(51, 60); + var end = new Point(60, 60); + var result = this._pathFinder.FindPath(start, end, this._grid, true); + Assert.That(result, Is.Not.Null); + var lastNode = result!.LastOrDefault(); + Assert.That(lastNode, Is.Not.Null); + Assert.That(lastNode.X, Is.EqualTo(end.X)); + Assert.That(lastNode.Y, Is.EqualTo(end.Y)); + } + + /// + /// Tests the straight path. + /// + [Test] + public void TestStraightPath_InSafezone_ButNotIncluded() + { + var start = new Point(51, 60); + var end = new Point(60, 60); + var result = this._pathFinder.FindPath(start, end, this._grid, false); + Assert.That(result, Is.Null); + } + /// /// Tests the diagonal path. /// @@ -57,7 +94,7 @@ public void TestDiagonalPath() { var start = new Point(100, 100); var end = new Point(110, 110); - var result = this._pathFinder.FindPath(start, end, this._grid); + var result = this._pathFinder.FindPath(start, end, this._grid, false); Assert.That(result, Is.Not.Null); Assert.That(result!.Count, Is.EqualTo(10)); for (int i = 1; i <= 10; i++) @@ -78,7 +115,7 @@ public void TestNoPathFound() { var start = new Point(110, 100); var end = new Point(115, 99); - var result = this._pathFinder.FindPath(start, end, this._grid); + var result = this._pathFinder.FindPath(start, end, this._grid, false); Assert.That(result, Is.Null); } } \ No newline at end of file