diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 6529dd8..6716b19 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -270,6 +270,7 @@ public sealed class WorldScene : Scene var thoughts = new ThoughtSet(content); UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals)); UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills)); + UpdateSystems.Add(new AnimalMemorySystem(Context.Clock, SecondsPerDay)); UpdateSystems.Add( new AnimalRutSystem(_animals, climate, content.Defs.Get("Rut")) ); diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 1163751..1ebfe88 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -83,9 +83,21 @@ public static class AnimalFactory Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)), } ); + + // Память: объём ∝ размеру мозга (примитивные ~0 — ничего не помнят). + entity.AddComponent( + new Memory + { + Entries = [], + Capacity = (int)MathF.Round(MathF.Max(0f, traits.BrainSize) * MemoryPerBrain), + } + ); return entity; } + /// Сколько записей памяти даёт единица размера мозга (объём памяти = brainSize × это). + public const float MemoryPerBrain = 16f; + // Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам. private static int SkillSeed(Genome genome) { diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 065d5fb..6d7b81e 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Friflo.Engine.ECS; using LittleSim.Content; +using Microsoft.Xna.Framework; using MrGameEng.Genetics; namespace LittleSim.Sim; @@ -509,6 +510,89 @@ public struct Egg : IComponent public float IncubateDays; } +/// Вид запомненного: угроза (избегать), корм/вода/партнёр (идти) — расширяемо. +public enum MemoryKind : byte +{ + Threat, + Food, + Water, + Mate, +} + +/// Запись памяти: что, где (воспринятая позиция — грубая, если по запаху), насколько уверенно +/// (падает со временем — забывание), сколько дней назад. +public struct MemoryEntry +{ + public MemoryKind Kind; + public Vector2 Pos; + public float Confidence; + public float AgeDays; +} + +/// +/// Память особи (объём ∝ мозгу): ограниченный набор записей о замеченном (сейчас — угрозы). Чувства +/// пишут сюда воспринятое; со временем уверенность падает и запись забывается; ИИ использует память, +/// когда цель не воспринимается сейчас (избегает зоны недавнего хищника). Список — managed-ссылка. +/// +public struct Memory : IComponent +{ + /// Активные записи (упорядочены по добавлению; слабейшая вытесняется при переполнении). + public List Entries; + + /// Сколько записей вмещает (∝ размеру мозга; 0 — память отсутствует). + public int Capacity; + + /// Запоминает/освежает наблюдение: близкое того же вида — обновляет, иначе добавляет (с + /// вытеснением слабейшего при переполнении). Вне ёмкости (Capacity 0) — ничего не помнит. + public readonly void Remember(MemoryKind kind, Vector2 pos, float mergeDist) + { + if (Entries is null || Capacity <= 0) + { + return; + } + + var mergeSq = mergeDist * mergeDist; + for (var i = 0; i < Entries.Count; i++) + { + if (Entries[i].Kind == kind && Vector2.DistanceSquared(Entries[i].Pos, pos) <= mergeSq) + { + Entries[i] = new MemoryEntry + { + Kind = kind, + Pos = pos, + Confidence = 1f, + AgeDays = 0f, + }; + return; + } + } + + if (Entries.Count >= Capacity) + { + var weakest = 0; + for (var i = 1; i < Entries.Count; i++) + { + if (Entries[i].Confidence < Entries[weakest].Confidence) + { + weakest = i; + } + } + + Entries.RemoveAt(weakest); + } + + Entries.Add( + new MemoryEntry + { + Kind = kind, + Pos = pos, + Confidence = 1f, + AgeDays = 0f, + } + ); + } +} + /// Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса). public static class AnimalSkills { diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 9655bb1..68cf691 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -270,6 +270,7 @@ public sealed class AnimalDecisionSystem : BaseSystem private const int GridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия) private const float HearingRangeMult = 1.4f; // слух шире зрения private const float SmellRangeMult = 1.8f; // запах — дальше всех (но грубый, см. Detect) + private const float RememberedThreatAvoidCells = 14f; // в этой зоне от памяти об угрозе зверь уходит прочь private readonly SpatialGrid _plantGrid; private readonly List _plantBuf = []; private readonly SpatialGrid _animalGrid; @@ -409,6 +410,7 @@ public sealed class AnimalDecisionSystem : BaseSystem ) ) { + self.GetComponent().Remember(MemoryKind.Threat, threatPos, _smellSnap); brain.Action = AnimalActions.Flee; brain.TargetPlant = -1; brain.TargetPrey = -1; @@ -491,11 +493,21 @@ public sealed class AnimalDecisionSystem : BaseSystem default: brain.Action = AnimalActions.Wander; brain.TargetPlant = -1; - // Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом — - // мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание. var toCenter = herdCenter - pos; var centerDist = toCenter.Length(); - if ( + if (TryRememberedThreat(self, pos, out var dangerPos)) + { + // Память: вблизи места недавнего хищника зверь уходит прочь (опасная зона). + var flee = pos - dangerPos; + var d = flee.Length(); + brain.Target = + d > 0.001f + ? pos + flee / d * (_cellSize * (4f + _rng.NextSingle() * 4f)) + : pos + new Vector2(_cellSize, 0f); + } + // Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом — + // мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание. + else if ( sociability >= HerdMinSociability && herdCount > 0 && centerDist > HerdSeparation * _cellSize @@ -526,6 +538,37 @@ public sealed class AnimalDecisionSystem : BaseSystem private float VisionRadius(int species, float vision) => MathF.Max(1f, _animalSet[species].Def.VisionCells) * MathF.Max(0.2f, vision) * _cellSize; + // Ближайшая запомненная угроза в зоне избегания (для блуждания — уйти от опасного места). + private bool TryRememberedThreat(Entity self, Vector2 pos, out Vector2 threatPos) + { + threatPos = default; + if (!self.TryGetComponent(out var mem) || mem.Entries is null) + { + return false; + } + + var bestSq = RememberedThreatAvoidCells * _cellSize; + bestSq *= bestSq; + var found = false; + foreach (var e in mem.Entries) + { + if (e.Kind != MemoryKind.Threat) + { + continue; + } + + var sq = Vector2.DistanceSquared(pos, e.Pos); + if (sq < bestSq) + { + bestSq = sq; + threatPos = e.Pos; + found = true; + } + } + + return found; + } + // Центр и размер ближайшей группы соплеменников в радиусе (для когезии/безопасности). O(n) на особь, // как и прочие сканы. Сам исключается по id; центр — средняя позиция соседей того же вида. private void HerdScan( @@ -2585,3 +2628,50 @@ public sealed class AnimalSkillSystem(GameClock clock, float secondsPerDay, Skil } } } + +/// +/// Забывание (память): уверенность каждой записи падает со временем, состарившиеся стираются. Свежие +/// наблюдения записывает/освежает ; эта система только «выветривает» +/// память, давая ей конечный срок. +/// +public sealed class AnimalMemorySystem(GameClock clock, float secondsPerDay) : QuerySystem +{ + private const float ForgetPerDay = 0.5f; // уверенность падает (память живёт ~2 дня без подтверждения) + + protected override void OnUpdate() + { + var days = clock.DeltaTime / secondsPerDay; + if (days <= 0f) + { + return; + } + + foreach (var (memories, _) in Query.Chunks) + { + var m = memories.Span; + for (var i = 0; i < m.Length; i++) + { + var entries = m[i].Entries; + if (entries is null) + { + continue; + } + + for (var k = entries.Count - 1; k >= 0; k--) + { + var e = entries[k]; + e.Confidence -= ForgetPerDay * days; + e.AgeDays += days; + if (e.Confidence <= 0f) + { + entries.RemoveAt(k); + } + else + { + entries[k] = e; + } + } + } + } + } +}