Phase C: plant reproduction (Mendel), death and persistence
Plants now live full lives. The genome gains lifespan, dispersal, fecundity, self-pollination, mutation-rate and a discrete dominant/recessive Morph gene; PlantGenome.Breed models meiosis (one allele per parent) plus mutation. A gated PlantLifecycleSystem builds a per-cell grid (density + a species/genome representative), reaps plants past their lifespan, and lets mature plants seed offspring — cross-pollinating with a mature same-species neighbour in dispersal range (falling back to self-pollination by gene) into a nearby land cell under the density cap. A shared PlantFactory creates every plant (initial scatter, births, save restore) and tints recessive-morph plants. Growth keeps aging mature plants so they can die of old age. The whole population (genomes, positions, age, stage) is saved to WorldSave and restored on load instead of re-scattering from the seed. plants.json carries the new genes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bfe1b139f0
commit
a04e867e87
@@ -3,7 +3,9 @@
|
||||
"defs": [
|
||||
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
||||
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
|
||||
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0, "spread": 0.08 },
|
||||
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
|
||||
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
||||
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08 },
|
||||
"stages": [
|
||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||
@@ -16,7 +18,9 @@
|
||||
|
||||
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2,
|
||||
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12,
|
||||
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3, "spread": 0.1 },
|
||||
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
|
||||
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
|
||||
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1 },
|
||||
"stages": [
|
||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
|
||||
{ "sizeCells": 1.2, "label": "plant.stage.mature" }
|
||||
@@ -24,7 +28,9 @@
|
||||
|
||||
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
|
||||
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13,
|
||||
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0, "spread": 0.1 },
|
||||
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
|
||||
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
|
||||
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1 },
|
||||
"stages": [
|
||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
|
||||
{ "sizeCells": 1.4, "label": "plant.stage.mature" }
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using LittleSim.Sim;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
|
||||
public sealed class PlantSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays { get; set; }
|
||||
|
||||
/// <summary>Текущая стадия роста.</summary>
|
||||
public int Stage { get; set; }
|
||||
|
||||
/// <summary>Плодородность клетки.</summary>
|
||||
public float CellFertility { get; set; }
|
||||
|
||||
/// <summary>Геном особи (аллели сериализуются как поля структуры).</summary>
|
||||
public PlantGenome Genome { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
|
||||
public sealed class PawnSave
|
||||
{
|
||||
@@ -70,6 +96,9 @@ public sealed class WorldSave
|
||||
/// <summary>Снимок жителей.</summary>
|
||||
public List<PawnSave> Pawns { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||
public List<PlantSave> Plants { get; set; } = [];
|
||||
|
||||
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||
public WorldConfig ToConfig() =>
|
||||
new()
|
||||
@@ -90,6 +119,7 @@ public sealed class SaveStore
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
IncludeFields = true, // аллели генома (PlantGenome) — публичные поля структуры
|
||||
};
|
||||
|
||||
private readonly string _directory;
|
||||
|
||||
@@ -91,6 +91,24 @@ public sealed class GenomeDef
|
||||
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
|
||||
public float Vigor { get; init; } = 1f;
|
||||
|
||||
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
||||
public float Lifespan { get; init; } = 80f;
|
||||
|
||||
/// <summary>Дальность расселения семян (в клетках).</summary>
|
||||
public float DispersalRange { get; init; } = 2f;
|
||||
|
||||
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
|
||||
public float ReproduceInterval { get; init; } = 8f;
|
||||
|
||||
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
|
||||
public float SelfPollination { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
|
||||
public float MutationRate { get; init; } = 0.05f;
|
||||
|
||||
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
|
||||
public float VariantChance { get; init; } = 0.15f;
|
||||
|
||||
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
||||
public float Spread { get; init; } = 0.08f;
|
||||
}
|
||||
|
||||
@@ -36,29 +36,19 @@ internal static class ScatterSpawner
|
||||
|
||||
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
||||
var index = plants.IndexOf(def);
|
||||
var species = plants[index];
|
||||
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||
var age = random.NextSingle() * species.MaturityDays;
|
||||
var stage = species.StageAt(age);
|
||||
var resolved = species.Stages[stage];
|
||||
var age = random.NextSingle() * plants[index].MaturityDays;
|
||||
|
||||
var sprite = new Sprite(resolved.Region, GameLayers.Beings);
|
||||
sprite.CenterOrigin();
|
||||
scene.Store.CreateEntity(
|
||||
new Transform2D(
|
||||
new Vector2(px, py),
|
||||
scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width)
|
||||
),
|
||||
sprite,
|
||||
new PlantGrowth
|
||||
{
|
||||
Species = index,
|
||||
Stage = stage,
|
||||
AgeDays = age,
|
||||
CellFertility = terrain.Fertility,
|
||||
},
|
||||
PlantGenome.FromDef(def.Genome, random)
|
||||
PlantFactory.Create(
|
||||
scene.Store,
|
||||
plants,
|
||||
index,
|
||||
new Vector2(px, py),
|
||||
age,
|
||||
PlantGenome.FromDef(def.Genome, random),
|
||||
terrain.Fertility,
|
||||
cellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ namespace LittleSim.Scenes;
|
||||
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
|
||||
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
|
||||
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
|
||||
/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие
|
||||
/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн.
|
||||
/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/
|
||||
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
|
||||
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
|
||||
/// </summary>
|
||||
public sealed class WorldScene : Scene
|
||||
{
|
||||
@@ -38,6 +39,9 @@ public sealed class WorldScene : Scene
|
||||
/// </summary>
|
||||
public const float SecondsPerDay = 480f;
|
||||
|
||||
/// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
|
||||
public const int DensityCap = 4;
|
||||
|
||||
private readonly WorldConfig _config;
|
||||
private readonly WorldSave? _save;
|
||||
private readonly RectF _bounds;
|
||||
@@ -46,6 +50,10 @@ public sealed class WorldScene : Scene
|
||||
private PauseMenu _pause = null!;
|
||||
private readonly List<Action> _speedRefreshers = [];
|
||||
|
||||
private PlantSet _plants = null!;
|
||||
private float[] _cellFertility = [];
|
||||
private bool[] _cellLand = [];
|
||||
|
||||
/// <summary>Новый мир из конфига.</summary>
|
||||
public WorldScene(WorldConfig config)
|
||||
: this(config, null) { }
|
||||
@@ -78,10 +86,20 @@ public sealed class WorldScene : Scene
|
||||
var climate = Context.UseClimate(ClimateSettings.Default);
|
||||
var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер
|
||||
|
||||
// Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed).
|
||||
var plants = new PlantSet(content, atlases, device);
|
||||
var random = new Random(_config.Seed);
|
||||
BuildTerrain(content, atlases, device, assets, plants, random);
|
||||
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
||||
_plants = new PlantSet(content, atlases, device);
|
||||
var loadingPlants = _save?.Plants is { Count: > 0 };
|
||||
BuildTerrain(
|
||||
content,
|
||||
atlases,
|
||||
device,
|
||||
assets,
|
||||
loadingPlants ? null : new Random(_config.Seed)
|
||||
);
|
||||
if (loadingPlants)
|
||||
{
|
||||
RestorePlants(content);
|
||||
}
|
||||
|
||||
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
||||
|
||||
@@ -103,7 +121,22 @@ public sealed class WorldScene : Scene
|
||||
var console = this.UseDevConsole();
|
||||
RegisterCommands(console, content, atlases);
|
||||
|
||||
UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, climate, dayNight, CellSize));
|
||||
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, dayNight, CellSize));
|
||||
UpdateSystems.Add(
|
||||
new PlantLifecycleSystem(
|
||||
Store,
|
||||
_plants,
|
||||
calendar,
|
||||
Context.Clock,
|
||||
_config.Width,
|
||||
_config.Height,
|
||||
CellSize,
|
||||
_cellFertility,
|
||||
_cellLand,
|
||||
DensityCap,
|
||||
_config.Seed
|
||||
)
|
||||
);
|
||||
UpdateSystems.Add(
|
||||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||
);
|
||||
@@ -127,17 +160,17 @@ public sealed class WorldScene : Scene
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Строит одну сущность-<see cref="Tilemap"/>: тайлсет из дефов рельефа (поверхность из
|
||||
/// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а
|
||||
/// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости).
|
||||
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
|
||||
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
|
||||
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
|
||||
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
|
||||
/// </summary>
|
||||
private void BuildTerrain(
|
||||
GameContent content,
|
||||
ModAtlases atlases,
|
||||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||||
AssetManager assets,
|
||||
PlantSet plants,
|
||||
Random random
|
||||
Random? scatterRandom
|
||||
)
|
||||
{
|
||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||
@@ -160,21 +193,29 @@ public sealed class WorldScene : Scene
|
||||
_config.SmoothPasses
|
||||
);
|
||||
var grid = new TileGrid(_config.Width, _config.Height);
|
||||
_cellFertility = new float[_config.Width * _config.Height];
|
||||
_cellLand = new bool[_config.Width * _config.Height];
|
||||
for (var x = 0; x < _config.Width; x++)
|
||||
{
|
||||
for (var y = 0; y < _config.Height; y++)
|
||||
{
|
||||
var terrain = content.Terrains.Classify(heights[x, y]);
|
||||
grid[x, y] = tileByDef[terrain];
|
||||
ScatterSpawner.Spawn(
|
||||
this,
|
||||
content,
|
||||
plants,
|
||||
terrain,
|
||||
new Point(x, y),
|
||||
CellSize,
|
||||
random
|
||||
);
|
||||
var cell = y * _config.Width + x;
|
||||
_cellFertility[cell] = terrain.Fertility;
|
||||
_cellLand[cell] = terrain.IsLand;
|
||||
if (scatterRandom is not null)
|
||||
{
|
||||
ScatterSpawner.Spawn(
|
||||
this,
|
||||
content,
|
||||
_plants,
|
||||
terrain,
|
||||
new Point(x, y),
|
||||
CellSize,
|
||||
scatterRandom
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +295,6 @@ public sealed class WorldScene : Scene
|
||||
|
||||
private string SaveWorld()
|
||||
{
|
||||
// Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида.
|
||||
var save = new WorldSave
|
||||
{
|
||||
Name = _config.Name,
|
||||
@@ -267,11 +307,54 @@ public sealed class WorldScene : Scene
|
||||
ElapsedSeconds = Context.Clock.TotalTime,
|
||||
};
|
||||
|
||||
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
|
||||
Store
|
||||
.Query<PlantGrowth, PlantGenome, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(ref PlantGrowth grow, ref PlantGenome gene, ref Transform2D transform, Entity _) =>
|
||||
{
|
||||
save.Plants.Add(
|
||||
new PlantSave
|
||||
{
|
||||
Species = _plants[grow.Species].Def.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
AgeDays = grow.AgeDays,
|
||||
Stage = grow.Stage,
|
||||
CellFertility = grow.CellFertility,
|
||||
Genome = gene,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
new SaveStore().Write(save);
|
||||
Log.Info($"World '{_config.Name}' saved");
|
||||
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
|
||||
return _config.Name;
|
||||
}
|
||||
|
||||
private void RestorePlants(GameContent content)
|
||||
{
|
||||
foreach (var plant in _save!.Plants)
|
||||
{
|
||||
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
|
||||
{
|
||||
continue; // вид пропал (мод убрали) — пропускаем растение
|
||||
}
|
||||
|
||||
PlantFactory.Create(
|
||||
Store,
|
||||
_plants,
|
||||
_plants.IndexOf(def),
|
||||
new Vector2(plant.X, plant.Y),
|
||||
plant.AgeDays,
|
||||
plant.Genome,
|
||||
plant.CellFertility,
|
||||
CellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void Switch(Scene scene)
|
||||
{
|
||||
if (!Context.Scenes.IsTransitioning)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Scenes;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Единая точка создания сущности-растения: спрайт стадии по возрасту, тинт по морфе генома и
|
||||
/// компоненты роста/генома. Переиспользуется начальным скаттером, размножением и загрузкой сейва —
|
||||
/// чтобы все растения собирались одинаково.
|
||||
/// </summary>
|
||||
public static class PlantFactory
|
||||
{
|
||||
// Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым.
|
||||
private static readonly Color VariantTint = new(214, 150, 176);
|
||||
|
||||
/// <summary>Создаёт растение вида <paramref name="species"/> с заданным возрастом и геномом.</summary>
|
||||
public static Entity Create(
|
||||
EntityStore store,
|
||||
PlantSet plants,
|
||||
int species,
|
||||
Vector2 position,
|
||||
float ageDays,
|
||||
in PlantGenome genome,
|
||||
float cellFertility,
|
||||
int cellSize
|
||||
)
|
||||
{
|
||||
var sp = plants[species];
|
||||
var stage = sp.StageAt(ageDays);
|
||||
var resolved = sp.Stages[stage];
|
||||
|
||||
var sprite = new Sprite(resolved.Region, GameLayers.Beings);
|
||||
sprite.CenterOrigin();
|
||||
sprite.Color = genome.IsVariant ? VariantTint : Color.White;
|
||||
|
||||
return store.CreateEntity(
|
||||
new Transform2D(
|
||||
position,
|
||||
scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width)
|
||||
),
|
||||
sprite,
|
||||
new PlantGrowth
|
||||
{
|
||||
Species = species,
|
||||
Stage = stage,
|
||||
AgeDays = ageDays,
|
||||
CellFertility = cellFertility,
|
||||
},
|
||||
genome
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,39 +4,42 @@ using LittleSim.Content;
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Диплоидный геном растения: по паре аллелей на ген. Для числовых генов экспрессия гибридная —
|
||||
/// фенотип есть среднее двух аллелей (дискретные доминант/рецессивные гены добавятся с размножением).
|
||||
/// Аллели — наследуемые данные (правятся в инспекторе); фенотип-свойства читает система роста.
|
||||
/// Диплоидный геном растения: по паре аллелей на ген. Числовые гены экспрессируются гибридно —
|
||||
/// фенотип есть среднее двух аллелей; дискретный ген морфы — по Менделю (рецессив виден только в
|
||||
/// гомозиготе). Аллели наследуются (правятся в инспекторе); фенотип-свойства читают системы роста и
|
||||
/// жизненного цикла. <see cref="Breed"/> моделирует мейоз и мутацию.
|
||||
/// </summary>
|
||||
public struct PlantGenome : IComponent
|
||||
{
|
||||
/// <summary>Аллели оптимума освещённости (0..1).</summary>
|
||||
// --- Числовые гены (аллель A/B), фенотип = среднее ------------------------------------------
|
||||
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;
|
||||
public float LifespanA;
|
||||
public float LifespanB;
|
||||
public float DispersalRangeA;
|
||||
public float DispersalRangeB;
|
||||
public float ReproduceIntervalA;
|
||||
public float ReproduceIntervalB;
|
||||
public float SelfPollinationA;
|
||||
public float SelfPollinationB;
|
||||
public float MutationRateA;
|
||||
public float MutationRateB;
|
||||
|
||||
// --- Дискретный ген морфы (0 — доминантный, 1 — рецессивный) --------------------------------
|
||||
public int MorphA;
|
||||
public int MorphB;
|
||||
|
||||
/// <summary>Фенотип: оптимальная освещённость.</summary>
|
||||
public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f;
|
||||
@@ -60,15 +63,36 @@ public struct PlantGenome : IComponent
|
||||
/// <summary>Фенотип: бодрость роста.</summary>
|
||||
public readonly float Vigor => (VigorA + VigorB) * 0.5f;
|
||||
|
||||
/// <summary>Фенотип: продолжительность жизни (дней).</summary>
|
||||
public readonly float Lifespan => (LifespanA + LifespanB) * 0.5f;
|
||||
|
||||
/// <summary>Фенотип: дальность расселения (клеток).</summary>
|
||||
public readonly float DispersalRange => (DispersalRangeA + DispersalRangeB) * 0.5f;
|
||||
|
||||
/// <summary>Фенотип: интервал размножения (дней).</summary>
|
||||
public readonly float ReproduceInterval => (ReproduceIntervalA + ReproduceIntervalB) * 0.5f;
|
||||
|
||||
/// <summary>Фенотип: способность к самоопылению (0..1).</summary>
|
||||
public readonly float SelfPollination => (SelfPollinationA + SelfPollinationB) * 0.5f;
|
||||
|
||||
/// <summary>Фенотип: темп мутаций (0..1).</summary>
|
||||
public readonly float MutationRate => (MutationRateA + MutationRateB) * 0.5f;
|
||||
|
||||
/// <summary>Фенотип морфы: рецессивный вариант проявляется только в гомозиготе (1,1).</summary>
|
||||
public readonly bool IsVariant => MorphA == 1 && MorphB == 1;
|
||||
|
||||
/// <summary>
|
||||
/// Генерирует особь из базового генома вида: каждый аллель — база ± относительный разброс
|
||||
/// (<see cref="GenomeDef.Spread"/>). Сидируется переданным <paramref name="random"/> — детерминизм.
|
||||
/// Генерирует особь из базового генома вида: числовые аллели — база ± относительный разброс
|
||||
/// (<see cref="GenomeDef.Spread"/>); аллель морфы = рецессивный (1) с вероятностью
|
||||
/// <see cref="GenomeDef.VariantChance"/>. Сидируется <paramref name="random"/> — детерминизм.
|
||||
/// </summary>
|
||||
public static PlantGenome FromDef(GenomeDef def, Random random)
|
||||
{
|
||||
float Allele(float baseValue) =>
|
||||
baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue;
|
||||
|
||||
int MorphAllele() => random.NextSingle() < def.VariantChance ? 1 : 0;
|
||||
|
||||
return new PlantGenome
|
||||
{
|
||||
OptimalLightA = Allele(def.OptimalLight),
|
||||
@@ -85,6 +109,63 @@ public struct PlantGenome : IComponent
|
||||
FertilityToleranceB = Allele(def.FertilityTolerance),
|
||||
VigorA = Allele(def.Vigor),
|
||||
VigorB = Allele(def.Vigor),
|
||||
LifespanA = Allele(def.Lifespan),
|
||||
LifespanB = Allele(def.Lifespan),
|
||||
DispersalRangeA = Allele(def.DispersalRange),
|
||||
DispersalRangeB = Allele(def.DispersalRange),
|
||||
ReproduceIntervalA = Allele(def.ReproduceInterval),
|
||||
ReproduceIntervalB = Allele(def.ReproduceInterval),
|
||||
SelfPollinationA = Allele(def.SelfPollination),
|
||||
SelfPollinationB = Allele(def.SelfPollination),
|
||||
MutationRateA = Allele(def.MutationRate),
|
||||
MutationRateB = Allele(def.MutationRate),
|
||||
MorphA = MorphAllele(),
|
||||
MorphB = MorphAllele(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Потомок двух родителей (мейоз): аллель A — случайный из родителя <paramref name="a"/>, аллель
|
||||
/// B — из <paramref name="b"/>; затем мутация (с вероятностью среднего темпа мутаций родителей —
|
||||
/// сдвиг числового аллеля на малую долю / переключение морфы). Самоопыление = <c>Breed(self, self)</c>.
|
||||
/// </summary>
|
||||
public static PlantGenome Breed(in PlantGenome a, in PlantGenome b, Random rng)
|
||||
{
|
||||
var rate = (a.MutationRate + b.MutationRate) * 0.5f;
|
||||
|
||||
float Mut(float v) =>
|
||||
rng.NextSingle() < rate ? v + (rng.NextSingle() * 2f - 1f) * 0.12f * v : v;
|
||||
float FromA(float x, float y) => Mut(rng.NextSingle() < 0.5f ? x : y);
|
||||
int MutMorph(int m) => rng.NextSingle() < rate ? 1 - m : m;
|
||||
|
||||
return new PlantGenome
|
||||
{
|
||||
OptimalLightA = FromA(a.OptimalLightA, a.OptimalLightB),
|
||||
OptimalLightB = FromA(b.OptimalLightA, b.OptimalLightB),
|
||||
LightToleranceA = FromA(a.LightToleranceA, a.LightToleranceB),
|
||||
LightToleranceB = FromA(b.LightToleranceA, b.LightToleranceB),
|
||||
OptimalTemperatureA = FromA(a.OptimalTemperatureA, a.OptimalTemperatureB),
|
||||
OptimalTemperatureB = FromA(b.OptimalTemperatureA, b.OptimalTemperatureB),
|
||||
TemperatureToleranceA = FromA(a.TemperatureToleranceA, a.TemperatureToleranceB),
|
||||
TemperatureToleranceB = FromA(b.TemperatureToleranceA, b.TemperatureToleranceB),
|
||||
OptimalFertilityA = FromA(a.OptimalFertilityA, a.OptimalFertilityB),
|
||||
OptimalFertilityB = FromA(b.OptimalFertilityA, b.OptimalFertilityB),
|
||||
FertilityToleranceA = FromA(a.FertilityToleranceA, a.FertilityToleranceB),
|
||||
FertilityToleranceB = FromA(b.FertilityToleranceA, b.FertilityToleranceB),
|
||||
VigorA = FromA(a.VigorA, a.VigorB),
|
||||
VigorB = FromA(b.VigorA, b.VigorB),
|
||||
LifespanA = FromA(a.LifespanA, a.LifespanB),
|
||||
LifespanB = FromA(b.LifespanA, b.LifespanB),
|
||||
DispersalRangeA = FromA(a.DispersalRangeA, a.DispersalRangeB),
|
||||
DispersalRangeB = FromA(b.DispersalRangeA, b.DispersalRangeB),
|
||||
ReproduceIntervalA = FromA(a.ReproduceIntervalA, a.ReproduceIntervalB),
|
||||
ReproduceIntervalB = FromA(b.ReproduceIntervalA, b.ReproduceIntervalB),
|
||||
SelfPollinationA = FromA(a.SelfPollinationA, a.SelfPollinationB),
|
||||
SelfPollinationB = FromA(b.SelfPollinationA, b.SelfPollinationB),
|
||||
MutationRateA = FromA(a.MutationRateA, a.MutationRateB),
|
||||
MutationRateB = FromA(b.MutationRateA, b.MutationRateB),
|
||||
MorphA = MutMorph(rng.NextSingle() < 0.5f ? a.MorphA : a.MorphB),
|
||||
MorphB = MutMorph(rng.NextSingle() < 0.5f ? b.MorphA : b.MorphB),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ public sealed class PlantGrowthSystem(
|
||||
var stages = plants[grow.Species].Stages;
|
||||
if (grow.Stage >= stages.Length - 1)
|
||||
{
|
||||
continue; // уже зрелое — рост окончен
|
||||
grow.AgeDays += deltaDays; // зрелое: старение в реальном времени → смерть по старости
|
||||
continue;
|
||||
}
|
||||
|
||||
ref var gene = ref dna[i];
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается
|
||||
/// не каждый кадр, а раз в <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку
|
||||
/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с
|
||||
/// вероятностью <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления
|
||||
/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в
|
||||
/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения
|
||||
/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно.
|
||||
/// </summary>
|
||||
public sealed class PlantLifecycleSystem : BaseSystem
|
||||
{
|
||||
private const float IntervalDays = 0.25f;
|
||||
|
||||
private readonly EntityStore _store;
|
||||
private readonly PlantSet _plants;
|
||||
private readonly Calendar _calendar;
|
||||
private readonly GameClock _clock;
|
||||
private readonly int _width;
|
||||
private readonly int _height;
|
||||
private readonly int _cellSize;
|
||||
private readonly float[] _cellFertility;
|
||||
private readonly bool[] _cellLand;
|
||||
private readonly int _densityCap;
|
||||
private readonly Random _rng;
|
||||
private readonly ArchetypeQuery<PlantGrowth, PlantGenome, Transform2D> _query;
|
||||
|
||||
private readonly int[] _count;
|
||||
private readonly CellRep[] _rep;
|
||||
private readonly List<Entity> _deaths = [];
|
||||
private readonly List<Candidate> _candidates = [];
|
||||
private readonly List<Birth> _births = [];
|
||||
private float _accumulator;
|
||||
|
||||
public PlantLifecycleSystem(
|
||||
EntityStore store,
|
||||
PlantSet plants,
|
||||
Calendar calendar,
|
||||
GameClock clock,
|
||||
int width,
|
||||
int height,
|
||||
int cellSize,
|
||||
float[] cellFertility,
|
||||
bool[] cellLand,
|
||||
int densityCap,
|
||||
int seed
|
||||
)
|
||||
{
|
||||
_store = store;
|
||||
_plants = plants;
|
||||
_calendar = calendar;
|
||||
_clock = clock;
|
||||
_width = width;
|
||||
_height = height;
|
||||
_cellSize = cellSize;
|
||||
_cellFertility = cellFertility;
|
||||
_cellLand = cellLand;
|
||||
_densityCap = densityCap;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<PlantGrowth, PlantGenome, Transform2D>();
|
||||
_count = new int[width * height];
|
||||
_rep = new CellRep[width * height];
|
||||
}
|
||||
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
_accumulator += _clock.DeltaTime / _calendar.SecondsPerDay; // прошло игровых дней (с учётом паузы/скорости)
|
||||
if (_accumulator < IntervalDays)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var elapsed = _accumulator;
|
||||
_accumulator = 0f;
|
||||
|
||||
BuildGridAndCollect();
|
||||
Reproduce(elapsed);
|
||||
Apply();
|
||||
}
|
||||
|
||||
// Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов.
|
||||
private void BuildGridAndCollect()
|
||||
{
|
||||
Array.Clear(_count);
|
||||
for (var c = 0; c < _rep.Length; c++)
|
||||
{
|
||||
_rep[c].Species = -1;
|
||||
}
|
||||
|
||||
_deaths.Clear();
|
||||
_candidates.Clear();
|
||||
_births.Clear();
|
||||
|
||||
foreach (var (growths, genomes, transforms, entities) in _query.Chunks)
|
||||
{
|
||||
var g = growths.Span;
|
||||
var dna = genomes.Span;
|
||||
var t = transforms.Span;
|
||||
for (var i = 0; i < g.Length; i++)
|
||||
{
|
||||
ref var grow = ref g[i];
|
||||
ref var gene = ref dna[i];
|
||||
if (grow.AgeDays > gene.Lifespan)
|
||||
{
|
||||
_deaths.Add(entities.EntityAt(i)); // умер от старости
|
||||
continue;
|
||||
}
|
||||
|
||||
var pos = t[i].Position;
|
||||
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
||||
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
||||
var cell = cy * _width + cx;
|
||||
_count[cell]++;
|
||||
|
||||
var mature = grow.Stage >= _plants[grow.Species].Stages.Length - 1;
|
||||
_rep[cell] = new CellRep
|
||||
{
|
||||
Species = grow.Species,
|
||||
Mature = mature,
|
||||
Genome = gene,
|
||||
};
|
||||
if (mature)
|
||||
{
|
||||
_candidates.Add(
|
||||
new Candidate
|
||||
{
|
||||
Cell = cell,
|
||||
Species = grow.Species,
|
||||
Genome = gene,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Проход 2: зрелые сеют потомка (перекрёстно или само-) в подходящую клетку радиуса.
|
||||
private void Reproduce(float elapsed)
|
||||
{
|
||||
foreach (var cand in _candidates)
|
||||
{
|
||||
var interval = MathF.Max(0.5f, cand.Genome.ReproduceInterval);
|
||||
if (_rng.NextSingle() >= elapsed / interval)
|
||||
{
|
||||
continue; // в этот тик не сеет
|
||||
}
|
||||
|
||||
var range = Math.Clamp((int)MathF.Round(cand.Genome.DispersalRange), 1, 6);
|
||||
var cx = cand.Cell % _width;
|
||||
var cy = cand.Cell / _width;
|
||||
|
||||
if (!TryFindPartner(cx, cy, range, cand.Species, out var partner))
|
||||
{
|
||||
if (_rng.NextSingle() >= cand.Genome.SelfPollination)
|
||||
{
|
||||
continue; // нет партнёра и самоопыление не удалось
|
||||
}
|
||||
|
||||
partner = cand.Genome; // самоопыление
|
||||
}
|
||||
|
||||
if (!TryPickTarget(cx, cy, range, out var tcell))
|
||||
{
|
||||
continue; // некуда сеять (нет суши/свободного места рядом)
|
||||
}
|
||||
|
||||
var child = PlantGenome.Breed(cand.Genome, partner, _rng);
|
||||
var tx = tcell % _width;
|
||||
var ty = tcell / _width;
|
||||
var position = new Vector2(
|
||||
(tx + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize,
|
||||
(ty + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize
|
||||
);
|
||||
_births.Add(
|
||||
new Birth
|
||||
{
|
||||
Species = cand.Species,
|
||||
Position = position,
|
||||
Genome = child,
|
||||
Fertility = _cellFertility[tcell],
|
||||
}
|
||||
);
|
||||
_count[tcell]++; // резервируем место, чтобы не переполнить клетку за тик
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindPartner(int cx, int cy, int range, int species, out PlantGenome partner)
|
||||
{
|
||||
for (var dy = -range; dy <= range; dy++)
|
||||
{
|
||||
for (var dx = -range; dx <= range; dx++)
|
||||
{
|
||||
if (dx == 0 && dy == 0)
|
||||
{
|
||||
continue; // партнёр — другая клетка (приоритет перекрёстного)
|
||||
}
|
||||
|
||||
var nx = cx + dx;
|
||||
var ny = cy + dy;
|
||||
if (nx < 0 || nx >= _width || ny < 0 || ny >= _height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ref var rep = ref _rep[ny * _width + nx];
|
||||
if (rep.Species == species && rep.Mature)
|
||||
{
|
||||
partner = rep.Genome;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partner = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryPickTarget(int cx, int cy, int range, out int cell)
|
||||
{
|
||||
for (var attempt = 0; attempt < 6; attempt++)
|
||||
{
|
||||
var tx = cx + _rng.Next(-range, range + 1);
|
||||
var ty = cy + _rng.Next(-range, range + 1);
|
||||
if (tx < 0 || tx >= _width || ty < 0 || ty >= _height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = ty * _width + tx;
|
||||
if (_cellLand[candidate] && _count[candidate] < _densityCap)
|
||||
{
|
||||
cell = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
cell = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
foreach (var entity in _deaths)
|
||||
{
|
||||
entity.DeleteEntity();
|
||||
}
|
||||
|
||||
foreach (var birth in _births)
|
||||
{
|
||||
PlantFactory.Create(
|
||||
_store,
|
||||
_plants,
|
||||
birth.Species,
|
||||
birth.Position,
|
||||
ageDays: 0f,
|
||||
birth.Genome,
|
||||
birth.Fertility,
|
||||
_cellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private struct CellRep
|
||||
{
|
||||
public int Species;
|
||||
public bool Mature;
|
||||
public PlantGenome Genome;
|
||||
}
|
||||
|
||||
private struct Candidate
|
||||
{
|
||||
public int Cell;
|
||||
public int Species;
|
||||
public PlantGenome Genome;
|
||||
}
|
||||
|
||||
private struct Birth
|
||||
{
|
||||
public int Species;
|
||||
public Vector2 Position;
|
||||
public PlantGenome Genome;
|
||||
public float Fertility;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user