Senses stage 3: bounded animal memory (remember & avoid threats)
Memory component holds a small ring of entries {kind, perceived pos, confidence,
age}, capacity = brainSize x 16 (tiny brains remember nothing). On detecting a
threat the animal records it (Memory.Remember, merge-by-proximity, evict weakest);
AnimalMemorySystem forgets entries over ~2 days. While wandering, an animal steers
away from the nearest remembered threat (danger zone lingers after the predator is
out of sight). Component is general (Food/Water/Mate kinds reserved for later) and
transient (not serialized, like AI targets). Build + --check-content clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3031bf2954
commit
f60c55399b
@@ -270,6 +270,7 @@ public sealed class WorldScene : Scene
|
|||||||
var thoughts = new ThoughtSet(content);
|
var thoughts = new ThoughtSet(content);
|
||||||
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
|
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
|
||||||
UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills));
|
UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills));
|
||||||
|
UpdateSystems.Add(new AnimalMemorySystem(Context.Clock, SecondsPerDay));
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
|
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,9 +83,21 @@ public static class AnimalFactory
|
|||||||
Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)),
|
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;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Сколько записей памяти даёт единица размера мозга (объём памяти = brainSize × это).</summary>
|
||||||
|
public const float MemoryPerBrain = 16f;
|
||||||
|
|
||||||
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
|
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
|
||||||
private static int SkillSeed(Genome genome)
|
private static int SkillSeed(Genome genome)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Friflo.Engine.ECS;
|
using Friflo.Engine.ECS;
|
||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
using MrGameEng.Genetics;
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
@@ -509,6 +510,89 @@ public struct Egg : IComponent
|
|||||||
public float IncubateDays;
|
public float IncubateDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Вид запомненного: угроза (избегать), корм/вода/партнёр (идти) — расширяемо.</summary>
|
||||||
|
public enum MemoryKind : byte
|
||||||
|
{
|
||||||
|
Threat,
|
||||||
|
Food,
|
||||||
|
Water,
|
||||||
|
Mate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Запись памяти: что, где (воспринятая позиция — грубая, если по запаху), насколько уверенно
|
||||||
|
/// (падает со временем — забывание), сколько дней назад.</summary>
|
||||||
|
public struct MemoryEntry
|
||||||
|
{
|
||||||
|
public MemoryKind Kind;
|
||||||
|
public Vector2 Pos;
|
||||||
|
public float Confidence;
|
||||||
|
public float AgeDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Память особи (объём ∝ мозгу): ограниченный набор записей о замеченном (сейчас — угрозы). Чувства
|
||||||
|
/// пишут сюда воспринятое; со временем уверенность падает и запись забывается; ИИ использует память,
|
||||||
|
/// когда цель не воспринимается сейчас (избегает зоны недавнего хищника). Список — managed-ссылка.
|
||||||
|
/// </summary>
|
||||||
|
public struct Memory : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Активные записи (упорядочены по добавлению; слабейшая вытесняется при переполнении).</summary>
|
||||||
|
public List<MemoryEntry> Entries;
|
||||||
|
|
||||||
|
/// <summary>Сколько записей вмещает (∝ размеру мозга; 0 — память отсутствует).</summary>
|
||||||
|
public int Capacity;
|
||||||
|
|
||||||
|
/// <summary>Запоминает/освежает наблюдение: близкое того же вида — обновляет, иначе добавляет (с
|
||||||
|
/// вытеснением слабейшего при переполнении). Вне ёмкости (Capacity 0) — ничего не помнит.</summary>
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).</summary>
|
/// <summary>Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).</summary>
|
||||||
public static class AnimalSkills
|
public static class AnimalSkills
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
private const int GridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия)
|
private const int GridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия)
|
||||||
private const float HearingRangeMult = 1.4f; // слух шире зрения
|
private const float HearingRangeMult = 1.4f; // слух шире зрения
|
||||||
private const float SmellRangeMult = 1.8f; // запах — дальше всех (но грубый, см. Detect)
|
private const float SmellRangeMult = 1.8f; // запах — дальше всех (но грубый, см. Detect)
|
||||||
|
private const float RememberedThreatAvoidCells = 14f; // в этой зоне от памяти об угрозе зверь уходит прочь
|
||||||
private readonly SpatialGrid<ForageEntry> _plantGrid;
|
private readonly SpatialGrid<ForageEntry> _plantGrid;
|
||||||
private readonly List<ForageEntry> _plantBuf = [];
|
private readonly List<ForageEntry> _plantBuf = [];
|
||||||
private readonly SpatialGrid<NeighborEntry> _animalGrid;
|
private readonly SpatialGrid<NeighborEntry> _animalGrid;
|
||||||
@@ -409,6 +410,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
self.GetComponent<Memory>().Remember(MemoryKind.Threat, threatPos, _smellSnap);
|
||||||
brain.Action = AnimalActions.Flee;
|
brain.Action = AnimalActions.Flee;
|
||||||
brain.TargetPlant = -1;
|
brain.TargetPlant = -1;
|
||||||
brain.TargetPrey = -1;
|
brain.TargetPrey = -1;
|
||||||
@@ -491,11 +493,21 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
default:
|
default:
|
||||||
brain.Action = AnimalActions.Wander;
|
brain.Action = AnimalActions.Wander;
|
||||||
brain.TargetPlant = -1;
|
brain.TargetPlant = -1;
|
||||||
// Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом —
|
|
||||||
// мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание.
|
|
||||||
var toCenter = herdCenter - pos;
|
var toCenter = herdCenter - pos;
|
||||||
var centerDist = toCenter.Length();
|
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
|
sociability >= HerdMinSociability
|
||||||
&& herdCount > 0
|
&& herdCount > 0
|
||||||
&& centerDist > HerdSeparation * _cellSize
|
&& centerDist > HerdSeparation * _cellSize
|
||||||
@@ -526,6 +538,37 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
private float VisionRadius(int species, float vision) =>
|
private float VisionRadius(int species, float vision) =>
|
||||||
MathF.Max(1f, _animalSet[species].Def.VisionCells) * MathF.Max(0.2f, vision) * _cellSize;
|
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<Memory>(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) на особь,
|
// Центр и размер ближайшей группы соплеменников в радиусе (для когезии/безопасности). O(n) на особь,
|
||||||
// как и прочие сканы. Сам исключается по id; центр — средняя позиция соседей того же вида.
|
// как и прочие сканы. Сам исключается по id; центр — средняя позиция соседей того же вида.
|
||||||
private void HerdScan(
|
private void HerdScan(
|
||||||
@@ -2585,3 +2628,50 @@ public sealed class AnimalSkillSystem(GameClock clock, float secondsPerDay, Skil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Забывание (память): уверенность каждой записи падает со временем, состарившиеся стираются. Свежие
|
||||||
|
/// наблюдения записывает/освежает <see cref="AnimalDecisionSystem"/>; эта система только «выветривает»
|
||||||
|
/// память, давая ей конечный срок.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalMemorySystem(GameClock clock, float secondsPerDay) : QuerySystem<Memory>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user