Herding / flocking via GeneSociability (boids)

Social species (deer 0.8, wolf 0.6, chicken 0.55, boar 0.3) now school together:
AnimalDecisionSystem scans nearby same-species neighbours (HerdScan) and, when
wandering, a lone social animal steers toward the herd centroid (cohesion) while
close members just mill about (separation). "Safety in numbers" shrinks the
threat-detection radius in a group, so herded prey are calmer and scatter less.
Asocial animals (sociability < 0.3) keep the old random wander. GeneSociability
+ trait; `animal` console prints insulation/sociability. Build + --check-content
clean (42 genes, 41 traits). Not GUI-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 17:02:28 +03:00
co-authored by Claude Opus 4.8
parent 5ac34f4dc7
commit c0822de650
5 changed files with 106 additions and 10 deletions
+3
View File
@@ -1208,6 +1208,9 @@ public sealed class WorldScene : Scene
$" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
+ $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
);
console.WriteLine(
$" behaviour: insulation {traits.Insulation:0.##}, sociability {traits.Sociability:0.##}"
);
}
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
+4
View File
@@ -51,6 +51,9 @@ public struct AnimalPhenotype
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
public float ToxinTolerance;
/// <summary>Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).</summary>
public float Sociability;
/// <summary>Продолжительность жизни (игровых дней).</summary>
public float Lifespan;
@@ -88,6 +91,7 @@ public struct AnimalPhenotype
Carnivory = T("carnivory"),
Omnivory = T("omnivory"),
ToxinTolerance = T("toxinTolerance"),
Sociability = T("sociability"),
Lifespan = T("lifespan"),
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"),
+85 -6
View File
@@ -232,6 +232,10 @@ public sealed class AnimalDecisionSystem : BaseSystem
{
private const float Interval = 0.6f;
private const float MateMoodFloor = 0.35f; // ниже — стресс/истощение, зверь не спаривается (A8)
private const float HerdMinSociability = 0.3f; // ниже — вид не стадный (когезия/безопасность выкл.)
private const float SafetyInNumbers = 0.5f; // макс. снижение радиуса реакции на угрозу в группе
private const int HerdFullCount = 6; // размер группы, при котором эффект «безопасность в числе» полон
private const float HerdSeparation = 1.5f; // ближе этого (в клетках) к центру — не сходимся плотнее
private readonly UtilityAi<AnimalContext> _brain;
private readonly GameClock _clock;
@@ -345,9 +349,28 @@ public sealed class AnimalDecisionSystem : BaseSystem
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent<Mood>();
// Стадность (boids): социальные виды считают центр/размер ближайшей группы соплеменников —
// для когезии при блуждании и «безопасности в числе» (спокойнее на угрозу в большой группе).
var sociability = o[i].Traits.Sociability;
var herdCount = 0;
var herdCenter = pos;
if (sociability >= HerdMinSociability)
{
HerdScan(pos, seeRadius, o[i].Species, self.Id, out herdCenter, out herdCount);
}
var threatRadius =
herdCount > 0
? senseRadius
* (
1f
- SafetyInNumbers * MathF.Min(1f, herdCount / (float)HerdFullCount)
)
: senseRadius;
// Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo).
if (TryFindThreat(pos, senseRadius, o[i].Species, o[i].Traits, out var threatPos))
if (TryFindThreat(pos, threatRadius, o[i].Species, o[i].Traits, out var threatPos))
{
brain.Action = AnimalActions.Flee;
brain.TargetPlant = -1;
@@ -431,11 +454,31 @@ public sealed class AnimalDecisionSystem : BaseSystem
default:
brain.Action = AnimalActions.Wander;
brain.TargetPlant = -1;
var angle = _rng.NextSingle() * MathF.Tau;
brain.Target =
pos
+ new Vector2(MathF.Cos(angle), MathF.Sin(angle))
* (_cellSize * (4f + _rng.NextSingle() * 6f));
// Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом —
// мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание.
var toCenter = herdCenter - pos;
var centerDist = toCenter.Length();
if (
sociability >= HerdMinSociability
&& herdCount > 0
&& centerDist > HerdSeparation * _cellSize
)
{
brain.Target =
pos
+ toCenter
/ centerDist
* (_cellSize * (3f + _rng.NextSingle() * 3f));
}
else
{
var angle = _rng.NextSingle() * MathF.Tau;
brain.Target =
pos
+ new Vector2(MathF.Cos(angle), MathF.Sin(angle))
* (_cellSize * (4f + _rng.NextSingle() * 6f));
}
break;
}
}
@@ -446,6 +489,42 @@ 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;
// Центр и размер ближайшей группы соплеменников в радиусе (для когезии/безопасности). O(n) на особь,
// как и прочие сканы. Сам исключается по id; центр — средняя позиция соседей того же вида.
private void HerdScan(
Vector2 from,
float radius,
int species,
int selfId,
out Vector2 center,
out int count
)
{
var sum = Vector2.Zero;
count = 0;
var radiusSq = radius * radius;
foreach (var (_, _, organisms, _, transforms, entities) in _animals.Chunks)
{
var oo = organisms.Span;
var tt = transforms.Span;
for (var i = 0; i < tt.Length; i++)
{
if (oo[i].Species != species || entities.EntityAt(i).Id == selfId)
{
continue;
}
if (Vector2.DistanceSquared(from, tt[i].Position) <= radiusSq)
{
sum += tt[i].Position;
count++;
}
}
}
center = count > 0 ? sum / count : from;
}
// Ближайший хищник-угроза в радиусе восприятия (для бегства жертвы). Угроза = другой вид, который
// ест мясо и достаточно крупный (см. IsThreatTo). O(n) на особь — как поиск корма.
private bool TryFindThreat(