Phase B: plant genome drives growth via environment suitability

Bump the engine submodule to d0104df (Suitability.Gaussian). Each plant now
carries a diploid PlantGenome (allele pairs, phenotype = allele average) seeded
from a per-species GenomeDef baseline at spawn. PlantGrowthSystem replaces the
flat fertility multiplier with vigor times a product of Gaussian suitabilities
for light (day/night), temperature (climate season) and the cell's fertility —
so plants grow faster in their preferred light/warmth/soil and crawl outside it.
Genomes are visible and editable in the ECS inspector. plants.json carries the
species genomes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 17:52:27 +03:00
co-authored by Claude Opus 4.8
parent aff5a947c5
commit bfe1b139f0
8 changed files with 176 additions and 16 deletions
+36 -1
View File
@@ -63,7 +63,39 @@ public sealed class PlantGrowthStage
public string? Label { get; init; }
}
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста.</summary>
/// <summary>
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
/// (разброс — <see cref="Spread"/>); фенотип = среднее аллелей.
/// </summary>
public sealed class GenomeDef
{
/// <summary>Оптимальная освещённость (0..1) и её толерантность.</summary>
public float OptimalLight { get; init; } = 0.7f;
/// <summary>Ширина толерантности по свету.</summary>
public float LightTolerance { get; init; } = 0.4f;
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
public float OptimalTemperature { get; init; } = 18f;
/// <summary>Ширина толерантности по температуре (°C).</summary>
public float TemperatureTolerance { get; init; } = 12f;
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
public float OptimalFertility { get; init; } = 1f;
/// <summary>Ширина толерантности по плодородности.</summary>
public float FertilityTolerance { get; init; } = 0.8f;
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
public float Vigor { get; init; } = 1f;
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
public float Spread { get; init; } = 0.08f;
}
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
public sealed class PlantDef : Def
{
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
@@ -77,6 +109,9 @@ public sealed class PlantDef : Def
/// <summary>Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.</summary>
public List<PlantGrowthStage> Stages { get; init; } = [];
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
public GenomeDef Genome { get; init; } = new();
}
/// <summary>
+3 -2
View File
@@ -56,8 +56,9 @@ internal static class ScatterSpawner
Species = index,
Stage = stage,
AgeDays = age,
GrowthRate = terrain.Fertility,
}
CellFertility = terrain.Fertility,
},
PlantGenome.FromDef(def.Genome, random)
);
}
}
+2 -2
View File
@@ -76,7 +76,7 @@ public sealed class WorldScene : Scene
var calendar = Context.UseCalendar(SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default);
this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер
var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер
// Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed).
var plants = new PlantSet(content, atlases, device);
@@ -103,7 +103,7 @@ public sealed class WorldScene : Scene
var console = this.UseDevConsole();
RegisterCommands(console, content, atlases);
UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, CellSize));
UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, climate, dayNight, CellSize));
UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
);
+90
View File
@@ -0,0 +1,90 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
namespace LittleSim.Sim;
/// <summary>
/// Диплоидный геном растения: по паре аллелей на ген. Для числовых генов экспрессия гибридная —
/// фенотип есть среднее двух аллелей (дискретные доминант/рецессивные гены добавятся с размножением).
/// Аллели — наследуемые данные (правятся в инспекторе); фенотип-свойства читает система роста.
/// </summary>
public struct PlantGenome : IComponent
{
/// <summary>Аллели оптимума освещённости (0..1).</summary>
public float OptimalLightA;
public float OptimalLightB;
/// <summary>Аллели толерантности по свету.</summary>
public float LightToleranceA;
public float LightToleranceB;
/// <summary>Аллели оптимума температуры (°C).</summary>
public float OptimalTemperatureA;
public float OptimalTemperatureB;
/// <summary>Аллели толерантности по температуре (°C).</summary>
public float TemperatureToleranceA;
public float TemperatureToleranceB;
/// <summary>Аллели оптимума плодородности.</summary>
public float OptimalFertilityA;
public float OptimalFertilityB;
/// <summary>Аллели толерантности по плодородности.</summary>
public float FertilityToleranceA;
public float FertilityToleranceB;
/// <summary>Аллели бодрости роста.</summary>
public float VigorA;
public float VigorB;
/// <summary>Фенотип: оптимальная освещённость.</summary>
public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f;
/// <summary>Фенотип: толерантность по свету.</summary>
public readonly float LightTolerance => (LightToleranceA + LightToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная температура.</summary>
public readonly float OptimalTemperature => (OptimalTemperatureA + OptimalTemperatureB) * 0.5f;
/// <summary>Фенотип: толерантность по температуре.</summary>
public readonly float TemperatureTolerance =>
(TemperatureToleranceA + TemperatureToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная плодородность.</summary>
public readonly float OptimalFertility => (OptimalFertilityA + OptimalFertilityB) * 0.5f;
/// <summary>Фенотип: толерантность по плодородности.</summary>
public readonly float FertilityTolerance => (FertilityToleranceA + FertilityToleranceB) * 0.5f;
/// <summary>Фенотип: бодрость роста.</summary>
public readonly float Vigor => (VigorA + VigorB) * 0.5f;
/// <summary>
/// Генерирует особь из базового генома вида: каждый аллель — база ± относительный разброс
/// (<see cref="GenomeDef.Spread"/>). Сидируется переданным <paramref name="random"/> — детерминизм.
/// </summary>
public static PlantGenome FromDef(GenomeDef def, Random random)
{
float Allele(float baseValue) =>
baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue;
return new PlantGenome
{
OptimalLightA = Allele(def.OptimalLight),
OptimalLightB = Allele(def.OptimalLight),
LightToleranceA = Allele(def.LightTolerance),
LightToleranceB = Allele(def.LightTolerance),
OptimalTemperatureA = Allele(def.OptimalTemperature),
OptimalTemperatureB = Allele(def.OptimalTemperature),
TemperatureToleranceA = Allele(def.TemperatureTolerance),
TemperatureToleranceB = Allele(def.TemperatureTolerance),
OptimalFertilityA = Allele(def.OptimalFertility),
OptimalFertilityB = Allele(def.OptimalFertility),
FertilityToleranceA = Allele(def.FertilityTolerance),
FertilityToleranceB = Allele(def.FertilityTolerance),
VigorA = Allele(def.Vigor),
VigorB = Allele(def.Vigor),
};
}
}
+2 -2
View File
@@ -18,6 +18,6 @@ public struct PlantGrowth : IComponent
/// <summary>Накопленный возраст в игровых днях.</summary>
public float AgeDays;
/// <summary>Множитель скорости роста (плодородность клетки): 1 — норма, больше — быстрее.</summary>
public float GrowthRate;
/// <summary>Плодородность клетки, на которой растёт растение — вход пригодности(плодородность).</summary>
public float CellFertility;
}
+36 -8
View File
@@ -1,19 +1,27 @@
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Lighting;
namespace LittleSim.Sim;
/// <summary>
/// Двигает рост растений: копит возраст по игровому календарю и при пересечении порога стадии
/// переключает спрайт на текстуру новой стадии, переотцентровывает и масштабирует его. Время
/// идёт по <see cref="Calendar"/> (пауза/скорость учитываются через <c>Tick.deltaTime</c>),
/// данные стадий — из <see cref="PlantSet"/>. Терминальную стадию пропускает (рост окончен).
/// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов
/// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) ·
/// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из генов
/// (см. <see cref="PlantGenome"/>). Свет/температура в фазе B глобальные (день/ночь и сезон); стадии
/// и спрайт переключаются по накопленному возрасту как прежде. Терминальную стадию пропускает.
/// </summary>
public sealed class PlantGrowthSystem(PlantSet plants, Calendar calendar, int cellSize)
: QuerySystem<PlantGrowth, Sprite, Transform2D>
public sealed class PlantGrowthSystem(
PlantSet plants,
Calendar calendar,
Climate climate,
DayNight dayNight,
int cellSize
) : QuerySystem<PlantGrowth, PlantGenome, Sprite, Transform2D>
{
protected override void OnUpdate()
{
@@ -23,9 +31,14 @@ public sealed class PlantGrowthSystem(PlantSet plants, Calendar calendar, int ce
return; // пауза — рост стоит
}
foreach (var (growths, sprites, transforms, _) in Query.Chunks)
// В фазе B свет и температура глобальные — считаем раз за кадр (фаза D даст локальный свет).
var light = dayNight.Intensity;
var temperature = climate.Temperature;
foreach (var (growths, genomes, sprites, transforms, _) in Query.Chunks)
{
var g = growths.Span;
var dna = genomes.Span;
var s = sprites.Span;
var t = transforms.Span;
for (var i = 0; i < g.Length; i++)
@@ -37,7 +50,22 @@ public sealed class PlantGrowthSystem(PlantSet plants, Calendar calendar, int ce
continue; // уже зрелое — рост окончен
}
grow.AgeDays += deltaDays * grow.GrowthRate; // плодородность ускоряет/замедляет рост
ref var gene = ref dna[i];
var rate =
gene.Vigor
* Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance)
* Suitability.Gaussian(
temperature,
gene.OptimalTemperature,
gene.TemperatureTolerance
)
* Suitability.Gaussian(
grow.CellFertility,
gene.OptimalFertility,
gene.FertilityTolerance
);
grow.AgeDays += deltaDays * rate;
var changed = false;
while (
grow.Stage < stages.Length - 1