Perf: spatial grid for plant foraging (drops the dominant O(plants) scan)
Plants vastly outnumber animals, and every herbivore forage decision scanned ALL plants — the dominant per-decision cost. New PlantGrid (uniform cell grid, cell lists pooled so rebuild is alloc-free) is rebuilt once per frame from plant positions; TryFindPlant/TryFindBestPlant now gather only candidates from cells overlapping the vision radius instead of iterating every plant. The entry carries position/id/toxicity/palatability so no component refetch is needed. Selection is unchanged (nearest / max-attractiveness, id tie-break), and since the result is order-invariant the behaviour is identical and deterministic. Build + check clean. Note: the animal-side neighbour scans (threat/prey/mate/herd) are still O(animals) per decision — animals are far fewer than plants, and an animal grid is a riskier change best done with a GUI run; deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2b9d1fd105
commit
15cad39240
@@ -264,6 +264,12 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
private readonly ArchetypeQuery<Transform2D, PlantGrowth, PlantOrganism> _plants;
|
||||
private readonly ArchetypeQuery<Transform2D, Corpse> _corpses;
|
||||
|
||||
// Пространственный индекс растений (перестраивается раз в кадр) + переиспользуемый буфер запроса —
|
||||
// поиск корма берёт только ближние растения вместо прохода по всем (O(растений) на каждое решение).
|
||||
private const int PlantGridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия)
|
||||
private readonly PlantGrid _plantGrid;
|
||||
private readonly List<PlantGrid.Entry> _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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Пространственный индекс растений (равномерная сетка ячеек) для радиус-запросов поиска корма: вместо
|
||||
/// прохода по ВСЕМ растениям на каждое решение травоядного (O(растений) на особь) — только растения в
|
||||
/// ближних ячейках. Перестраивается раз в кадр из позиций растений (O(растений) линейно). В ячейке лежит
|
||||
/// то, что нужно поиску — позиция, id, токсичность и вкусность — без дорефетча компонентов. Списки ячеек
|
||||
/// переиспользуются (пул), чтобы перестройка не аллоцировала. Порядок обхода детерминирован (вложенные
|
||||
/// циклы по ячейкам + порядок вставки), а выбор всё равно ломает ничьи по меньшему id — детерминизм цел.
|
||||
/// </summary>
|
||||
public sealed class PlantGrid
|
||||
{
|
||||
/// <summary>Запись растения в ячейке: id, позиция и признаки, нужные поиску корма.</summary>
|
||||
public readonly record struct Entry(int Id, Vector2 Pos, float Toxicity, float Palatability);
|
||||
|
||||
private readonly float _cell;
|
||||
private readonly Dictionary<(int, int), List<Entry>> _cells = new();
|
||||
private readonly Stack<List<Entry>> _pool = new();
|
||||
|
||||
public PlantGrid(float cellSize) => _cell = cellSize > 0f ? cellSize : 1f;
|
||||
|
||||
/// <summary>Очищает сетку (списки ячеек возвращаются в пул для переиспользования).</summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var list in _cells.Values)
|
||||
{
|
||||
list.Clear();
|
||||
_pool.Push(list);
|
||||
}
|
||||
|
||||
_cells.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Добавляет растение в его ячейку.</summary>
|
||||
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<Entry>();
|
||||
_cells[key] = list;
|
||||
}
|
||||
|
||||
list.Add(new Entry(id, pos, toxicity, palatability));
|
||||
}
|
||||
|
||||
/// <summary>Собирает в <paramref name="results"/> растения из ячеек, перекрывающих радиус (точную
|
||||
/// дистанцию проверяет вызывающий). Буфер переиспользуется — без аллокаций на запрос.</summary>
|
||||
public void Collect(Vector2 center, float radius, List<Entry> 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));
|
||||
}
|
||||
Reference in New Issue
Block a user