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<string> 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3dfa8c658f
commit
2b9d1fd105
@@ -41,6 +41,10 @@ public sealed class AnimalSet
|
|||||||
|
|
||||||
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
|
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
|
||||||
public required GenomeTemplate Template { get; init; }
|
public required GenomeTemplate Template { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей
|
||||||
|
/// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле.</summary>
|
||||||
|
public required float RefBodySize { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
|
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
|
||||||
@@ -121,6 +125,10 @@ public sealed class AnimalSet
|
|||||||
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
|
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
|
||||||
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
|
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
|
||||||
Template = BuildTemplate(def.Genome, genes),
|
Template = BuildTemplate(def.Genome, genes),
|
||||||
|
RefBodySize =
|
||||||
|
def.Genome.TryGetValue("GeneMaxBodySize", out var refSize) && refSize > 0f
|
||||||
|
? refSize
|
||||||
|
: 1f,
|
||||||
};
|
};
|
||||||
_index[def] = i;
|
_index[def] = i;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public static class AnimalFactory
|
|||||||
private static int SkillSeed(Genome genome)
|
private static int SkillSeed(Genome genome)
|
||||||
{
|
{
|
||||||
var acc = 0;
|
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);
|
acc += (int)(allele.A * 7919f) + (int)(allele.B * 104729f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,8 @@ public sealed class HealthState
|
|||||||
|
|
||||||
Pain = Math.Clamp(pain, 0f, 1f);
|
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)
|
foreach (var h in _hediffs)
|
||||||
{
|
{
|
||||||
if (h.Def.CapMods.Count == 0)
|
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<string>(caps.Keys))
|
|
||||||
{
|
|
||||||
caps[capacity] *= scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Capacities = caps;
|
Capacities = caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,15 +155,18 @@ public sealed class AnimalNeedsSystem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела:
|
// Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела:
|
||||||
// расход ∝ масса^0.75, нормирован на эталон вида → молодняк экономнее, крупные особи
|
// расход ∝ масса^0.75 = (линейный размер/эталон)^2.25 (масса ∝ размер³), нормирован на
|
||||||
// прожорливее (масса растёт с размером/возрастом). Множитель к расходу убывающих нужд.
|
// эталон вида → молодняк экономнее, крупные прожорливее. Эталон предпосчитан (без словаря).
|
||||||
var sp = animals[o[i].Species];
|
var sp = animals[o[i].Species];
|
||||||
var stageScale = sp.Stages[Math.Clamp(g[i].Stage, 0, sp.Stages.Length - 1)].Scale;
|
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 =
|
var metabolism =
|
||||||
MathF.Max(0.1f, o[i].Traits.Metabolism)
|
MathF.Max(0.1f, o[i].Traits.Metabolism)
|
||||||
* (1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f))
|
* (1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f))
|
||||||
* Mass.KleiberFactor(mass, sp.Def.BaseMassKg);
|
* MathF.Pow(sizeRatio, 2.25f);
|
||||||
var lethalEmpty = false;
|
var lethalEmpty = false;
|
||||||
for (var k = 0; k < needs.Count; k++)
|
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 eating = hh[i].State.Capacity(AnimalCapacities.Eating);
|
||||||
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
|
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
|
||||||
var self = entities.EntityAt(i);
|
var self = entities.EntityAt(i);
|
||||||
|
self.TryGetComponent<Skills>(out var skills); // один лукап на особь (эффекты + XP)
|
||||||
// Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость.
|
// Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость.
|
||||||
if (self.TryGetComponent<Carrier>(out var carrier) && carrier.CarriedKg > 0f)
|
if (self.TryGetComponent<Carrier>(out var carrier) && carrier.CarriedKg > 0f)
|
||||||
{
|
{
|
||||||
@@ -1009,7 +1013,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
if (MoveTo(ref pos, brain.Target, speed * seconds))
|
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);
|
Refill(values, AnimalActions.Eat, days, foodEff * forageF);
|
||||||
Graze(
|
Graze(
|
||||||
brain.TargetPlant,
|
brain.TargetPlant,
|
||||||
@@ -1025,7 +1029,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
);
|
);
|
||||||
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
|
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
|
||||||
TryIngestSeed(brain.TargetPlant, self);
|
TryIngestSeed(brain.TargetPlant, self);
|
||||||
GrantXp(_forageSkill, self, days);
|
GrantXp(_forageSkill, skills, days);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -1079,9 +1083,9 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
speed * seconds,
|
speed * seconds,
|
||||||
days,
|
days,
|
||||||
foodEff,
|
foodEff,
|
||||||
SkillFactor(_huntSkill, self)
|
SkillFactor(_huntSkill, skills)
|
||||||
);
|
);
|
||||||
GrantXp(_huntSkill, self, days);
|
GrantXp(_huntSkill, skills, days);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AnimalActions.Flee:
|
case AnimalActions.Flee:
|
||||||
@@ -1089,9 +1093,9 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
MoveTo(
|
MoveTo(
|
||||||
ref pos,
|
ref pos,
|
||||||
brain.Target,
|
brain.Target,
|
||||||
speed * FleeSpeedMult * SkillFactor(_evadeSkill, self) * seconds
|
speed * FleeSpeedMult * SkillFactor(_evadeSkill, skills) * seconds
|
||||||
);
|
);
|
||||||
GrantXp(_evadeSkill, self, days);
|
GrantXp(_evadeSkill, skills, days);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default: // Wander
|
default: // Wander
|
||||||
@@ -1278,14 +1282,9 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Бонус-множитель от уровня навыка: 0.7 (новичок) … 1.3 (мастер). Навык не определён модом → 1 (без эффекта).
|
// Бонус-множитель от уровня навыка: 0.7 (новичок) … 1.3 (мастер). Навык не определён модом → 1 (без эффекта).
|
||||||
private float SkillFactor(int idx, Entity self)
|
private static float SkillFactor(int idx, in Skills s)
|
||||||
{
|
{
|
||||||
if (
|
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
|
||||||
idx < 0
|
|
||||||
|| !self.TryGetComponent<Skills>(out var s)
|
|
||||||
|| s.Levels is null
|
|
||||||
|| idx >= s.Levels.Length
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
return 1f;
|
return 1f;
|
||||||
}
|
}
|
||||||
@@ -1294,14 +1293,9 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Начисляет опыт навыку от практики: рост ∝ скорость обучения × страсть × (1−уровень) (убывающая отдача).
|
// Начисляет опыт навыку от практики: рост ∝ скорость обучения × страсть × (1−уровень) (убывающая отдача).
|
||||||
private void GrantXp(int idx, Entity self, float days)
|
private void GrantXp(int idx, in Skills s, float days)
|
||||||
{
|
{
|
||||||
if (
|
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
|
||||||
idx < 0
|
|
||||||
|| !self.TryGetComponent<Skills>(out var s)
|
|
||||||
|| s.Levels is null
|
|
||||||
|| idx >= s.Levels.Length
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,14 @@ public static class AnimalCapacities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CapacityCalc
|
public static class CapacityCalc
|
||||||
{
|
{
|
||||||
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary>
|
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1);
|
||||||
public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain)
|
/// <paramref name="capacityScale"/> — общий множитель (старение), применяется к итогу без лишних аллокаций.</summary>
|
||||||
|
public static Dictionary<string, float> Compute(
|
||||||
|
PartInstance[] parts,
|
||||||
|
float blood,
|
||||||
|
float pain,
|
||||||
|
float capacityScale = 1f
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
|
// Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
|
||||||
// только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
|
// только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
|
||||||
@@ -100,10 +106,12 @@ public static class CapacityCalc
|
|||||||
|
|
||||||
// В результат — только объявленные телом способности: известные по формуле зависимостей, прочие
|
// В результат — только объявленные телом способности: известные по формуле зависимостей, прочие
|
||||||
// (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог.
|
// (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог.
|
||||||
|
var scale = Clamp01(capacityScale);
|
||||||
var result = new Dictionary<string, float>(System.StringComparer.Ordinal);
|
var result = new Dictionary<string, float>(System.StringComparer.Ordinal);
|
||||||
foreach (var capacity in declared)
|
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;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user