Design a modular character stats system in Unreal Engine for an RPG where stats (health, strength, speed, resistances) are constantly modified by equipment and timed effects. Many buffs and debuffs stack, expire, and overlap — additive and multiplicative — and removing one buff must restore exactly its contribution without corrupting the base value or other modifiers. Designers must be able to add new stats and define item/effect modifiers without rewriting the system. Gameplay and UI must react when a stat changes without polling every frame, and the current value must be cheap to recompute (not rescanned the world every tick). In multiplayer the authoritative values come from the server. Describe how you represent a stat and its modifiers, how the final value is derived and kept consistent as buffs stack and expire, how consumers are notified, and the trade-offs of your approach.
Use a UStatComponent holding each stat as a base value plus a list of modifiers; the current value is computed from base and active modifiers. Items and effects add or remove modifiers, and the component broadcasts a delegate on change.
- ✗Mutating the final stat value directly so the base value is lost when buffs stack or expire
- ✗Sharing stats in a global singleton instead of a per-character component
- ✗Recomputing stats every Tick instead of recalculating only when a modifier changes
- →How would you order additive vs. multiplicative modifiers when computing the final value?
- →How would you handle a stat like health where current value differs from max value?
Key classes
UStatComponent— aUActorComponent: owns all of a character's stats.FStat— aUSTRUCT:BaseValueand aTArray<FStatModifier> Modifiers; a cachedCurrentValue.FStatModifier— aUSTRUCT: stat type, operation (Add/Multiply/Override), magnitude, source.EStatType— an enum orFGameplayTagenumerating stats (Health, Strength, Speed).
Data model
A stat = an immutable BaseValue + a list of modifiers. The final value is recomputed from those two parts and never edited directly. A modifier remembers its source, so removing a buff drops exactly its modifiers instead of guessing a delta.
Data flow
- An item is equipped / an effect applied →
UStatComponent::AddModifier(FStatModifier). - The component marks the stat dirty and recomputes: base → additive → multiplicative → override.
- Removal →
RemoveModifiersBySource(Source), then recompute again. - When the final value changes, the component broadcasts an
OnStatChanged(Type, NewValue)delegate. - UI and gameplay subscribe to the delegate — never poll in
Tick.
Replication considerations
BaseValue and final values are replicated by the server. The modifier list can replicate for UI (to show buff sources) or stay server-only if the client only needs the final value. Applying modifiers is a server operation; an OnRep of the final value updates the client UI.
Designer-facing config
Starting BaseValues are a DataAsset or DataTable per character class. Item modifiers are set in a UItemDataAsset. Per-level growth curves are a UCurveTable. New stats are added as enum/tag values without rewriting the component.
Trade-offs
- Base + modifiers vs. a single field: the model is more complex but stacks and removes buffs correctly; a single field loses the base.
- Event-driven recompute vs. every frame: event-driven is cheaper and predictable; every frame is simpler but wastes CPU.