Repository navigation
Reactive
Sakura have a built-in observable value primitive, we call it Reactive<T>. It used for anything that need to be observed or synchronize across components (e.g. config settings, volume level, window modes and more)
var health = new Reactive<int>(100);
// subscribe to changes
health.ValueChanged += e =>
Logger.Debug($"Health changed from {e.OldValue} to {e.NewValue}");
health.Value = 80; // fires the event: OldValue=100, NewValue=80
health.Value = 80; // no event — value didn't change`ValueChangedEvent carries both the old and new value so you can react to the delta without storing the previous value yourself.
Use .Value or use built-in implicit conversion:
int current = health.Value; // explicit
int current = health; // implicit — same thingvar speed = new Reactive<float>(1.0f);
speed.Value = 2.5f;
bool isDefault = speed.IsDefault; // false
speed.Value = speed.Default; // back to 1.0f, fires eventBinding wires two Reactive<T> objects together so that when one changes, the other automatically follows. This is how the framework synchronises config settings with runtime state — the config manager holds the authoritative value and the runtime system binds to it.
var source = new Reactive<double>(1.0);
var follower = new Reactive<double>(0.0);
follower.BindTo(source); // follower.Value is now 1.0 (immediately updated on bind)
source.Value = 0.5; // follower.Value is now 0.5 automaticallyNote : The binding is one-directional -> follower watches source, not the other way around. Changing follower.Value directly does not affect source.
follower.UnbindFrom(source);
follower.UnbindAll();When your application or game changes the master volume slider, that slider's Reactive<double> is bound to AudioManager.MasterVolume, which in turn is bound to the config manager's persisted FrameworkSetting.MasterVolume setting. Changing the slider value propagates through the chain automatically — you never manually copy values between layers.
The framework provide some subclasses for common primitives:
| Type | Use case |
|---|---|
| Reactive | Any reference or value type |
| ReactiveBool | Boolean flags (extends Reactive) |
| ReactiveInt | Integer values with min/max clamping |
| ReactiveFloat | Float values with min/max clamping |
| ReactiveNumber | Base for numeric reactives |
| --- | --- |
The numeric types add MinValue and MaxValue constraints so the value is automatically clamped when set.
var volume = new ReactiveFloat(1.0f) { MinValue = 0f, MaxValue = 1f };
volume.Value = 1.5f; // clamped to 1.0f
volume.Value = -0.3f; // clamped to 0.0fSet Disabled to true make the reactive read-only, all write via .Value, BindTo updates and Parse are silently ignored :
var locked = new Reactive<int>(42);
locked.Disabled = true;
locked.Value = 99; // ignored
Console.WriteLine(locked.Value); // still 42Every framework setting is a Reactive<T> retrieved from FrameworkConfigManager (or your own ConfigManager subclass). When you get a setting, you get a live reactive object. Any change to it is automatically saved to disk:
// Getting a setting — always returns the same Reactive<T> for the same key
Reactive<double> masterVolume = Host.FrameworkConfigManager.Get<double>(FrameworkSetting.MasterVolume);
// Changing it writes to disk and fires ValueChanged
masterVolume.Value = 0.8;The framework internally uses BindWith (BindTo convenience from the config manager) to connect these reactives to the runtime subsystems. In App.Load():
var masterVolume = Host.FrameworkConfigManager.Get<double>(FrameworkSetting.MasterVolume);
AudioManager.MasterVolume.BindTo(masterVolume);From this point, changing AudioManager.MasterVolume.Value updates the persisted setting, and loading a saved config updates the audio system — both directions flow correctly because the audio manager also subscribes to ValueChanged.
See current available in FrameworkConfigManager
For your own settings :
[SettingSource("mygame.ini")]
public enum MyAppSetting {
MusicVolume,
SfxVolume,
Difficulty,
CustomSettingSomething
}
public class MyConfigManager : ConfigManager<MyAppSetting>
{
public MyConfigManager(Storage storage) : base(storage)
{
Get(MyAppSetting.MusicVolume, 1.0);
Get(MyAppSetting.SfxVolume, 1.0);
Get(MyAppSetting.Difficulty, 1);
Get(MyAppSetting.CustomSettingSomething, true);
}
}
// In App.Load():
var myConfig = new MyConfigManager(Storage);
myConfig.Load();
// Bind game audio to your config
AudioManager.TrackVolume.BindTo(myConfig.Get<double>(MyAppSetting.MusicVolume));
AudioManager.SampleVolume.BindTo(myConfig.Get<double>(MyAppSetting.SfxVolume));
// Also adjust custom setting
myConfig.Get<bool>(MyAppSetting.CustomSettingSomething).ValueChanged += e => {
if (e.NewValue)
enableSomething();
else
disableSomething();
}We really recommend you to use the reactive for sync the state for multiple UI component and more components.
public class Player
{
public Reactive<int> Health { get; } = new ReactiveInt(100) { MinValue = 0, MaxValue = 100 };
public Reactive<bool> IsDead { get; } = new ReactiveBool(false);
public Player()
{
Health.ValueChanged += e =>
{
if (e.NewValue <= 0)
IsDead.Value = true;
};
}
}
// UI sync
player.Health.ValueChanged += e => healthBar.SetValue(e.NewValue);
player.IsDead.ValueChanged += e => { if (e.NewValue) showDeathScreen(); };
// Play sound when hurt
player.Health.ValueChanged += e =>
{
if (e.NewValue < e.OldValue)
hurtSound.Play();
};-
ValueChangeddoes not fire if the new value equals the current value — equality is checked first. This means you can safely set.Valuein a loop without triggering cascading events. - Bindings do not prevent the follower from being set independently. If you call
follower.Value = xdirectly, the event fires and the value changes — the binding only means changes from source also propagate. If you want a truly read-only reactive driven by a source, setDisabled = trueon the follower after binding. -
ConfigManager.Get<T>always returns the sameReactive<T>instance for a given key. Call it from multiple places safely — they all get the same object.