De ReactJS: une organisation en composants autonomes
| public class MoralDisplay : ConnectedMonoBehaviour { |
| public Slider Slider; |
| public TextMeshProUGUI Text; |
| |
| |
| 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); |
| } |
| } |
| public class MoralDisplay : ConnectedMonoBehaviour { |
| public Slider Slider; |
| public TextMeshProUGUI Text; |
| |
| |
| 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); |
| } |
| } |
De Redux: Centralisation de l'état applicatif
- Minimal mais suffisant
- Pas de données redondantes ou calculées
- Utilisation sélecteurs réactifs pour accéder aux données calculées ou composées
- Solution pour rendre possible l'autonomie des composants
De Redux: Le composant s'abonne aux fragments de données dont il a besoin
| |
| protected override void OnSetup(State state) { |
| var diceState = state.Game.Character[this.CharacterId].Dice[this.DiceIndex]; |
| var selectedDiceState = state.Local.SelectedDice; |
| |
| Subscribe(diceState.Used, diceState.Color, (used, color) => { |
| Face.color = Configurator.GetColor(color).WithAlpha(used ? DisabledOpacity : 1f); |
| }); |
| Subscribe(diceState.Value, value => { |
| Face.sprite = Configurator.DB.DiceFaces[Mathf.Clamp(value - 1, 0, 5)]; |
| }); |
| Subscribe(selectedDiceState, selectedDice => { |
| var isSelected = selectedDice.HasValue && selectedDice.Value.DiceIndex == diceIdx; |
| if (isSelected) { |
| breathAnim.Play(); |
| } else { |
| breathAnim.ResetAndStop(); |
| } |
| }); |
| } |
| |
| protected override void OnSetup(State state) { |
| var diceState = state.Game.Character[this.CharacterId].Dice[this.DiceIndex]; |
| var selectedDiceState = state.Local.SelectedDice; |
| |
| Subscribe(diceState.Used, diceState.Color, (used, color) => { |
| Face.color = Configurator.GetColor(color).WithAlpha(used ? DisabledOpacity : 1f); |
| }); |
| Subscribe(diceState.Value, value => { |
| Face.sprite = Configurator.DB.DiceFaces[Mathf.Clamp(value - 1, 0, 5)]; |
| }); |
| Subscribe(selectedDiceState, selectedDice => { |
| var isSelected = selectedDice.HasValue && selectedDice.Value.DiceIndex == diceIdx; |
| if (isSelected) { |
| breathAnim.Play(); |
| } else { |
| breathAnim.ResetAndStop(); |
| } |
| }); |
| } |
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/
Comment construire un state et des sélecteurs observables ?
Réactivité : type réactif
| |
| |
| var reactiveProperty = new Reactive<int>(99); |
| var cancellationSource = new CancellationTokenSource(); |
| |
| reactiveProperty.ForEachAsync(x => { |
| Debug.Log(x); |
| }, cancellationSource.Token); |
| |
| property.Value = 90; |
| property.Value = 89; |
| cancellationSource.Cancel(); |
| property.Value = 77; |
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(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)); |
| } |
| } |
| 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(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)); |
| } |
| } |
| 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(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
| |
| public static RegionState GetRegion(this GameState gameState, int regionIndex) { |
| return gameState.Regions[regionIndex]; |
| } |
| |
| |
| 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(state.Local.SelectedRegion, state.Game.IsTurnOver).ForEachAsync( |
| |
| |
| (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 { |
| |
| internal CancellationTokenSource? _resetSource; |
| protected CancellationToken ResetToken => _resetSource.Token; |
| |
| protected GameManager GameManager; |
| |
| 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; |
| |
| |
| 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); |
| } |
| } |
ConnectedMonoBehaviour à l'œuvre
| |
| protected override void OnSetup(State state) { |
| var diceState = state.GetDice(this.CharacterId, transform.GetSiblingIndex()); |
| var selectedDiceState = state.Local.SelectedDice; |
| |
| Subscribe(diceState.Used, diceState.Color, (used, color) => { |
| Face.color = Configurator.GetColor(color).WithAlpha(used ? DisabledOpacity : 1f); |
| }); |
| Subscribe(diceState.Value, value => { |
| Face.sprite = Configurator.DB.DiceFaces[Mathf.Clamp(value - 1, 0, 5)]; |
| }); |
| Subscribe(selectedDiceState, selectedDice => { |
| var isSelected = selectedDice.HasValue && selectedDice.Value.DiceIndex == diceIdx; |
| if (isSelected) { |
| breathAnim.Play(); |
| } else { |
| breathAnim.ResetAndStop(); |
| } |
| }); |
| } |