TinkState
Uncomplicated Reactive Library
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10995824/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997789/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997786/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10995820/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10999395/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997799/dwarf_m_256.png)
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
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997799/dwarf_m_256.png)
Strength: 69 + 420
Attack: 163 + 12
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997833/SwordT1.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997834/GemYellow.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997836/FrameSquare.png)
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;
}
});
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997945/pasted-from-clipboard.png)
Experiments
class HeroStats : Model
{
[Observable] public int Strength { get; private set; }
[Observable] public int AttackDamage => Balance.CalculateDamage(Strength);
}
too much magic?
THANK YOU!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10997799/dwarf_m_256.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998127/AxeDoubleT2.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998129/WandT2.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998130/BowT2.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998131/PotionRed.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998133/GemBlue.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998135/Coin.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998135/Coin.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998135/Coin.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998137/Scroll.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/80378/images/10998141/ShieldSmallT1.png)
TinkState
By Dan Korostelev
TinkState
- 138