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

  • 74