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 _pendingQueries = new(); private Dictionary _paths = new(); private int _nextQueryHandle; private NativeArray _nodeCache; private NativeArray _runningQueries; private NativeArray _results; private JobHandle? _currentJob; private NativeStream _pathStream; private int _version; private int _jobAge; public PathfindingSystem(IServiceLocator serviceLocator) : base(serviceLocator) { } public UniTask InitializeAsync() { _signalBus.Subscribe(OnWorldGenerationCompleted); return UniTask.CompletedTask; } public void Dispose() { _signalBus.Unsubscribe(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(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(MaxConcurrentQueries, Allocator.Persistent); _results = new NativeArray(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(_world.BlockMap.GetNativeArray(), Allocator.TempJob); var fertility = new NativeArray(_world.Fertility.GetNativeArray(), Allocator.TempJob); var entityIdMap = new NativeArray(_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()); 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 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(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 Queries; [WriteOnly] public NativeArray Results; [NativeDisableParallelForRestriction] public NativeArray NodeCache; [NativeDisableParallelForRestriction] public NativeStream PathStream; [ReadOnly] public NativeArray BlockMap; [ReadOnly] public NativeArray Fertility; [ReadOnly] public NativeArray EntityIdMap; public int2 WorldSize; [BurstCompile] public void Execute(int index) { var query = Queries[index]; var tileCount = WorldSize.x * WorldSize.y; var nodes = new NativeSlice(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 nodes, int index) { return ref UnsafeUtility.ArrayElementAsRef(nodes.GetUnsafePtr(), index); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref AStarNode GetNodeRW(in NativeSlice 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 nodes, in AStarNode current) { var temp = new NativeList(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 _items; public NativePriorityQueue(int capacity, Allocator allocator) { _items = new NativeList(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; } } } } }