diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 441731d..a4f73d7 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -264,6 +264,12 @@ public sealed class AnimalDecisionSystem : BaseSystem private readonly ArchetypeQuery _plants; private readonly ArchetypeQuery _corpses; + // Пространственный индекс растений (перестраивается раз в кадр) + переиспользуемый буфер запроса — + // поиск корма берёт только ближние растения вместо прохода по всем (O(растений) на каждое решение). + private const int PlantGridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия) + private readonly PlantGrid _plantGrid; + private readonly List _plantBuf = []; + public AnimalDecisionSystem( EntityStore store, GameClock clock, @@ -281,6 +287,7 @@ public sealed class AnimalDecisionSystem : BaseSystem _cellSize = cellSize; _shore = shore; _rng = new Random(seed); + _plantGrid = new PlantGrid(cellSize * PlantGridCells); _animals = store.Query< AnimalNeeds, AnimalBrain, @@ -323,6 +330,7 @@ public sealed class AnimalDecisionSystem : BaseSystem protected override void OnUpdateGroup() { var delta = _clock.DeltaTime; + RebuildPlantGrid(); foreach ( var (needsChunk, brains, organisms, growths, transforms, entities) in _animals.Chunks ) @@ -720,25 +728,42 @@ public sealed class AnimalDecisionSystem : BaseSystem ? TryFindBestPlant(from, radius, toxinTolerance, out plantId, out position) : TryFindPlant(from, radius, out plantId, out position); - // Рефлекс: ближайшее растение в радиусе (ничья — меньший id, детерминизм). + // Перестраивает пространственный индекс растений из их текущих позиций (раз в кадр, линейно). + private void RebuildPlantGrid() + { + _plantGrid.Clear(); + foreach (var (transforms, _, organisms, entities) in _plants.Chunks) + { + var t = transforms.Span; + var o = organisms.Span; + for (var i = 0; i < t.Length; i++) + { + ref readonly var tr = ref o[i].Traits; + _plantGrid.Add( + entities.EntityAt(i).Id, + t[i].Position, + tr.Toxicity, + tr.Palatability + ); + } + } + } + + // Рефлекс: ближайшее растение в радиусе (ничья — меньший id, детерминизм). Кандидаты — из сетки. private bool TryFindPlant(Vector2 from, float radius, out int plantId, out Vector2 position) { var bestSq = radius * radius; plantId = -1; position = default; - foreach (var (transforms, _, _, entities) in _plants.Chunks) + _plantGrid.Collect(from, radius, _plantBuf); + foreach (var e in _plantBuf) { - var t = transforms.Span; - for (var i = 0; i < t.Length; i++) + var sq = Vector2.DistanceSquared(from, e.Pos); + if (sq < bestSq || (sq == bestSq && e.Id < plantId)) { - var sq = Vector2.DistanceSquared(from, t[i].Position); - var id = entities.EntityAt(i).Id; - if (sq < bestSq || (sq == bestSq && id < plantId)) - { - bestSq = sq; - plantId = id; - position = t[i].Position; - } + bestSq = sq; + plantId = e.Id; + position = e.Pos; } } @@ -762,31 +787,25 @@ public sealed class AnimalDecisionSystem : BaseSystem var bestScore = float.NegativeInfinity; plantId = -1; position = default; - foreach (var (transforms, _, organisms, entities) in _plants.Chunks) + _plantGrid.Collect(from, radius, _plantBuf); + foreach (var e in _plantBuf) { - var t = transforms.Span; - var o = organisms.Span; - for (var i = 0; i < t.Length; i++) + var sq = Vector2.DistanceSquared(from, e.Pos); + if (sq > radiusSq) { - var sq = Vector2.DistanceSquared(from, t[i].Position); - if (sq > radiusSq) - { - continue; - } + continue; + } - ref readonly var tr = ref o[i].Traits; - var perceivedToxin = tr.Toxicity * (1f - tol); - var score = - tr.Palatability - - ToxinAvoidWeight * perceivedToxin - - ForageDistanceWeight * (radius > 0f ? MathF.Sqrt(sq) / radius : 0f); - var id = entities.EntityAt(i).Id; - if (score > bestScore || (score == bestScore && id < plantId)) - { - bestScore = score; - plantId = id; - position = t[i].Position; - } + var perceivedToxin = e.Toxicity * (1f - tol); + var score = + e.Palatability + - ToxinAvoidWeight * perceivedToxin + - ForageDistanceWeight * (radius > 0f ? MathF.Sqrt(sq) / radius : 0f); + if (score > bestScore || (score == bestScore && e.Id < plantId)) + { + bestScore = score; + plantId = e.Id; + position = e.Pos; } } diff --git a/src/LittleSim/Sim/PlantGrid.cs b/src/LittleSim/Sim/PlantGrid.cs new file mode 100644 index 0000000..6eca745 --- /dev/null +++ b/src/LittleSim/Sim/PlantGrid.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace LittleSim.Sim; + +/// +/// Пространственный индекс растений (равномерная сетка ячеек) для радиус-запросов поиска корма: вместо +/// прохода по ВСЕМ растениям на каждое решение травоядного (O(растений) на особь) — только растения в +/// ближних ячейках. Перестраивается раз в кадр из позиций растений (O(растений) линейно). В ячейке лежит +/// то, что нужно поиску — позиция, id, токсичность и вкусность — без дорефетча компонентов. Списки ячеек +/// переиспользуются (пул), чтобы перестройка не аллоцировала. Порядок обхода детерминирован (вложенные +/// циклы по ячейкам + порядок вставки), а выбор всё равно ломает ничьи по меньшему id — детерминизм цел. +/// +public sealed class PlantGrid +{ + /// Запись растения в ячейке: id, позиция и признаки, нужные поиску корма. + public readonly record struct Entry(int Id, Vector2 Pos, float Toxicity, float Palatability); + + private readonly float _cell; + private readonly Dictionary<(int, int), List> _cells = new(); + private readonly Stack> _pool = new(); + + public PlantGrid(float cellSize) => _cell = cellSize > 0f ? cellSize : 1f; + + /// Очищает сетку (списки ячеек возвращаются в пул для переиспользования). + public void Clear() + { + foreach (var list in _cells.Values) + { + list.Clear(); + _pool.Push(list); + } + + _cells.Clear(); + } + + /// Добавляет растение в его ячейку. + public void Add(int id, Vector2 pos, float toxicity, float palatability) + { + var key = CellOf(pos); + if (!_cells.TryGetValue(key, out var list)) + { + list = _pool.Count > 0 ? _pool.Pop() : new List(); + _cells[key] = list; + } + + list.Add(new Entry(id, pos, toxicity, palatability)); + } + + /// Собирает в растения из ячеек, перекрывающих радиус (точную + /// дистанцию проверяет вызывающий). Буфер переиспользуется — без аллокаций на запрос. + public void Collect(Vector2 center, float radius, List results) + { + results.Clear(); + var minX = (int)MathF.Floor((center.X - radius) / _cell); + var maxX = (int)MathF.Floor((center.X + radius) / _cell); + var minY = (int)MathF.Floor((center.Y - radius) / _cell); + var maxY = (int)MathF.Floor((center.Y + radius) / _cell); + for (var cx = minX; cx <= maxX; cx++) + { + for (var cy = minY; cy <= maxY; cy++) + { + if (_cells.TryGetValue((cx, cy), out var list)) + { + results.AddRange(list); + } + } + } + } + + private (int, int) CellOf(Vector2 p) => + ((int)MathF.Floor(p.X / _cell), (int)MathF.Floor(p.Y / _cell)); +}