Skip to content

Latest commit

 

History

History
192 lines (141 loc) · 11.8 KB

README.md

File metadata and controls

192 lines (141 loc) · 11.8 KB

Характеристики сущности

Скорость, сила прыжка, рост, время подката — всё, что можно выразить числом и что меняется от экипировки, баффов и глобальных правил. Итоговое значение собирается из трёх слоёв, и каждый слой отвечает за своё:

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
Loading

Правила применяются после модификаторов и не могут быть ими обойдены: 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.