Skip to content

Commit

Permalink
Merge pull request #489 from MUnique/dev/path-on-safezone
Browse files Browse the repository at this point in the history
Made pathfinding on safezone possible
  • Loading branch information
sven-n authored Sep 7, 2024
2 parents 8f2fd4b + 3c5d137 commit edd4cca
Show file tree
Hide file tree
Showing 19 changed files with 148 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/GameLogic/GameMapTerrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/// <summary>
Expand Down
8 changes: 8 additions & 0 deletions src/GameLogic/INpcIntelligence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public interface INpcIntelligence
/// </summary>
NonPlayerCharacter Npc { get; set; }

/// <summary>
/// Gets or sets a value indicating whether this instance can walk on safezone.
/// </summary>
/// <value>
/// <c>true</c> if this instance can walk on safezone; otherwise, <c>false</c>.
/// </value>
bool CanWalkOnSafezone { get; }

/// <summary>
/// Registers a hit from an attacker.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/GameLogic/ISupportWalk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ namespace MUnique.OpenMU.GameLogic;
/// </summary>
public interface ISupportWalk : ILocateable
{
/// <summary>
/// Gets or sets a value indicating whether this instance can walk on safezone.
/// </summary>
/// <value>
/// <c>true</c> if this instance can walk on safezone; otherwise, <c>false</c>.
/// </value>
bool CanWalkOnSafezone { get; }

/// <summary>
/// Gets a value indicating whether this instance is walking.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/GameLogic/NPC/BasicMonsterIntelligence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ public class BasicMonsterIntelligence : INpcIntelligence, IDisposable
this.Dispose();
}

/// <summary>
/// Gets or sets a value indicating whether this instance can walk on safezone.
/// </summary>
/// <value>
/// <c>true</c> if this instance can walk on safezone; otherwise, <c>false</c>.
/// </value>
public bool CanWalkOnSafezone { get; protected set; }

/// <inheritdoc/>
public NonPlayerCharacter Npc
{
Expand Down
8 changes: 8 additions & 0 deletions src/GameLogic/NPC/GuardIntelligence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public sealed class GuardIntelligence : BasicMonsterIntelligence
{
private Point _spawnPoint;

/// <summary>
/// Initializes a new instance of the <see cref="GuardIntelligence"/> class.
/// </summary>
public GuardIntelligence()
{
this.CanWalkOnSafezone = true;
}

/// <inheritdoc/>
public override bool CanWalkOn(Point target)
{
Expand Down
16 changes: 11 additions & 5 deletions src/GameLogic/NPC/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map,
/// </value>
public bool IsWalking => this.WalkTarget != default;

/// <inheritdoc />
public bool CanWalkOnSafezone => this._intelligence.CanWalkOnSafezone;

/// <summary>
/// Gets the target by which this instance was summoned by.
/// </summary>
public Player? SummonedBy => (this._intelligence as SummonedMonsterIntelligence)?.Owner;

/// <inheritdoc />
public Point WalkTarget => this._walker.CurrentTarget;

/// <inheritdoc/>
Expand Down Expand Up @@ -130,7 +134,7 @@ public async ValueTask<bool> 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;
Expand Down Expand Up @@ -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))
Expand Down
3 changes: 3 additions & 0 deletions src/GameLogic/NPC/NullMonsterIntelligence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public NonPlayerCharacter Npc
set => this._npc = value;
}

/// <inheritdoc />
public bool CanWalkOnSafezone => false;

