Files
2026-05-21 16:04:49 +02:00

530 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Cysharp.Threading.Tasks;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IPathfinder))]
[BurstCompile]
public unsafe class PathfindingSystem : GameSystem, IInitializable, IDisposable, IUpdatable, IPathfinder, IOnWorldGenerationCompletedCallback
{
private const int MaxConcurrentQueries = 8;
[InjectService]
private World _world;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private ISignalBus _signalBus;
private Queue<PathQuery> _pendingQueries = new();
private Dictionary<int, (TilePath, PathQueryResult)> _paths = new();
private int _nextQueryHandle;
private NativeArray<AStarNode> _nodeCache;
private NativeArray<PathQuery> _runningQueries;
private NativeArray<PathQueryResult> _results;
private JobHandle? _currentJob;
private NativeStream _pathStream;
private int _version;
private int _jobAge;
public PathfindingSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
if (_nodeCache.IsCreated) _nodeCache.Dispose();
if (_runningQueries.IsCreated) _runningQueries.Dispose();
if (_results.IsCreated) _results.Dispose();
}
public UniTask OnWorldGenerationCompletedAsync(World world)
{
var tileCount = _world.Size.x * _world.Size.y;
_nodeCache = new NativeArray<AStarNode>(MaxConcurrentQueries * tileCount, Allocator.Persistent);
for (var threadIndex = 0; threadIndex < MaxConcurrentQueries; threadIndex++)
{
var slice = threadIndex * tileCount;
var tileIndex = 0;
for (var y = 0; y < _world.Size.y; y++)
for (var x = 0; x < _world.Size.x; x++)
_nodeCache[slice++] = new AStarNode
{
Index = tileIndex++,
Tile = new int2(x, y)
};
}
_runningQueries = new NativeArray<PathQuery>(MaxConcurrentQueries, Allocator.Persistent);
_results = new NativeArray<PathQueryResult>(MaxConcurrentQueries, Allocator.Persistent);
_version = 1;
return UniTask.CompletedTask;
}
public int FindPath(in TilePath path, ref PathQuery query)
{
query.Handle = _nextQueryHandle++;
_pendingQueries.Enqueue(query);
_paths.Add(query.Handle, (path, PathQueryResult.InProgress));
return query.Handle;
}
public void Update()
{
if (!CompleteCurrentJob()) return;
var queryCount = math.min(_pendingQueries.Count, MaxConcurrentQueries);
if (queryCount <= 0) return;
for (var i = 0; i < queryCount; i++) _runningQueries[i] = _pendingQueries.Dequeue();
var blockMap = new NativeArray<BlockReason>(_world.BlockMap.GetNativeArray(), Allocator.TempJob);
var fertility = new NativeArray<FertilityMapValue>(_world.Fertility.GetNativeArray(), Allocator.TempJob);
var entityIdMap = new NativeArray<EntityIdMapValue>(_world.EntityIdMap.GetNativeArray(), Allocator.TempJob);
CopyCritterIds(entityIdMap);
_pathStream = new NativeStream(queryCount, Allocator.TempJob);
_currentJob = new PathJob
{
Version = _version++,
Queries = _runningQueries,
Results = _results,
NodeCache = _nodeCache,
PathStream = _pathStream,
BlockMap = blockMap,
Fertility = fertility,
EntityIdMap = entityIdMap,
WorldSize = _world.Size
}.Schedule(queryCount, 1);
blockMap.Dispose(_currentJob.Value);
entityIdMap.Dispose(_currentJob.Value);
fertility.Dispose(_currentJob.Value);
_jobAge = 1;
}
private void OnWorldGenerationCompleted(WorldGenerationCompletedSignal worldGenerationCompleted)
{
worldGenerationCompleted.Callbacks.Add(this);
}
private bool CompleteCurrentJob()
{
if (_currentJob == null) return true;
if (!_currentJob.Value.IsCompleted && ++_jobAge < 4) return false;
_currentJob.Value.Complete();
_currentJob = null;
var reader = _pathStream.AsReader();
for (var i = 0; i < reader.ForEachCount; i++)
{
if (_results[i] != PathQueryResult.Success) continue;
var queryHandle = _runningQueries[i].Handle;
var (path, _) = _paths[queryHandle];
reader.BeginForEachIndex(i);
while (reader.RemainingItemCount > 0) path.Steps.Add(reader.Read<int2>());
reader.EndForEachIndex();
}
_pathStream.Dispose();
_pathStream = default;
for (var i = 0; i < _runningQueries.Length; i++) _paths[_runningQueries[i].Handle] = (default, _results[i]);
return true;
}
private void CopyCritterIds(NativeArray<EntityIdMapValue> entityIdMap)
{
foreach (var critter in _entityCache.GetCritterAgents())
{
ref var critterState = ref critter.GetCritterStateRW();
if (critterState.LockId != Entity.InvalidId) continue;
var tile = _tileSpace.WorldToTile(critter.Position);
var index = math.mad(tile.y, _world.Size.x, tile.x);
ref var value = ref UnsafeUtility.ArrayElementAsRef<EntityIdMapValue>(entityIdMap.GetUnsafePtr(), index);
value.CritterHerdId = critterState.CritterDefinitionId;
}
}
public PathQueryResult CompletePath(int queryHandle)
{
if (!_paths.TryGetValue(queryHandle, out var result)) return PathQueryResult.InvalidQuery;
if (result.Item2 != PathQueryResult.InProgress) _paths.Remove(queryHandle);
return result.Item2;
}
private struct AStarNode
{
public int Index;
public int2 Tile;
public int Version;
public int Parent;
public int G;
public bool IsClosed;
public void Clear(int version)
{
Version = version;
Parent = -1;
G = int.MaxValue;
IsClosed = false;
}
}
[BurstCompile]
private struct PathJob : IJobParallelFor
{
private const int StraightStepCost = 10;
private const int DiagonalStepCost = 14;
public int Version;
[ReadOnly]
public NativeArray<PathQuery> Queries;
[WriteOnly]
public NativeArray<PathQueryResult> Results;
[NativeDisableParallelForRestriction]
public NativeArray<AStarNode> NodeCache;
[NativeDisableParallelForRestriction]
public NativeStream PathStream;
[ReadOnly]
public NativeArray<BlockReason> BlockMap;
[ReadOnly]
public NativeArray<FertilityMapValue> Fertility;
[ReadOnly]
public NativeArray<EntityIdMapValue> EntityIdMap;
public int2 WorldSize;
[BurstCompile]
public void Execute(int index)
{
var query = Queries[index];
var tileCount = WorldSize.x * WorldSize.y;
var nodes = new NativeSlice<AStarNode>(NodeCache, tileCount * index, tileCount);
var sourceIndex = GetNodeIndex(query.SourceTile);
ref var source = ref GetNodeRW(nodes, sourceIndex);
if (IsGoal(query, source))
{
Results[index] = PathQueryResult.Success;
return;
}
source.Clear(Version);
source.G = 0;
var open = new NativePriorityQueue((int)(0.2f * tileCount), Allocator.Temp);
open.Add(sourceIndex, GetHScore(query.SourceTile, query.DestinationRect, query.SearchType));
var directions = (query.TraversalRules & PathTraversalRules.CardinalDirectionsOnly) != 0 ? DirectionVectors.Directions4 : DirectionVectors.Directions8;
var pathFound = false;
while (open.TryPop(out var currentIndex))
{
ref var current = ref GetNodeRW(nodes, currentIndex);
current.IsClosed = true;
foreach (var direction in directions)
{
var neighborTile = current.Tile + direction;
if (!ValidateTile(query.SourceTile, neighborTile, query.MaxDistance)) continue;
ref var neighbor = ref GetNodeRW(nodes, neighborTile);
if (neighbor.Version < Version) neighbor.Clear(Version);
if (neighbor.IsClosed) continue;
var moveScore = EvaluateMove(currentIndex, neighbor.Index, query.TargetId, query.TraversalRules);
if (moveScore == int.MaxValue) continue;
if (IsGoal(query, neighbor))
{
neighbor.Parent = currentIndex;
WritePath(index, query, nodes, neighbor);
pathFound = true;
break;
}
var tentativeG = current.G + moveScore;
if (tentativeG >= neighbor.G) continue;
neighbor.Parent = currentIndex;
neighbor.G = tentativeG;
var f = tentativeG + GetHScore(neighborTile, query.DestinationRect, query.SearchType);
open.Add(neighbor.Index, f);
}
if (pathFound) break;
}
Results[index] = pathFound ? PathQueryResult.Success : PathQueryResult.Failure;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsGoal(in PathQuery query, in AStarNode node)
{
if (query.SearchType == PathSearchType.FindPath && query.DestinationRect.Contains(node.Tile)) return true;
var entityIdValue = EntityIdMap[node.Index];
switch (query.SearchType)
{
case PathSearchType.SearchResourceNode:
return entityIdValue.ResourceNodeId == query.TargetId;
case PathSearchType.SearchFertileTile:
var fertility = Fertility[node.Index];
return fertility.MaxFertility > 0 && fertility.CurrentFertility >= fertility.MaxFertility && fertility.LockId == Entity.InvalidId;
case PathSearchType.SearchCritterHerd:
return entityIdValue.CritterHerdId == query.TargetId;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ValidateTile(int2 pathSource, int2 tile, int maxDistance)
{
return math.all(tile >= 0) && math.all(tile < WorldSize) && (maxDistance <= 0 || TileMath.StepCount(pathSource, tile) <= maxDistance);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetNodeIndex(int2 tile)
{
return math.mad(tile.y, WorldSize.x, tile.x);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref AStarNode GetNodeRW(in NativeSlice<AStarNode> nodes, int index)
{
return ref UnsafeUtility.ArrayElementAsRef<AStarNode>(nodes.GetUnsafePtr(), index);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref AStarNode GetNodeRW(in NativeSlice<AStarNode> nodes, int2 tile)
{
return ref GetNodeRW(nodes, GetNodeIndex(tile));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int EvaluateMove(int leavingIndex, int enteringIndex, int targetId, PathTraversalRules rules)
{
var enteringBuildingId = EntityIdMap[enteringIndex].BuildingId;
if (enteringBuildingId != Entity.InvalidId)
{
var leavingBuildingId = EntityIdMap[leavingIndex].BuildingId;
if (enteringBuildingId == leavingBuildingId || (enteringBuildingId == targetId && leavingBuildingId == Entity.InvalidId)) return 0;
}
var blockReason = BlockMap[enteringIndex];
var blocksAgents = (blockReason & BlockReason.BlocksAgents) != 0;
if (blocksAgents) return int.MaxValue;
var roadsOnly = (rules & PathTraversalRules.RoadsOnly) != 0;
var isRoad = (blockReason & BlockReason.Road) != 0;
if (roadsOnly && !isRoad) return int.MaxValue;
var fertileTilesOnly = (rules & PathTraversalRules.FertileTilesOnly) != 0;
if (fertileTilesOnly && Fertility[enteringIndex].MaxFertility <= 0) return int.MaxValue;
return StepCost(leavingIndex, enteringIndex);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int StepCost(int leavingIndex, int enteringIndex)
{
var delta = math.abs(leavingIndex - enteringIndex);
return delta == 1 || delta == WorldSize.x ? StraightStepCost : DiagonalStepCost;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetHScore(int2 tile, TileRect rect, PathSearchType searchType)
{
return searchType switch
{
PathSearchType.FindPath => StraightStepCost * TileMath.StepCount(tile, rect),
_ => 0
};
}
private void WritePath(int threadIndex, in PathQuery query, in NativeSlice<AStarNode> nodes, in AStarNode current)
{
var temp = new NativeList<int2>(Allocator.Temp);
while (true)
{
if ((BlockMap[current.Index] & BlockReason.BlocksAgents) == 0 || EntityIdMap[current.Index].BuildingId == query.TargetId) temp.Add(current.Tile);
if (math.all(current.Tile == query.SourceTile)) break;
current = ref GetNodeRW(nodes, current.Parent);
}
var pathWriter = PathStream.AsWriter();
pathWriter.BeginForEachIndex(threadIndex);
for (var i = temp.Length - 1; i >= 0; i--) pathWriter.Write(temp[i]);
pathWriter.EndForEachIndex();
}
}
[BurstCompile]
private struct NativePriorityQueue : IDisposable
{
private NativeList<Item> _items;
public NativePriorityQueue(int capacity, Allocator allocator)
{
_items = new NativeList<Item>(capacity, allocator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
_items.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
_items.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(int index, int score)
{
var i = _items.Length;
_items.Add(new Item(index, score));
BubbleUp(i);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryPop(out int index)
{
var length = _items.Length;
if (length == 0)
{
index = 0;
return false;
}
index = _items[0].Index;
var last = length - 1;
_items[0] = _items[last];
_items.RemoveAt(last);
if (_items.Length > 0) BubbleDown(0);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BubbleUp(int i)
{
while (i > 0)
{
var parent = (i - 1) >> 1;
if (_items[i].Score >= _items[parent].Score) break;
(_items[i], _items[parent]) = (_items[parent], _items[i]);
i = parent;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BubbleDown(int i)
{
var count = _items.Length;
while (true)
{
var left = (i << 1) + 1;
var right = left + 1;
var smallest = i;
if (left < count && _items[left].Score < _items[smallest].Score) smallest = left;
if (right < count && _items[right].Score < _items[smallest].Score) smallest = right;
if (smallest == i) break;
(_items[i], _items[smallest]) = (_items[smallest], _items[i]);
i = smallest;
}
}
private struct Item
{
public int Index;
public int Score;
public Item(int index, int score)
{
Index = index;
Score = score;
}
}
}
}
}