Спроектируйте модульную систему характеристик персонажа в Unreal Engine для RPG, где характеристики (здоровье, сила, скорость, сопротивления) постоянно меняются снаряжением и эффектами с таймером. Множество баффов и дебаффов стакаются, истекают и накладываются — аддитивно и мультипликативно, — и снятие одного баффа должно восстановить ровно его вклад, не повредив базовое значение и другие модификаторы. Дизайнеры должны добавлять новые характеристики и задавать модификаторы предметов/эффектов без переписывания системы. Геймплей и UI должны реагировать на изменение характеристики без опроса каждый кадр, а текущее значение должно дёшево пересчитываться (не пересканировать мир каждый тик). В мультиплеере авторитетные значения приходят с сервера. Опишите, как вы представляете характеристику и её модификаторы, как выводится итоговое значение и держится согласованным при стакании и истечении баффов, как уведомляются потребители, и компромиссы вашего подхода.
Используйте UStatComponent, хранящий каждую характеристику как базовое значение плюс список модификаторов; текущее значение вычисляется из базы и активных модификаторов. Предметы и эффекты добавляют или убирают модификаторы, а компонент рассылает делегат при изменении.
- ✗Менять итоговое значение напрямую, теряя базовое при стакании или истечении баффов
- ✗Хранить характеристики в глобальном синглтоне вместо компонента на персонажа
- ✗Пересчитывать характеристики каждый Tick вместо пересчёта только при изменении модификатора
- →Как бы вы упорядочили аддитивные и мультипликативные модификаторы при вычислении итогового значения?
- →Как бы вы обработали характеристику вроде здоровья, где текущее значение отличается от максимума?
Ключевые классы
UStatComponent—UActorComponent: владеет всеми характеристиками персонажа.FStat—USTRUCT:BaseValueиTArray<FStatModifier> Modifiers; кэшCurrentValue.FStatModifier—USTRUCT: тип характеристики, операция (Add/Multiply/Override), величина, источник.EStatType— enum илиFGameplayTag, перечисляющий характеристики (Health, Strength, Speed).
Модель данных
Характеристика = неизменяемая BaseValue + список модификаторов. Итог пересчитывается из этих двух частей, итог никогда не правится напрямую. Модификатор помнит свой источник, поэтому снятие баффа удаляет именно его модификаторы, а не угадывает дельту.
Поток данных
- Предмет надет / эффект применён →
UStatComponent::AddModifier(FStatModifier). - Компонент помечает характеристику грязной и пересчитывает: база → аддитивные → мультипликативные → override.
- Снятие →
RemoveModifiersBySource(Source), снова пересчёт. - При изменении итога компонент рассылает делегат
OnStatChanged(Type, NewValue). - UI и геймплей подписаны на делегат — без опроса в
Tick.
Соображения по репликации
BaseValue и итоговые значения реплицируются сервером. Список модификаторов может реплицироваться для UI (показать источники баффов) или храниться только на сервере, если клиенту нужен лишь итог. Применение модификаторов — серверная операция; OnRep итога обновляет клиентский UI.
Настройка дизайнером
Стартовые BaseValue — DataAsset или DataTable на класс персонажа. Модификаторы предметов задаются в UItemDataAsset. Кривые роста по уровню — UCurveTable. Новые характеристики добавляются как значения enum/тега без переписывания компонента.
Компромиссы
- База + модификаторы vs. одно поле: модель сложнее, но корректно стакает и снимает баффы; одно поле теряет базу.
- Пересчёт по событию vs. каждый кадр: по событию дешевле и предсказуемо; каждый кадр проще, но тратит CPU зря.