2025-12-04Bruno Fernandes

Reactive Modifiable Properties in C#

This post is going to be a bit different from the usual web development focused content. I want to share little a C# package I built to handle modifiable properties in a reactive way using R3. The package is mainly built for Unity, but it's usable in other environments. Although I initially intended for it to be used in action games and RPG-style character stats, it evolved into a generic state management tool that has uses beyond gaming. If you're into game development or just curious about how to manage complex state changes in a clean and maintainable way, please check it out!

I've always been a big fan of turn-based RPG video games. The strategic depth of managing resources, planning moves, and optimizing character builds has a unique appeal.

I myself am building my own JRPG-inspired game in Unity currently (surprise!). These games need a flexible way to handle character stats that can be modified by various effects, equipment, and buffs. As an engineer who does mostly web work, I found myself missing the tools and patterns I usually rely on for managing state and side effects to handle these complex interactions, and the farther I got, the more unwieldy it became to express buffs, equipment, healing rules, and designer whimsy with plain C# fields and events in a way that makes it easy to bind these values into a UI representation. I wanted a single pipeline where “deal damage,” “cap HP at MaxHP,” “apply 20% EXP boost,” and “show the difference in stats of equipping a certain new armor” could coexist. That’s what pushed me to build com.brunocpf.modifiable-property: a reactive, disposable-friendly stat stack for C# and Unity that leans on R3 for signal flow.

Repository & Docs

Stack

  • Unity 2021+ package (UPM install via Git URL)
  • C# 9 generics with optional custom math structs
  • R3 (Cysharp) for observables and schedulers
  • Tests~/ for fast playmode/standalone verification
  • Samples~/ (in progress) for plug-and-play inspectors

Why Not Just Use Floats?

Typical RPG stat code mixes together four concerns:

  1. Accumulated state (the “real” HP).
  2. Change-time logic (is healing blocked? does a special status effect reduce damage? is there a buff that increases experience gain?).
  3. View-time modifiers (equipment, temporary buffs).
  4. Telemetry (who dealt the blow? which skill granted EXP?).

Splitting those into deltas, filters, bounds, modifiers, and observables makes it easier to reason about each aspect independently. Designers can stack effects without worrying about the order of operations when multiple buffs/debuffs are active.

The Five-Layer Flow

Raw Deltas
    ↓
Filters (change-time)
    ↓
Bounds
    ↓
Base Value
    ↓
Modifiers (view-time)
    ↓
Effective Value

Each layer is additive and deterministic. Filters can rewrite or veto deltas (changes to the base value) before they ever mutate the base. Bounds guarantee hard floors/ceilings (e.g., HP never drops below 0, certain stats never exceed 999, etc.). Modifiers transform the base value into the effective value used by gameplay systems like damage calculations or UI displays.

At any point in the pipeline, you can use R3 observables to listen for changes, log events, or trigger additional side effects in a clean, decoupled way. Observability is baked in and allows for powerful debugging.

1var hp = new ModifiableProperty<int, DamageCtx>(
2    initialValue: 120,
3    min: 0,
4    max: 120
5);
6
7// Raw delta (reduce HP by 35 from a goblin attack)
8hp.AddDelta(-35, new DamageCtx(source: goblin));
9
10// Change-time filter (e.g., Stone Skin buff that halves incoming damage, i.e., any negative delta to HP)
11using var shield = hp.PushFilter(
12    id: "stone-skin",
13    filter: d => d.Delta < 0 ? d with { Delta = (int)(d.Delta * 0.5f) } : d,
14    priority: 50
15);
16
17// View-time modifier (e.g., Emerald Pendant accessory that increases max HP by 10%)
18using var relic = hp.PushModifier(
19    id: "emerald-pendant",
20    modifier: v => (int)(v * 1.1f),
21    priority: 100
22);

Because filters and modifiers are disposable, buffs pop on/off with using scopes or via explicit lifetime tracking. You can implement equipment systems and status effects as collections of disposables that clean up when unequipped or expired, and the pipeline automatically rebuilds itself to reflect the new effective value.

You can even combine multiple effects at once with a CompositeDisposable to represent complex states like “equipped sword provides atk boost + shield + haste buff” without worrying about manually keeping track of what needs to be removed when the item is unequipped: all you need to do is dispose the composite handle.

Delta Contexts

Every AddDelta call can include structured metadata. In my project I pass around records that include a reference to the object representing the battler who owns the property (stat) and the context in which that delta was applied (skill used, level up, etc.). This allows systems to react to specific changes in a decoupled way.

1atk.ProcessedDeltas.Subscribe(delta =>
2{
3    if (delta.Context is LevelUpCtx ctx)
4    {
5        Debug.Log($"{ctx.Battler.Name}'s ATK increased by {delta.Delta} due to leveling up!");
6
7        // Trigger additional effects
8    }
9});
10
11...
12
13atk.AddDelta(5, new LevelUpCtx(battler));

Context makes the pipeline debuggable and opens up tooling: I can visualize an action-by-action breakdown just by listening to ProcessedDeltas. You can also implement complex game logic that depends on the source of changes without tightly coupling systems together.

All of this makes managing invariants easy to understand and enforce: rules are applied in a deterministic order, and view-time modifiers can’t affect the underlying state.

Encapsulation is also encouraged. I can hand out IReadOnlyModifiableProperty<T> references to UI or AI systems without exposing mutation methods.

Non-GameDev Uses

While I built this package with RPG-style character stats in mind, the underlying principles are applicable to any domain where state can be modified by multiple independent factors. Here are a few examples:

  • Financial Applications: Managing account balances with deposits, withdrawals, fees, and interest rates.
  • Inventory Management: Tracking stock levels with incoming shipments, sales, returns, and adjustments.
  • Health Monitoring: Tracking patient vitals with medication effects, lifestyle changes, and environmental factors.
  • Simulation Systems: Managing dynamic properties in simulations where multiple forces or factors influence state.

Roadmap

  1. Build a sample scene RPG-style character stats and buffs.
  2. Build a visual debugger to visualize the entire pipeline and streams in action in the Unity Editor.
  3. Experiment with serialization helpers so save data can snapshot entire pipelines (will likely need an additional abstraction layer to represent serializable effects).

You can experiment with the package by adding it to your Unity project (instructions in the repo's README). If you have any feedback or ideas, please reach out!