Скорость, сила прыжка, рост, время подката — всё, что можно выразить числом и что меняется от экипировки, баффов и глобальных правил. Итоговое значение собирается из трёх слоёв, и каждый слой отвечает за своё:
EntityStatsBase → персональные модификаторы → GameRules → результат
базовое экипировка, питомцы, границы
значение баффы проекта
Базовое значение принадлежит виду сущности, модификаторы — конкретному экземпляру, правила — всему проекту. Порядок неизменен: сначала база, потом персональное, в конце ограничения.
flowchart TD
Stats["EntityStatsBase<br/>базовое значение вида"]
subgraph SRC["Персональные модификаторы экземпляра"]
Children["Провайдеры на дочерних объектах<br/>шляпа, питомец, набор"]
Owned["Добавленные из кода<br/>бустер, VIP, способность"]
end
Calc["(base + сумма Add) × произведение Multiply<br/>Override заменяет результат целиком"]
Rules["GameRules<br/>Min/Max проекта, последний слой"]
Value["GetStat → float"]
Round["GetStatInt / GetStatLong<br/>округление в самом конце"]
Stats --> Calc
Children --> Collector["StatModifierCollector"]
Owned --> Collector
Collector --> Calc
Delegate["Делегат-модификатор<br/>расчёт без компонента на сцене"] --> Calc
Calc --> Rules
Rules --> Value
Value --> Round
Правила применяются после модификаторов и не могут быть ими обойдены: Override
заменяет значение экземпляра, но не снимает границы проекта. Округление стоит ещё
дальше — после правил, а не между слоями.
| Тип | Роль |
|---|---|
EntityStatsBase<TEnum> |
ScriptableObject со словарём «ключ → базовое значение» |
StatModifier, StatModifier<TEnum> |
одно изменение одной характеристики |
StatModifierType |
как применять: Add, Multiply, Override |
IStatModifiersProvider, IStatModifierProvider |
источник модификаторов: шляпа, питомец, набор |
StatModifierCollector |
компонент сущности: собирает модификаторы с дочерних объектов и принимает добавленные из кода |
EntityStatsUtils |
расчёт итогового значения |
EntityStatsBase<TEnum> — ассет с таблицей характеристик. Ключи задаёт провайдер
перечислений, поэтому набор характеристик у каждого вида сущности свой:
public class PlayerStats : EntityStatsBase<PlayerStatsEnumeration> { }Ассет обычно один на вид: все игроки ссылаются на один и тот же Player stats.asset,
а различия между конкретными игроками дают модификаторы.
[SerializeField] private List<StatModifier<PlayerStatsEnumeration>> statModifiers;| Поле | Что задаёт |
|---|---|
Property |
характеристика, на которую действует модификатор |
Value |
величина |
Type |
Add, Multiply или Override |
Priority |
у Override больше — сильнее; у Add и Multiply не используется |
Порядок внутри слоя фиксирован и от порядка добавления не зависит:
(base + сумма всех Add) × произведение всех Multiply
Override отменяет весь расчёт и подставляет своё значение. Между несколькими
Override выигрывает больший Priority, при равенстве — добавленный последним.
Неродовой StatModifier — база для сериализации и общих списков; в инспекторе
и в коде используется StatModifier<TEnum>, который знает свой набор ключей.
Источник — компонент на дочернем объекте сущности: надетая шляпа, питомец рядом, активный набор предметов.
public class HatDefinition : WearableDefinitionBase<HatEntity>, IStatModifiersProvider
{
[SerializeField] private List<StatModifier<PlayerStatsEnumeration>> statModifiers;
public IEnumerable<StatModifier> StatModifiers => statModifiers.ToList();
}IStatModifierProvider — тот же контракт для источника с единственным модификатором.
StatModifierCollector вешается на корень сущности рядом с EntityLinkBase. Он
обходит дочерние объекты, складывает найденные модификаторы в свой
FloatPropertyContainer и отдаёт результат по запросу.
| Когда пересобирает | Как |
|---|---|
| При старте сущности | Start() |
| При смене экипировки | IEntityEquipmentChangedEvent для своей сущности |
| Вручную | CollectProviders() — поднимает EntityEvents.RefreshStats |
Собираются только активные дочерние объекты: выключенная шляпа модификаторов не даёт.
Бустер, VIP-статус или способность объекта в иерархии сущности не имеют, поэтому провайдером стать не могут. Они добавляют модификатор напрямую, назвав себя владельцем:
var boost = new StatModifier<PlayerStatsEnumeration>(
PlayerStatsEnumeration.WalkSpeed, 1.25f, StatModifierType.Multiply);
collector.AddModifier(this, boost);
// ...по истечении бустера
collector.RemoveModifiers(this);Модификаторы владельца переживают пересборку: при смене экипировки они применяются
заново. RemoveModifiers снимает все модификаторы одного владельца разом. Оба метода
поднимают EntityEvents.RefreshStats, поэтому подписчики пересчитают характеристики
сами.
// Обычный путь: модификаторы даёт сборщик на префабе
float speed = EntityStatsUtils.GetStat(
PlayerStatsEnumeration.WalkSpeed, Core.Stats, statCollector);
int jumps = EntityStatsUtils.GetStatInt(
PlayerStatsEnumeration.JumpCount, Core.Stats, statCollector, 1);Второй путь — передать модификаторы делегатом. Тогда стат считается без компонента на сцене: в редакторе, в предпросмотре предмета, в тесте.
float preview = EntityStatsUtils.GetStat(
PlayerStatsEnumeration.WalkSpeed, stats, (stat, value) => value * 1.2f);Оба пути заканчиваются одинаково — GameRules.ApplyStatRules(), глобальные границы
характеристики. Подробности в GameRules.
GetStatInt и GetStatLong округляют после всех модификаторов и правил, а не
в промежутке. Для дробных величин — времени, долей секунды, множителей — берите
GetStat: иначе 0.15 превратится в 0.
-
Вся цепочка считает во
float. Целые числа больше ~16.7 млн перестают быть точными, иGetStatLongрасширяет доdoubleуже испорченное значение. Сейчас все существующие характеристики физические: скорость, сила прыжка, рост. Когда через характеристики пойдут деньги или доход, это станет проблемой. Исправить можно переводом цепочки наdouble. Тогда меняется и сериализованный словарьEntityStatsBase<TEnum>, поэтому безопаснее добавить новое поле, а не менять тип у существующей коллекции. -
Целые характеристики не решают проблему.
Multiplyнад целым обрубает дробную часть на каждом шаге. Нужен один достаточно широкий тип, и округлять один раз в конце. -
Нечисловые характеристики в эту систему не ложатся. Для «разрешён двойной прыжок» или «тип брони» операции
AddиMultiplyбессмысленны. Булевы решения из нескольких источников согласует FlagsSystem, в характеристики их не тянут. -
Поиск характеристики линейный.
EntityStatsBase<TEnum>.TryGetперебирает весь словарь и на каждой итерации вызываетToEnumeration(). Для характеристик, читаемых каждый кадр, это заметно — кешируйте результат.
- Entity — сущности, описание, реестры и время жизни
- PropertyContainer — устройство слоя персональных модификаторов
- GameRules — глобальные границы характеристик
- Enumeration — ключи характеристик
Окно показывает модификатор строкой «Название: ×2». Название — из StatLabels: характеристику заводит игра,
она же регистрирует переводимое название (StatLabels.Register(stat, provider)) до показа окна. Без регистрации
пишется имя перечисления. Так каталог питомцев (PetInfoWindow) пишет «Сила: ×1,5» для StoneSkill StoneSkipping.