TinkState
Uncomplicated Reactive Library
Strength: 69
class HeroStats
{
public int Strength;
}
class HeroStatsView : MonoBehaviour
{
[SerializeField] TMP_Text strengthLabel;
public void Init(HeroStats stats)
{
strengthLabel.text = stats.Strength.ToString();
}
}
typical approach
- define events/messages
- dispatch when changing
- subscribe for observing
typical issues
- boilerplaty
- error-prone
- non-unified
- complicated
- costly
Strength: 69 + 420
Attack: 163 + 12
TinkState
interface Observable<T>
{
T Value { get; }
IDisposable Bind(Action<T> callback);
}
class HeroStats
{
public Observable<int> Strength;
}
class HeroStatsView : MonoBehaviour
{
[SerializeField] TMP_Text strengthLabel;
public void Init(HeroStats stats)
{
stats.Strength.Bind(strength => strengthLabel.text = strength.ToString());
}
}
class HeroStats
{
public Observable<int> Strength => strength;
readonly State<int> strength;
public HeroStats(int initialStrength)
{
strength = Observable.State(initialStrength);
}
public void BuffMe()
{
strength.Value *= 2;
}
}
class HeroStats
{
public Observable<int> Strength => strength;
public readonly Observable<int> AttackDamage;
readonly State<int> strength;
public HeroStats(int initialStrength)
{
strength = Observable.State(initialStrength);
AttackDamage = Observable.Auto(() => {
return Balance.CalculateDamage(strength.Value)
});
}
}
class HeroStatsView : MonoBehaviour
{
[SerializeField] TMP_Text strengthLabel;
[SerializeField] TMP_Text attackDamageLabel;
public void Init(HeroStats stats)
{
stats.Strength.Bind(strength =>
strengthLabel.text = strength.ToString());
stats.AttackDamage.Bind(damage =>
attackDamageLabel.text = damage.ToString());
}
}
class Weapon
{
public readonly int DamageBonus;
}
class HeroStats
{
public Observable<int> Strength => strength;
public readonly Observable<int> AttackDamage;
readonly State<int> strength;
readonly State<Weapon> currentWeapon;
public HeroStats(int initialStrength, Weapon initialWeapon)
{
strength = Observable.State(initialStrength);
currentWeapon = Observable.State(initialWeapon);
AttackDamage = Observable.Auto(() =>
{
var damage = Balance.CalculateDamage(strength.Value);
var weapon = currentWeapon.Value;
if (weapon != null) damage += weapon.DamageBonus;
return damage;
});
}
}
- Universal
- Declarative
- Sensible
- Just works :)
IDisposable Bind(Action<T> callback);
Binding lifetimes
class HeroStatsView : MonoBehaviour
{
[SerializeField] TMP_Text attackDamageLabel;
public void Init(HeroStats stats)
{
var binding = stats.AttackDamage.Bind(damage => attackDamageLabel.text = damage.ToString());
gameObject.DisposeOnDestroy(binding);
}
}
class HeroStatsView : MonoBehaviour
{
[SerializeField] TMP_Text attackDamageLabel;
public void Init(HeroStats stats)
{
gameObject.RunOnActive(() =>
{
var binding = stats.AttackDamage.Bind(damage => attackDamageLabel.text = damage.ToString());
return binding;
});
}
}
Performance principles
- No allocations on dispatch
- No boxing
- No unnecessary objects
- Computation result caching
- No dispatch without changes
- Binding batching
Extras
var list = Observable.List(new [] { 1, 2, 3 });
var sum = Observable.Auto(() => list.Sum());
Debug.Log(sum.Value); // 6
var searchString = Observable.State("");
var foundPlayers = Observable.Auto(async () => await FindPlayers(searchString.Value));
foundPlayers.Bind(result =>
{
switch (result.Status)
{
case AsyncComputeStatus.Loading:
ShowSpinner();
break;
case AsyncComputeStatus.Done:
ShowResult(result.Result);
break;
case AsyncComputeStatus.Failed:
LogError(result.Exception);
break;
}
});
Experiments
class HeroStats : Model
{
[Observable] public int Strength { get; private set; }
[Observable] public int AttackDamage => Balance.CalculateDamage(Strength);
}
too much magic?
THANK YOU!
TinkState
By Dan Korostelev
TinkState
- 210