Perf: spatial grid for animal neighbour queries (drops O(animals^2) AI scans)
Generalized the plant grid into SpatialGrid<T> (PlantGrid.cs -> SpatialGrid.cs) and added an animal grid. AnimalDecisionSystem rebuilds both once per frame; the animal grid entry precomputes the fields the AI needs (species, body size, eats-meat, sex, adult) so queries do not refetch components. Threat / prey / mate / herd scans now gather only neighbours from cells overlapping the vision radius instead of iterating every animal per decision. IsThreatTo/IsPreyFor take precomputed values; dead MateNeedIndex removed. Mate stays species-agnostic (unchanged behaviour). Added id tie-breaks to threat/mate selection so results are order-invariant -> fully deterministic regardless of grid iteration order. Build + check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
347fac2c1b
commit
b61c6acff3
@@ -264,11 +264,14 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
private readonly ArchetypeQuery<Transform2D, PlantGrowth, PlantOrganism> _plants;
|
private readonly ArchetypeQuery<Transform2D, PlantGrowth, PlantOrganism> _plants;
|
||||||
private readonly ArchetypeQuery<Transform2D, Corpse> _corpses;
|
private readonly ArchetypeQuery<Transform2D, Corpse> _corpses;
|
||||||
|
|
||||||
// Пространственный индекс растений (перестраивается раз в кадр) + переиспользуемый буфер запроса —
|
// Пространственные индексы (перестраиваются раз в кадр) + переиспользуемые буферы запросов: поиск
|
||||||
// поиск корма берёт только ближние растения вместо прохода по всем (O(растений) на каждое решение).
|
// корма/угрозы/добычи/партнёра/стада берёт только ближние сущности вместо прохода по всем (было
|
||||||
private const int PlantGridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия)
|
// O(растений)+O(животных) на каждое решение). В ячейке — предпосчитанные признаки (без дорефетча).
|
||||||
private readonly PlantGrid _plantGrid;
|
private const int GridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия)
|
||||||
private readonly List<PlantGrid.Entry> _plantBuf = [];
|
private readonly SpatialGrid<ForageEntry> _plantGrid;
|
||||||
|
private readonly List<ForageEntry> _plantBuf = [];
|
||||||
|
private readonly SpatialGrid<NeighborEntry> _animalGrid;
|
||||||
|
private readonly List<NeighborEntry> _animalBuf = [];
|
||||||
|
|
||||||
public AnimalDecisionSystem(
|
public AnimalDecisionSystem(
|
||||||
EntityStore store,
|
EntityStore store,
|
||||||
@@ -287,7 +290,8 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
_cellSize = cellSize;
|
_cellSize = cellSize;
|
||||||
_shore = shore;
|
_shore = shore;
|
||||||
_rng = new Random(seed);
|
_rng = new Random(seed);
|
||||||
_plantGrid = new PlantGrid(cellSize * PlantGridCells);
|
_plantGrid = new SpatialGrid<ForageEntry>(cellSize * GridCells);
|
||||||
|
_animalGrid = new SpatialGrid<NeighborEntry>(cellSize * GridCells);
|
||||||
_animals = store.Query<
|
_animals = store.Query<
|
||||||
AnimalNeeds,
|
AnimalNeeds,
|
||||||
AnimalBrain,
|
AnimalBrain,
|
||||||
@@ -330,7 +334,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
protected override void OnUpdateGroup()
|
protected override void OnUpdateGroup()
|
||||||
{
|
{
|
||||||
var delta = _clock.DeltaTime;
|
var delta = _clock.DeltaTime;
|
||||||
RebuildPlantGrid();
|
RebuildGrids();
|
||||||
foreach (
|
foreach (
|
||||||
var (needsChunk, brains, organisms, growths, transforms, entities) in _animals.Chunks
|
var (needsChunk, brains, organisms, growths, transforms, entities) in _animals.Chunks
|
||||||
)
|
)
|
||||||
@@ -521,22 +525,18 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
var sum = Vector2.Zero;
|
var sum = Vector2.Zero;
|
||||||
count = 0;
|
count = 0;
|
||||||
var radiusSq = radius * radius;
|
var radiusSq = radius * radius;
|
||||||
foreach (var (_, _, organisms, _, transforms, entities) in _animals.Chunks)
|
_animalGrid.Collect(from, radius, _animalBuf);
|
||||||
|
foreach (var e in _animalBuf)
|
||||||
{
|
{
|
||||||
var oo = organisms.Span;
|
if (e.Species != species || e.Id == selfId)
|
||||||
var tt = transforms.Span;
|
|
||||||
for (var i = 0; i < tt.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (oo[i].Species != species || entities.EntityAt(i).Id == selfId)
|
continue;
|
||||||
{
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Vector2.DistanceSquared(from, tt[i].Position) <= radiusSq)
|
if (Vector2.DistanceSquared(from, e.Pos) <= radiusSq)
|
||||||
{
|
{
|
||||||
sum += tt[i].Position;
|
sum += e.Pos;
|
||||||
count++;
|
count++;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,52 +555,46 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
{
|
{
|
||||||
var bestSq = radius * radius;
|
var bestSq = radius * radius;
|
||||||
threatPos = default;
|
threatPos = default;
|
||||||
var found = false;
|
var threatId = -1;
|
||||||
var (_, selfEatsMeat) = AnimalFactory.Diet(selfTraits, _animalSet[selfSpecies].Def);
|
var (_, selfEatsMeat) = AnimalFactory.Diet(selfTraits, _animalSet[selfSpecies].Def);
|
||||||
var selfBody = selfTraits.BodySize;
|
var selfBody = selfTraits.BodySize;
|
||||||
foreach (var (_, _, organisms, _, transforms, _) in _animals.Chunks)
|
_animalGrid.Collect(from, radius, _animalBuf);
|
||||||
|
foreach (var e in _animalBuf)
|
||||||
{
|
{
|
||||||
var oo = organisms.Span;
|
if (
|
||||||
var tt = transforms.Span;
|
e.Species == selfSpecies
|
||||||
for (var i = 0; i < tt.Length; i++)
|
|| !IsThreatTo(selfEatsMeat, selfBody, e.EatsMeat, e.BodySize)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (
|
continue;
|
||||||
oo[i].Species == selfSpecies
|
}
|
||||||
|| !IsThreatTo(selfEatsMeat, selfBody, oo[i].Species, oo[i].Traits)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sq = Vector2.DistanceSquared(from, tt[i].Position);
|
var sq = Vector2.DistanceSquared(from, e.Pos);
|
||||||
if (sq < bestSq)
|
if (sq < bestSq || (sq == bestSq && e.Id < threatId))
|
||||||
{
|
{
|
||||||
bestSq = sq;
|
bestSq = sq;
|
||||||
threatPos = tt[i].Position;
|
threatPos = e.Pos;
|
||||||
found = true;
|
threatId = e.Id;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return found;
|
return threatId >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Является ли вид-кандидат угрозой особи: он ест мясо и не мельче её. Чистая жертва (не мясоед)
|
// Является ли вид-кандидат угрозой особи: он ест мясо и не мельче её. Чистая жертва (не мясоед)
|
||||||
// бежит от любого хищника не мельче себя; сам хищник — лишь от заметно более крупного мясоеда.
|
// бежит от любого хищника не мельче себя; сам хищник — лишь от заметно более крупного мясоеда.
|
||||||
private bool IsThreatTo(
|
private static bool IsThreatTo(
|
||||||
bool selfEatsMeat,
|
bool selfEatsMeat,
|
||||||
float selfBody,
|
float selfBody,
|
||||||
int otherSpecies,
|
bool otherEatsMeat,
|
||||||
in AnimalPhenotype otherTraits
|
float otherBody
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var (_, otherEatsMeat) = AnimalFactory.Diet(otherTraits, _animalSet[otherSpecies].Def);
|
|
||||||
if (!otherEatsMeat)
|
if (!otherEatsMeat)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var otherBody = otherTraits.BodySize;
|
|
||||||
return selfEatsMeat ? otherBody > selfBody * 1.1f : otherBody >= selfBody * 0.9f;
|
return selfEatsMeat ? otherBody > selfBody * 1.1f : otherBody >= selfBody * 0.9f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,35 +641,25 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Живая добыча.
|
// 2) Живая добыча (из сетки животных).
|
||||||
var bestPreySq = radius * radius;
|
var bestPreySq = radius * radius;
|
||||||
var preyId = -1;
|
var preyId = -1;
|
||||||
var preyPos = default(Vector2);
|
var preyPos = default(Vector2);
|
||||||
var selfBody = selfTraits.BodySize;
|
var selfBody = selfTraits.BodySize;
|
||||||
foreach (var (_, _, organisms, _, transforms, entities) in _animals.Chunks)
|
_animalGrid.Collect(from, radius, _animalBuf);
|
||||||
|
foreach (var e in _animalBuf)
|
||||||
{
|
{
|
||||||
var oo = organisms.Span;
|
if (e.Species == selfSpecies || e.Id == selfId || !IsPreyFor(selfBody, e.BodySize))
|
||||||
var tt = transforms.Span;
|
|
||||||
for (var i = 0; i < tt.Length; i++)
|
|
||||||
{
|
{
|
||||||
var id = entities.EntityAt(i).Id;
|
continue;
|
||||||
if (oo[i].Species == selfSpecies || id == selfId)
|
}
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsPreyFor(selfBody, oo[i].Traits))
|
var sq = Vector2.DistanceSquared(from, e.Pos);
|
||||||
{
|
if (sq < bestPreySq || (sq == bestPreySq && e.Id < preyId))
|
||||||
continue;
|
{
|
||||||
}
|
bestPreySq = sq;
|
||||||
|
preyId = e.Id;
|
||||||
var sq = Vector2.DistanceSquared(from, tt[i].Position);
|
preyPos = e.Pos;
|
||||||
if (sq < bestPreySq || (sq == bestPreySq && id < preyId))
|
|
||||||
{
|
|
||||||
bestPreySq = sq;
|
|
||||||
preyId = id;
|
|
||||||
preyPos = tt[i].Position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,8 +685,8 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
|
|
||||||
// Подходит ли особь в добычу охотнику данного размера: важно лишь, что она не крупнее охотника
|
// Подходит ли особь в добычу охотнику данного размера: важно лишь, что она не крупнее охотника
|
||||||
// (на превосходящих не нападаем). Вид/мясоедство добычи не важны — хищник ест и травоядных, и прочих.
|
// (на превосходящих не нападаем). Вид/мясоедство добычи не важны — хищник ест и травоядных, и прочих.
|
||||||
private static bool IsPreyFor(float hunterBody, in AnimalPhenotype otherTraits) =>
|
private static bool IsPreyFor(float hunterBody, float otherBody) =>
|
||||||
otherTraits.BodySize <= hunterBody * 1.25f;
|
otherBody <= hunterBody * 1.25f;
|
||||||
|
|
||||||
// Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм).
|
// Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм).
|
||||||
// Порог интеллекта, с которого травоядное РАЗБИРАЕТ корм (избегает ядовитого/невкусного). Ниже —
|
// Порог интеллекта, с которого травоядное РАЗБИРАЕТ корм (избегает ядовитого/невкусного). Ниже —
|
||||||
@@ -728,8 +712,9 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
? TryFindBestPlant(from, radius, toxinTolerance, out plantId, out position)
|
? TryFindBestPlant(from, radius, toxinTolerance, out plantId, out position)
|
||||||
: TryFindPlant(from, radius, out plantId, out position);
|
: TryFindPlant(from, radius, out plantId, out position);
|
||||||
|
|
||||||
// Перестраивает пространственный индекс растений из их текущих позиций (раз в кадр, линейно).
|
// Перестраивает пространственные индексы растений и животных из текущих позиций (раз в кадр, линейно).
|
||||||
private void RebuildPlantGrid()
|
// Для животных СРАЗУ считает признаки, нужные ИИ (мясоед/взрослость), чтобы запросы их не пересчитывали.
|
||||||
|
private void RebuildGrids()
|
||||||
{
|
{
|
||||||
_plantGrid.Clear();
|
_plantGrid.Clear();
|
||||||
foreach (var (transforms, _, organisms, entities) in _plants.Chunks)
|
foreach (var (transforms, _, organisms, entities) in _plants.Chunks)
|
||||||
@@ -740,10 +725,36 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
{
|
{
|
||||||
ref readonly var tr = ref o[i].Traits;
|
ref readonly var tr = ref o[i].Traits;
|
||||||
_plantGrid.Add(
|
_plantGrid.Add(
|
||||||
entities.EntityAt(i).Id,
|
new ForageEntry(
|
||||||
t[i].Position,
|
entities.EntityAt(i).Id,
|
||||||
tr.Toxicity,
|
t[i].Position,
|
||||||
tr.Palatability
|
tr.Toxicity,
|
||||||
|
tr.Palatability
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_animalGrid.Clear();
|
||||||
|
foreach (var (_, _, organisms, growths, transforms, entities) in _animals.Chunks)
|
||||||
|
{
|
||||||
|
var o = organisms.Span;
|
||||||
|
var g = growths.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < t.Length; i++)
|
||||||
|
{
|
||||||
|
var sp = _animalSet[o[i].Species];
|
||||||
|
var (_, eatsMeat) = AnimalFactory.Diet(o[i].Traits, sp.Def);
|
||||||
|
_animalGrid.Add(
|
||||||
|
new NeighborEntry(
|
||||||
|
entities.EntityAt(i).Id,
|
||||||
|
t[i].Position,
|
||||||
|
o[i].Species,
|
||||||
|
o[i].Traits.BodySize,
|
||||||
|
eatsMeat,
|
||||||
|
o[i].IsMale,
|
||||||
|
AnimalFactory.IsAdult(sp, g[i].Stage)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -847,44 +858,27 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
var bestSq = radius * radius;
|
var bestSq = radius * radius;
|
||||||
mateId = -1;
|
mateId = -1;
|
||||||
position = default;
|
position = default;
|
||||||
var mateNeed = -1;
|
_animalGrid.Collect(from, radius, _animalBuf);
|
||||||
foreach (var (needsChunk, _, organisms, growths, transforms, entities) in _animals.Chunks)
|
foreach (var e in _animalBuf)
|
||||||
{
|
{
|
||||||
var nn = needsChunk.Span;
|
if (e.IsMale == selfMale || !e.IsAdult)
|
||||||
var oo = organisms.Span;
|
|
||||||
var gg = growths.Span;
|
|
||||||
var tt = transforms.Span;
|
|
||||||
for (var i = 0; i < tt.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (
|
continue; // тот же пол / не взрослый (self отсеивается по полу)
|
||||||
oo[i].IsMale == selfMale
|
}
|
||||||
|| !AnimalFactory.IsAdult(_animalSet[oo[i].Species], gg[i].Stage)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
continue; // тот же пол / не взрослый (self отсеивается по полу)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mateNeed < 0)
|
// Влечение партнёра не проверяется (учёт придёт с половым отбором); вид не фильтруется —
|
||||||
{
|
// межвидовые пары возможны, как и раньше. self отсекается совпадением пола и sq≈0.
|
||||||
mateNeed = MateNeedIndex(nn[i].Values); // влечение — самый высокий drive-кандидат
|
var sq = Vector2.DistanceSquared(from, e.Pos);
|
||||||
}
|
if (sq > 0.01f && (sq < bestSq || (sq == bestSq && e.Id < mateId)))
|
||||||
|
{
|
||||||
var sq = Vector2.DistanceSquared(from, tt[i].Position);
|
bestSq = sq;
|
||||||
if (sq > 0.01f && sq < bestSq)
|
mateId = e.Id;
|
||||||
{
|
position = e.Pos;
|
||||||
bestSq = sq;
|
|
||||||
mateId = entities.EntityAt(i).Id;
|
|
||||||
position = tt[i].Position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mateId >= 0;
|
return mateId >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Влечение партнёра отсекается в исполнении (контакт), а для выбора достаточно пола/взрослости —
|
|
||||||
// упрощённый поиск (полный учёт влечения партнёра придёт с половым отбором). Возвращает 0 (заглушка).
|
|
||||||
private static int MateNeedIndex(float[] _) => 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Запись пространственного индекса: позиция (для раскладки по ячейкам) и id (для tie-break/исключения себя).</summary>
|
||||||
|
public interface ISpatialEntry
|
||||||
|
{
|
||||||
|
Vector2 Pos { get; }
|
||||||
|
int Id { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Растение для поиска корма: позиция/id + признаки, нужные выбору (токсичность, вкусность).</summary>
|
||||||
|
public readonly record struct ForageEntry(int Id, Vector2 Pos, float Toxicity, float Palatability)
|
||||||
|
: ISpatialEntry;
|
||||||
|
|
||||||
|
/// <summary>Сосед-животное для ИИ: позиция/id + предпосчитанные признаки (вид, размер, мясоед, пол, взрослость),
|
||||||
|
/// чтобы запросы угрозы/добычи/партнёра/стада не дорефетчили компоненты.</summary>
|
||||||
|
public readonly record struct NeighborEntry(
|
||||||
|
int Id,
|
||||||
|
Vector2 Pos,
|
||||||
|
int Species,
|
||||||
|
float BodySize,
|
||||||
|
bool EatsMeat,
|
||||||
|
bool IsMale,
|
||||||
|
bool IsAdult
|
||||||
|
) : ISpatialEntry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пространственный индекс (равномерная сетка ячеек) для радиус-запросов: вместо прохода по ВСЕМ
|
||||||
|
/// сущностям на каждый запрос — только из ближних ячеек. Перестраивается раз в кадр (O(n) линейно);
|
||||||
|
/// списки ячеек берутся из пула, поэтому перестройка не аллоцирует. Обход детерминирован (вложенные
|
||||||
|
/// циклы по ячейкам + порядок вставки); выбор всё равно ломает ничьи по меньшему id — детерминизм цел.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SpatialGrid<T>
|
||||||
|
where T : struct, ISpatialEntry
|
||||||
|
{
|
||||||
|
private readonly float _cell;
|
||||||
|
private readonly Dictionary<(int, int), List<T>> _cells = new();
|
||||||
|
private readonly Stack<List<T>> _pool = new();
|
||||||
|
|
||||||
|
public SpatialGrid(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(T item)
|
||||||
|
{
|
||||||
|
var key = CellOf(item.Pos);
|
||||||
|
if (!_cells.TryGetValue(key, out var list))
|
||||||
|
{
|
||||||
|
list = _pool.Count > 0 ? _pool.Pop() : new List<T>();
|
||||||
|
_cells[key] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Собирает в <paramref name="results"/> элементы из ячеек, перекрывающих радиус (точную
|
||||||
|
/// дистанцию проверяет вызывающий). Буфер переиспользуется — без аллокаций на запрос.</summary>
|
||||||
|
public void Collect(Vector2 center, float radius, List<T> 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