Skip to content

Attributes — FAQ

My modifier isn't doing anything

Check in this order, because they are ordered by how often they are the answer:

  1. Is a combiner wired? Without an IAttributeCombiner on the AttributeSet — inspector slot or SetCombiner — providers are not consulted at all and reads return clamped base values. This is the designed default, not a failure, and the container says so once in the console when it finds providers it has no combiner for. The framework ships no combiner on purpose: how contributions stack is your design decision.
  2. Is the provider's enabled box ticked? A disabled provider does not run. One on a merely inactive GameObject still does — the semantics settled framework-wide.
  3. Is the provider on the owner's chain? Discovery is GetComponentsInParent from the AttributeSet — a provider on a sibling or a child contributes nothing.
  4. Is the id exactly right? Ids are trimmed but case-sensitive, compared ordinal: "Strength" and "strength" are different attributes.
  5. Did the combiner throw, or return NaN? Both are reported in the console and the read falls back to the base value — a broken formula looks like "modifiers ignored".

Where do I put my stacking formula?

In an IAttributeCombiner your project owns — it is a few lines of C#, wired once. See Integration Surfaces for a worked example. There is no shipped default, and that is a boundary rather than a backlog item: read System Boundaries before waiting for one.

Why is there no Strength / Level / Reputation attribute built in?

Because a shipped stat sheet decides what kinds of characters every game has, and that is your decision. An attribute is created by authoring a row or by the first SetBaseValue with a new id — your vocabulary, not the framework's. Even the Crafting adapter's level is only a default string on the component.

My HUD shows a stale effective value

Effective values are computed reads and deliberately have no change event — the container cannot know when a provider's answer changes. Subscribe to BaseValueChanged for the owned half, and re-read TryGetValue at the moments your game changes something (equip, unequip, status applied). If you drive a bar from one number, decide which number: TryGetBaseValue is the stored value, TryGetValue is the answer after contributions.

Can values and bounds go negative?

Yes, both, everywhere. The container imposes no sign convention. What it refuses is silence: a non-finite value is refused loudly, and an inverted bound pair (min above max) is reported and ignored rather than quietly rewritten. If a bound seems not to apply, check the console — an inverted pair authoring warning is exactly that symptom.

Why does an attribute I created in code ignore the min and max I authored?

Because bounds are authored configuration, and an id created by SetBaseValue has no authoring to read them from. The store says so in its own words: "An id created at runtime has no bounds." The inspector rows configure the ids they name and nothing else, so an id your code mints is unbounded however the rows above it look — and that is the deliberate shape rather than an oversight. A container that invented a range for a value it was never told about would be deciding policy about something the game owns.

What is not deliberate is finding out the hard way, so the container says it: create an id in a set that authors bounds on any row and you get one warning naming that id. Once per id, and only when bounds are authored somewhere — a set with no bounds at all has promised nothing, and creating attributes in code is an ordinary thing to do.

If the id should have a range, either author it, or clamp in the code that decides what the value means. The second is not a workaround: the range is a design decision about your values, and the code that knows what they mean is the only place that can apply one to an id the inspector never saw.

What gets saved, and what happens to attributes created at runtime?

Base values only — never effective values, contributions or bounds; whichever system grants a contribution saves its own half, which is what keeps every fact in the file written exactly once. Restore is a rewind: authored rows are re-seeded, saved values applied on top, and a runtime-created attribute the save does not mention is gone afterwards — at the moment the save was taken, it did not exist. An authored row the save predates returns to its authored base, so adding attributes to your game does not break old saves.

Two identity rules worth knowing: an AttributeSet without a StableId is not saved at all (there would be no way to find it again), and a saved id whose authored row was deleted restores as a runtime-created attribute — ids are data, so nothing dangles.

Crafting isn't gating by my level attribute

Check in order:

  1. Is AttributeLevelSource on the crafting owner's chain? LevelGateValidator finds the nearest ICraftingLevelSource by GetComponentInParent from the owner.
  2. Does the configured attribute id match? The component reads level by default; if your attribute is character-level, set the id on the component.
  3. Is the fallback doing the gating? When no attribute resolves, the component reports its serialized fallback (default 0) — and a found source is authoritative, so a fallback of 0 gates below LevelGateValidator's default minimum of 1. That failure looks like "crafting always blocked"; the fix is the fallback field, or the missing AttributeSet.
  4. Remember the floor. A character at 4.9 is level 4. Partial progress toward a level is not that level.

Does deleting Attributes break anything?

No. The core references no other system, every adapter and the save participant are define-gated on REV_ATTRIBUTES_PRESENT, and every test assembly carries the same constraint — delete Runtime/Systems/Attributes and all of it compiles out. A save file containing an Attributes section then reports that section as unrecognised, which the save system treats as carryable rather than an error.

The two questions that resolve most confusion

Is there a combiner? decides whether contributions exist at all. Base or effective? decides which number you should be reading. Most surprises trace to one of the two.