De Flux et Redux: la circulation des données en sens unique
structure proposée par flux et redux
- Source:
https://facebook.github.io/flux/docs/in-depth-overview/
Implémentation
Comment construire un state et des sélecteurs observables ?
Réactivité : type réactif
// Implémentation basée sur AsyncReactiveProperty de UniTask
// (https://github.com/Cysharp/UniTask)
var reactiveProperty = new Reactive<int>(99);
var cancellationSource = new CancellationTokenSource();
reactiveProperty.ForEachAsync(x => { // Démarre la subscription grace à LINQ
Debug.Log(x); // Réagit à chaque changement de valeur
}, cancellationSource.Token); // Arrête la subscription grace à un token
property.Value = 90; // log 90
property.Value = 89; // log 89
cancellationSource.Cancel();
property.Value = 77; // aucun log
Réactivité : état réactif simple
public class CharacterState {
public readonly Reactive<byte> Health;
public CharacterState(Reactive<byte> health) {
Health = new(health);
}
public void UpdateValue(CharacterState nextValue) {
Health.Value = next.Health;
}
}
Réactivité : état réactif composé
public class GameState {
public readonly Reactive<byte> Moral;
public readonly Dictionary<CharacterId, CharacterState> Characters;
public GameState(Reactive<byte> moral, Dictionary<CharacterId, CharacterState> characters) {
Moral = new(moral);
Characters = new();
// UpdateDictionary(modifyMe, targetValue, howToUpdate)
UpdateDictionary(Characters, characters, (curr, next) => curr.UpdateValue(next));
}
public void UpdateValue(GameState nextValue) {
Moral.Value = nextValue.Moral.Value;
UpdateDictionary(Characters, characters, (curr, next) => curr.UpdateValue(next));
}
}
Outils de lecture du state
Les accesseurs pour faciliter l'utilisation et découpler l'utilisation du state de sa forme
Des sélecteurs pour mettre en cache des données calculées et les exposer réactivement
Un composant surchargeable qui fournit les accès au contexte de l'application
(ConnectedMonoBehaviour)
Accesseurs
faciliter l'utilisation et découpler l'utilisation du state de sa forme
// Get region state
public static RegionState GetRegion(this GameState gameState, int regionIndex) {
return gameState.Regions[regionIndex];
}
// Check if an item is in the designated character's inventory.
public static bool CharactersHaveItem(this GameState gameState, CharacterId c, ItemId i) {
foreach (var inventorySlot in gameState.Characters[c].Inventory.Slots) {
if (inventorySlot.Item.Value == i && inventorySlot.Quantity.Value > 0) return true;
}
return false;
}
Sélecteurs
Mettre en cache des données calculées et les exposer réactivement
var canMoveToRegionSelector = new Reactive<bool>();
// CombineLatest est un outil de Unitask pour aggréger des sources réactives
CombineLatest(state.Local.SelectedRegion, state.Game.IsTurnOver).ForEachAsync(
// Met à jour canMoveToRegionSelectors à chaque fois que la valeur de
// SelectedRegion ou IsTurnOver est modifié
(region, turnOver) => canMoveToRegionSelectors.Value = region != null && !turnOver,
cancelSelectorsToken
);
Comment construire un composant réactif ?
ConnectedMonoBehaviour
Un composant surchargeable qui donne accès au contexte.
public abstract class ConnectedMonoBehaviour : MonoBehaviour {
// En fin de vie on annulera le token pour débrancher toutes ses subscriptions
internal CancellationTokenSource? _resetSource;
protected CancellationToken ResetToken => _resetSource.Token;
// Expose automatiquement une instance qui contient le State de l'application
protected GameManager GameManager;
// Ici le composant pourra se brancher au state et événements
protected abstract void OnSetup(State state);
protected virtual void OnEnable() {
if (!_isSetup) {
_isSetup = true;
_resetSource?.Cancel(false);
_resetSource = new CancellationTokenSource();
OnSetup(GameManager.State);
}
}
public virtual void Dispose() {
_resetSource?.Cancel(false);
_isSetup = false;
}
protected virtual void OnDestroy() {
Dispose();
}
}
ConnectedMonoBehaviour à l'œuvre
public class MoralDisplay : ConnectedMonoBehaviour {
public Slider Slider;
public TextMeshProUGUI Text;
// On regroupe vue et comportements
protected override void OnSetup(State state) {
Slider.maxValue = MoralInitialValue;
Subscribe(state.Game.Moral, moral => {
Slider.value = moral;
Text.text = $"{moral}<#767676>/{Slider.maxValue}";
});
OnClick(Slider, () => {
OkModal.Open("Si le moral tombe à 0, vous perdez la partie.");
}, TimeSpan.Zero);
}
}