From 2b9d1fd10596690e846a31816e61fe785872a248 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 22:39:36 +0300 Subject: [PATCH] Perf: kill per-frame allocations and string lookups in hot sim paths - AnimalNeedsSystem (per-frame, every animal): drop the string-keyed genome lookup and Mass.OfAnimal call; Kleiber factor is now (size/refBody)^2.25 using a precomputed AnimalSet.Species.RefBodySize. No dict probe per animal per frame. - AnimalActionSystem: fetch the Skills component ONCE per acting animal instead of twice per action (SkillFactor + GrantXp took in Skills now). - RecomputeCapacities: apply the senescence scale inside CapacityCalc.Compute instead of a second pass that allocated a List of keys every recompute (per animal per health tick, and per injury in combat). - AnimalFactory.SkillSeed: iterate genome.Alleles (IReadOnlyDictionary, no copy) instead of ToDictionary() which allocated a dict per spawn. Behaviour-preserving (same numbers). Build + --check-content clean. Co-Authored-By: Claude Opus 4.8 --- src/LittleSim/Content/AnimalSet.cs | 8 ++++++ src/LittleSim/Sim/AnimalFactory.cs | 2 +- src/LittleSim/Sim/AnimalOrganism.cs | 12 ++------- src/LittleSim/Sim/AnimalSystems.cs | 42 +++++++++++++---------------- src/LittleSim/Sim/CapacityCalc.cs | 14 +++++++--- 5 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs index ca3fbb8..50681be 100644 --- a/src/LittleSim/Content/AnimalSet.cs +++ b/src/LittleSim/Content/AnimalSet.cs @@ -41,6 +41,10 @@ public sealed class AnimalSet /// Набор генов вида: базовые значения для генерации особи. public required GenomeTemplate Template { get; init; } + + /// Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей + /// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле. + public required float RefBodySize { get; init; } } // Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные. @@ -121,6 +125,10 @@ public sealed class AnimalSet Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages, Body = content.Defs.TryGet(def.Body, out var body) ? body : null, Template = BuildTemplate(def.Genome, genes), + RefBodySize = + def.Genome.TryGetValue("GeneMaxBodySize", out var refSize) && refSize > 0f + ? refSize + : 1f, }; _index[def] = i; } diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 1d15c7c..4ce20a1 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -90,7 +90,7 @@ public static class AnimalFactory private static int SkillSeed(Genome genome) { var acc = 0; - foreach (var (_, allele) in genome.ToDictionary()) + foreach (var (_, allele) in genome.Alleles) // IReadOnlyDictionary без копии — без аллокации { acc += (int)(allele.A * 7919f) + (int)(allele.B * 104729f); } diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 61e7102..2a53ad3 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -216,7 +216,8 @@ public sealed class HealthState Pain = Math.Clamp(pain, 0f, 1f); - var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain); + // Множитель старения применяется внутри Compute (без второго прохода/аллокации списка ключей). + var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain, capacityScale); foreach (var h in _hediffs) { if (h.Def.CapMods.Count == 0) @@ -231,15 +232,6 @@ public sealed class HealthState } } - if (capacityScale < 1f) - { - var scale = Math.Clamp(capacityScale, 0f, 1f); - foreach (var capacity in new List(caps.Keys)) - { - caps[capacity] *= scale; - } - } - Capacities = caps; } diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 4b03c52..441731d 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -155,15 +155,18 @@ public sealed class AnimalNeedsSystem( } // Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела: - // расход ∝ масса^0.75, нормирован на эталон вида → молодняк экономнее, крупные особи - // прожорливее (масса растёт с размером/возрастом). Множитель к расходу убывающих нужд. + // расход ∝ масса^0.75 = (линейный размер/эталон)^2.25 (масса ∝ размер³), нормирован на + // эталон вида → молодняк экономнее, крупные прожорливее. Эталон предпосчитан (без словаря). var sp = animals[o[i].Species]; var stageScale = sp.Stages[Math.Clamp(g[i].Stage, 0, sp.Stages.Length - 1)].Scale; - var mass = Mass.OfAnimal(sp.Def, o[i].Traits, stageScale); + var sizeRatio = MathF.Max( + 0.01f, + o[i].Traits.BodySize * stageScale / sp.RefBodySize + ); var metabolism = MathF.Max(0.1f, o[i].Traits.Metabolism) * (1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f)) - * Mass.KleiberFactor(mass, sp.Def.BaseMassKg); + * MathF.Pow(sizeRatio, 2.25f); var lethalEmpty = false; for (var k = 0; k < needs.Count; k++) { @@ -985,6 +988,7 @@ public sealed class AnimalActionSystem : BaseSystem var eating = hh[i].State.Capacity(AnimalCapacities.Eating); var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion); var self = entities.EntityAt(i); + self.TryGetComponent(out var skills); // один лукап на особь (эффекты + XP) // Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость. if (self.TryGetComponent(out var carrier) && carrier.CarriedKg > 0f) { @@ -1009,7 +1013,7 @@ public sealed class AnimalActionSystem : BaseSystem if (MoveTo(ref pos, brain.Target, speed * seconds)) { // Навык добычи корма: опытный форажир извлекает больше сытости; растёт от практики. - var forageF = SkillFactor(_forageSkill, self); + var forageF = SkillFactor(_forageSkill, skills); Refill(values, AnimalActions.Eat, days, foodEff * forageF); Graze( brain.TargetPlant, @@ -1025,7 +1029,7 @@ public sealed class AnimalActionSystem : BaseSystem ); // Эндозоохория: шанс проглотить семя зрелого растения (высадит позже). TryIngestSeed(brain.TargetPlant, self); - GrantXp(_forageSkill, self, days); + GrantXp(_forageSkill, skills, days); } break; @@ -1079,9 +1083,9 @@ public sealed class AnimalActionSystem : BaseSystem speed * seconds, days, foodEff, - SkillFactor(_huntSkill, self) + SkillFactor(_huntSkill, skills) ); - GrantXp(_huntSkill, self, days); + GrantXp(_huntSkill, skills, days); break; case AnimalActions.Flee: @@ -1089,9 +1093,9 @@ public sealed class AnimalActionSystem : BaseSystem MoveTo( ref pos, brain.Target, - speed * FleeSpeedMult * SkillFactor(_evadeSkill, self) * seconds + speed * FleeSpeedMult * SkillFactor(_evadeSkill, skills) * seconds ); - GrantXp(_evadeSkill, self, days); + GrantXp(_evadeSkill, skills, days); break; default: // Wander @@ -1278,14 +1282,9 @@ public sealed class AnimalActionSystem : BaseSystem } // Бонус-множитель от уровня навыка: 0.7 (новичок) … 1.3 (мастер). Навык не определён модом → 1 (без эффекта). - private float SkillFactor(int idx, Entity self) + private static float SkillFactor(int idx, in Skills s) { - if ( - idx < 0 - || !self.TryGetComponent(out var s) - || s.Levels is null - || idx >= s.Levels.Length - ) + if (idx < 0 || s.Levels is null || idx >= s.Levels.Length) { return 1f; } @@ -1294,14 +1293,9 @@ public sealed class AnimalActionSystem : BaseSystem } // Начисляет опыт навыку от практики: рост ∝ скорость обучения × страсть × (1−уровень) (убывающая отдача). - private void GrantXp(int idx, Entity self, float days) + private void GrantXp(int idx, in Skills s, float days) { - if ( - idx < 0 - || !self.TryGetComponent(out var s) - || s.Levels is null - || idx >= s.Levels.Length - ) + if (idx < 0 || s.Levels is null || idx >= s.Levels.Length) { return; } diff --git a/src/LittleSim/Sim/CapacityCalc.cs b/src/LittleSim/Sim/CapacityCalc.cs index 2e2af92..ea120d5 100644 --- a/src/LittleSim/Sim/CapacityCalc.cs +++ b/src/LittleSim/Sim/CapacityCalc.cs @@ -40,8 +40,14 @@ public static class AnimalCapacities /// public static class CapacityCalc { - /// Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1). - public static Dictionary Compute(PartInstance[] parts, float blood, float pain) + /// Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1); + /// — общий множитель (старение), применяется к итогу без лишних аллокаций. + public static Dictionary Compute( + PartInstance[] parts, + float blood, + float pain, + float capacityScale = 1f + ) { // Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет // только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность — @@ -100,10 +106,12 @@ public static class CapacityCalc // В результат — только объявленные телом способности: известные по формуле зависимостей, прочие // (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог. + var scale = Clamp01(capacityScale); var result = new Dictionary(System.StringComparer.Ordinal); foreach (var capacity in declared) { - result[capacity] = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity); + var value = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity); + result[capacity] = scale < 1f ? value * scale : value; } return result;