/// <inheritdoc />
public void RegisterHit(IAttacker attacker)
{
Expand Down
3 changes: 3 additions & 0 deletions src/GameLogic/NPC/TrapIntelligenceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public abstract class TrapIntelligenceBase : INpcIntelligence, IDisposable
private Timer? _aiTimer;
private Trap? _trap;

/// <inheritdoc />
public bool CanWalkOnSafezone => false;

/// <summary>
/// CanWalkOn?
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ public Player(IGameContext gameContext)
/// <inheritdoc />
public ILogger<Player> Logger { get; }

/// <inheritdoc />
public bool CanWalkOnSafezone => true;

/// <inheritdoc />
public bool IsWalking => this._walker.CurrentTarget != default;

Expand Down
26 changes: 24 additions & 2 deletions src/Pathfinding/BaseGridNetwork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ public abstract class BaseGridNetwork : INetwork
/// </summary>
private const byte UnreachableGridNodeValue = 0;

/// <summary>
/// The bit flag which marks a safezone node.
/// </summary>
private const byte SafezoneBitFlag = 0b1000_0000;

/// <summary>
/// The bit mask for the cost of a node.
/// </summary>
private const byte CostBitMask = 0b0111_1111;

private static readonly sbyte[,] DirectionOffsets =
{
{ 0, -1 },
Expand All @@ -37,6 +47,11 @@ public abstract class BaseGridNetwork : INetwork
/// </summary>
private byte[,]? _grid;

/// <summary>
/// A flag, if safezone nodes should be included in the network.
/// </summary>
private bool _includeSafezone;

/// <summary>
/// Initializes a new instance of the <see cref="BaseGridNetwork"/> class.
/// </summary>
Expand All @@ -47,11 +62,12 @@ protected BaseGridNetwork(bool allowDiagonals)
}

/// <inheritdoc/>
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;
}

Expand All @@ -74,7 +90,13 @@ public IEnumerable<Node> 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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Pathfinding/FullGridNetwork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ public override Node GetNodeAt(Point position)
}

/// <inheritdoc/>
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)
Expand Down
4 changes: 3 additions & 1 deletion src/Pathfinding/INetwork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, <see cref="BaseGridNetwork.UnreachableGridNodeValue" />.
/// If the highest bit of a value is set, it means it's a coordinate of a safezone.
/// </param>
/// <param name="includeSafezone">If set to <c>true</c>, safezone nodes should be included in the search.</param>
/// <returns>
/// If the preparations were successful and the pathfinding can proceed.
/// </returns>
bool Prepare(Point start, Point end, byte[,] grid);
bool Prepare(Point start, Point end, byte[,] grid, bool includeSafezone);
}
4 changes: 3 additions & 1 deletion src/Pathfinding/IPathFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, <see cref="BaseGridNetwork.UnreachableGridNodeValue" />.
/// If the highest bit of a value is set, it means it's a coordinate of a safezone.
/// </param>
/// <param name="includeSafezone">If set to <c>true</c>, safezone nodes should be included in the search.</param>
/// <param name="cancellationToken">The optional cancellation token to cancel the operation.</param>
/// <returns>The path between start and end, including <paramref name="end"/>, but excluding <paramref name="start"/>.</returns>
IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken = default);
IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default);
}
8 changes: 4 additions & 4 deletions src/Pathfinding/PathFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ public PathFinder(INetwork network, IPriorityQueue<Node> openList)
public IHeuristic Heuristic { get; set; } = new NoHeuristic();

/// <inheritdoc/>
public IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken = default)
public IList<PathResultNode>? 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)
{
Expand All @@ -98,7 +98,7 @@ public PathFinder(INetwork network, IPriorityQueue<Node> openList)
}
}

private IList<PathResultNode>? FindPathInner(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken)
private IList<PathResultNode>? FindPathInner(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken)
{
if (this.MaximumDistanceExceeded(start, end))
{
Expand All @@ -107,7 +107,7 @@ public PathFinder(INetwork network, IPriorityQueue<Node> openList)

var pathFound = false;
this._openList.Clear();
if (!this._network.Prepare(start, end, terrain))
if (!this._network.Prepare(start, end, terrain, includeSafezone))
{
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Pathfinding/PreCalculation/PreCalculatedPathFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public PreCalculatedPathFinder(IEnumerable<PathInfo> pathInfos)
}

/// <inheritdoc/>
public IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, CancellationToken cancellationToken = default)
public IList<PathResultNode>? FindPath(Point start, Point end, byte[,] terrain, bool includeSafezone, CancellationToken cancellationToken = default)
{
var result = new List<PathResultNode>();
Point nextStep;
Expand Down
2 changes: 1 addition & 1 deletion src/Pathfinding/PreCalculation/PreCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private IEnumerable<PathInfo> 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];
Expand Down
15 changes: 15 additions & 0 deletions src/Pathfinding/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/Pathfinding/ScopedGridNetwork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public ScopedGridNetwork(bool allowDiagonals = true, byte maximumSegmentSideLeng
}

/// <inheritdoc/>
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);
Expand Down Expand Up @@ -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)
{
Expand Down
Loading

0 comments on commit edd4cca

Please sign in to comment.