Net State Machine
v0.4.0
Une bibliothèque de création de machines d'état pour .NET.
L'exemple suivant montre certaines fonctions de la machine à états.
en utilisant Enderlook.StateMachine ; classe publique Character{private static StateMachineFactory<States, Events, Character> ? factory;private readonly Random rnd = new();private readonly StateMachine<States, Events, Character> stateMachine;private int health = 100;private int food = 100;private enum States{Sleep,Play,GettingFood,Hunt,Gather,} Événements d'énumération privée{HasFullHealth,LowHealth,IsHungry,IsNoLongerHungry,}tâche asynchrone statique publique Main(){Character Character = new();while (true){Console.Clear();// Exécute un appel de mise à jour de la machine à états et lui transmet un paramètre arbitraire.// Le paramètre est générique donc il ne le fait pas allouer sur les types de valeur.// Ce paramètre est passé au délégué abonné qui accepte le type d'argument générique dans sa signature.// Si vous ne souhaitez pas transmettre de paramètre, vous pouvez supprimer l'appel de méthode .With().// Ceci Le système de paramètres peut également être utilisé avec les méthodes d'événement d'incendie.character.stateMachine.With(character.rnd.NextSingle()).Update();Console.WriteLine($"State: {character.stateMachine.CurrentState}.");Console.WriteLine( $"Santé : {character.health}.");Console.WriteLine($"Nourriture : {character.food}.");attendre Task.Delay(10).ConfigureAwait(false);}}public Character(){// Crée une instance de la machine à états.stateMachine = GetStateMachineFactory().Create(this); // Alternativement, si vous souhaitez transmettre des paramètres à l'initialisation de la machine à états, vous pouvez faire : // stateMachine = GetStateMachineFactory().With(parameter).Create(this). // La méthode `.With(parameter)` peut être concaténée autant de fois que nécessaire. // Le modèle `stateMachine.With(p1).With(p2)...With(pn).SomeMethod(...)` est également valide pour les méthodes `Fire()`, `FireImmediately()` et `Update ()`.}StateMachineFactory statique privé<States, Events, Character> GetStateMachineFactory(){if (l'usine n'est pas nulle)return factory;StateMachineFactory<States, Événements, personnage>? factory_ = StateMachine<States, Events, Character>// Les machines à états sont créées à partir d'usines, ce qui rend la création de plusieurs instances// moins chères en CPU et en mémoire puisque le calcul est effectué une seule fois et partagé entre les instances créées..CreateFactoryBuilder()// Détermine l'état initial de la machine à états.// Le deuxième paramètre détermine comment les délégués OnEntry doivent être exécutés lors de l'initialisation de la machine à états,// InitializationPolicy.Ignore signifie qu'ils ne doivent pas être exécutés. run..SetInitialState(States.Sleep, InitializationPolicy.Ignore)// Configure un état..In(States.Sleep)// Exécuté chaque fois que nous entrons dans cet état..OnEntry(() => Console.WriteLine("Going au lit."))// Exécuté à chaque fois que nous sortons de cet état..OnExit(() => Console.WriteLine("Se lever."))// Exécuté chaque fois que la méthode de mise à jour (soit Update() ou With<T>(T).Update()) est exécutée et se trouve dans cet état.// Tous les événements fournissent une surcharge pour transmettre un destinataire, afin qu'il puisse être paramétré pendant la construction d'instances concrètes.// Fournit également une surcharge pour transmettre un paramètre de type arbitraire, afin qu'il puisse être paramétré lors de l'appel de With<T>(T).Update().// Fournit également une surcharge pour transmettre à la fois un destinataire et un paramètre de type arbitraire.// Cela surcharge s'applique également aux méthodes OnEntry(...), OnExit(...), If(...) et Do(...) ..OnUpdate(@this => @this.OnUpdateSleep()).On(Events .HasFullHealth)// Exécuté chaque fois que cet événement est déclenché dans cet état..Do(() => Console.WriteLine("Pick toys."))// Nouvel état à transiter..Goto(States.Play)// Alternativement, vous pouvez configurer la politique d'exécution des événements pendant la transition.// L'appel de méthode ci-dessus est équivalent à:// .OnEntryPolicy(TransitionPolicy.ChildFirstWithCulling).OnExitPolicy(TransitionPolicy.ParentFirstWithCulling).Goto(States.Play)..On(Events.IsHungry)// Exécuter uniquement le prochain appel si la condition est vraie..If (@this => @this.IsVeryWounded())// Nous restons dans notre état actuel sans exécuter les délégués OnEntry ni OnExit..StaySelf()// La méthode ci-dessus est un raccourci de:// .OnEntryPolicy(TransitionPolicy.Ignore).OnExitPolicy(TransitionPolicy.Ignore).Goto(States.Sleep).// Si nous voulions pour exécuter ces délégués, nous pouvons utiliser:// .GotoSelf(false)// Quel est le raccourci de:// .OnEntryPolicy(TransitionPolicy.ChildFirstWithCullingInclusive).OnExitPolicy(TransitionPolicy.ParentFirstWithCullingInclusive).Goto(States.Sleep).// Si en plus, nous voulions exécuter des délégués de transition à partir de ses états parents (ce qui n'est pas utile dans cet exemple puisque State.Sleep n'est pas un sous-état), nous pouvons faire:// .GotoSelf(true)// Quel est le raccourci de:// .OnEntryPolicy(TransitionPolicy.ChildFirst).OnExitPolicy(TransitionPolicy.ParentFirst).Goto(States.Sleep).// Sinon, exécutez l'appel suivant si la condition est vraie..If (@this => @this.IsWounded()). Goto(States.Gather)// Sinon, exécutez-le sans condition..Goto(States.Hunt)// Ignorez cet événement dans ce transition.// (Si nous n'ajoutons pas ceci et que nous déclenchons accidentellement cet événement, une exception est levée)..Ignore(Events.LowHealth)// Quel est le raccourci de:// .On(Events.LowHealth).OnEntryPolicy (TransitionPolicy.Ignore).OnExitPolicy(TransitionPolicy.Ignore).Goto(States.Sleep)..In(States.Play).OnUpdate (@this => @this.OnUpdatePlay()).On(Events.IsHungry).If(@this => @this.IsWounded()).Goto(States.Gather).Goto(States.Hunt).In(States.GettingFood). OnEntry(() => Console.WriteLine("Aller chercher de la nourriture.")).OnExit(() => Console.WriteLine("Arrêtez d'aller chercher de la nourriture.")).In(States.Gather)// Détermine que cet état est un sous-état d'un autre.// Cela signifie que les délégués OnUpdate dans l'état parent seront également exécutés.// En fonction également de OnEntryPolicy et OnExitPolicy configurés lors des transitions,// les délégués OnEntry et OnExit abonnés dans cet état peut être exécuté pendant les transitions dans les sous-états..IsSubStateOf(States.GettingFood).OnUpdate((Caractère @this, paramètre float) => @this.OnUpdateGather(parameter)).On(Events.IsNoLongerHungry).If(@this => @this.IsWounded()).Goto(States.Sleep).Goto(States.Play).On(Events. HasFullHealth).Goto(States.Hunt).In(States.Hunt).IsSubStateOf(States.GettingFood).OnEntry(() => Console.WriteLine("Prenez l'arc.")).OnExit(() => Console.WriteLine("Lâchez l'arc.")).OnUpdate((Caractère @this, paramètre flottant) => @this.OnUpdateHunt(paramètre)) .On(Events.IsNoLongerHungry).Goto(States.Sleep).On(Events.LowHealth).Goto(States.Sleep).Finalize();// Le est utile pour réduire l'utilisation de la mémoire dans les situations multithreading.// En effet, la fabrique contient des données communes entre les instances,// donc si deux instances sont créées à partir de deux usines différentes, cela consommera plus de mémoire// que deux instances créées à partir de la même usine .Interlocked.CompareExchange(ref factory, factory_, null);return factory;}private bool IsVeryWounded() => santé <= 50;private bool IsWounded() => santé <= 75;private void OnUpdateHunt(float chance){food += (int)MathF.Round(rnd.Next(8) * chance);if (food >= 100){food = 100;stateMachine.Fire(Events.IsNoLongerHungry); // Alternativement, si vous souhaitez passer des paramètres à l'initialisation de la machine à états, vous pouvez faire : // stateMachine.With(paramter).Fire(Events.IsNoLongerHungry);}health -= (int)MathF.Round(rnd.Next(6) * (1 - chance));if (health <= 20)stateMachine. Fire(Events.LowHealth);}private void OnUpdateGather(float chance){food += (int)MathF.Round(rnd.Next(3) * chance);if (food >= 100){food = 100;stateMachine.Fire(Events.IsNoLongerHungry);}if (rnd.Next(1) % 1 = = 0){santé++;si (santé >= 100){santé = 100;stateMachine.Fire(Events.HasFullHealth);}}}vide privé OnUpdatePlay(){food -= 3;if (food <= 0){food = 0;stateMachine.Fire(Events.IsHungry);}}vide privé OnUpdateSleep(){santé++;if (santé >= 100){santé = 100;stateMachine.Fire(Events.HasFullHealth);}nourriture -= 2;if (nourriture <= 0){nourriture = 0;stateMachine.Fire(Events.IsHungry);}}}
classe publique scellée StateMachine<TState, TEvent, TRecipient>where TState : notnullwhere TEvent : notnull{/// Obtenir l'état (sous)actuel de cette machine à états.public TState CurrentState { get ; }/// Récupère l'état (sous-)état actuel et toute sa hiérarchie d'état parent.public ReadOnlySlice<TState> CurrentStateHierarchy { get; }/// Get accepte les événements par current (sub)state.public ReadOnlySlice<TEvent> CurrentAcceptedEvents { get; }/// Crée une usine builder.public static StateMachineBuilder<TState, TEvent, TRecipient> CreateFactoryBuilder();/// Obtient l'état parent de l'état spécifié./// Si l'état n'est pas un sous-état, renvoie false.public bool GetParentStateOf(TState state, [NotNullWhen(true)] out TState? parentState);/// Obtient la hiérarchie parent de l'état spécifié. Si l'état n'est pas un sous-état, renvoie empty.public ReadOnlySlice<TState> GetParentHierarchyOf(TState state);/// Récupère les événements acceptés par l'état spécifié.public ReadOnlySlice<TEvent> GetAcceptedEventsBy(TState state);/// Détermine si l'état actuel est l'état spécifié ou un sous-état (imbriqué) de cet état spécifié.public bool IsInState(TState state);/// Déclenche un événement sur la machine à états./// Si la machine à états déclenche déjà un état, elle est mise en file d'attente pour s'exécuter après la fin de l'événement en cours.public void Fire(TEvent @event);/// Déclenche un événement sur la machine à états./// L'événement ne sera pas mis en file d'attente mais réellement exécuté, en ignorant les événements précédemment mis en file d'attente./// Si les événements suivants sont mis en file d'attente pendant l'exécution des rappels de cet événement, ils seront également exécutés. après la fin de cet événement.public void FireImmediately(TEvent @event);/// Exécute les rappels de mise à jour enregistrés dans l'état actuel.public void Update();/// Stocke un ou plusieurs paramètres qui peuvent être transmis à l'abonné délégués.public ParametersBuilder With<T>(paramètre T);public readonly struct ParametersBuilder{/// Stocke un paramètre qui peut être transmis à callbacks.public ParametersBuilder With<TParameter>(paramètre TParameter);/// Identique à Fire(TEvent) dans la classe parent mais inclut toutes les valeurs stockées qui peuvent être transmises aux délégués abonnés.public void Fire(TEvent);/// Identique à FireImmediately(TEvent) ) dans la classe parent mais inclut toute la valeur stockée qui peut être transmise aux délégués abonnés. public void FireImmediately(TEvent);/// Identique à Update(TEvent) dans la classe parent mais inclut toute la valeur stockée qui peut être transmise à délégués abonnés.public void Update(TEvent);}public readonly struct InitializeParametersBuilder{/// Stocke un paramètre qui peut être transmis à callbacks.public InitializeParametersBuilder With<TParameter>(Paramètre TParameter);/// Crée la machine d'état.public StateMachine <TState, TEvent, TRecipient> Create(TRecipient destinataire);}}classe publique scellée StateMachineFactory<TState, TEvent, TRecipient>where TState : notnullwhere TEvent : notnull{/// Crée une machine à états configurée et initialisée à l'aide de la configuration fournie par cette usine.public StateMachine<TState, TEvent, TRecipient> Create(TRecipient destinataire);/// Stocke un paramètre(s) pouvant être transmis aux délégués abonnés.public StateMachine<TState, TEvent, TRecipient >. initializationPolicy` détermine comment les délégués abonnés aux fournts OnEntry de l'état spécifié (et des états parents) seront exécuté lors de l'initialisation de l'état machine.public StateMachineBuilder<TState, TEvent, TRecipient> SetInitialState(TState state, ExecutionPolicy initializationPolicy = ExecutionPolicy.ChildFirst);/// Ajouter un nouvel état ou charger un state.public StateBuilder<TState précédemment ajouté. TEvent, TRecipient> In(TState state);/// Crée une fabrique en utilisant comme configuration le builder.public StateMachineFactory<TState, TEvent, TRecipient> Finalize();}classe publique scellée StateBuilder<TState, TEvent, TRecipient> : IFinalisablewhere TState : notnullwhere TEvent : notnull{/// Avance l'appel à StateMachineBuilder<TState, TEvent, TRecipient> .In(État TState).public StateBuilder<TState, TEvent, TRecipient> In(TState state);/// Avance l'appel à StateMachineBuilder<TState, TEvent, TRecipient>.Finalize();public StateMachineFactory<TState, TEvent, TRecipient> Finalize();/// Marque cet état comme sous-état de le state.public StateBuilder<TState, TEvent, TRecipient> IsSubStateOf(TState) spécifié state);/// Détermine une action à exécuter lors de l'entrée dans cet état.public StateBuilder<TState, TEvent, TRecipient> OnEntry(Action action);/// Identique à OnEntry(Action) mais transmet le destinataire en tant que paramètre.public StateBuilder <TState, TEvent, TRecipient> OnEntry(Action<TRecipient> action);/// Identique à OnEntry(Action) mais transmet au délégué tout paramètre passé lors de l'appel qui correspond au type de paramètre générique./// Si aucun paramètre passé avec le paramètre générique spécifié n'est trouvé, il est ignoré.public StateBuilder<TState, TEvent, TRecipient> OnEntry<TParameter>(Action<TParameter> action);/// Version combinée de OnEntry(Action<TRecipient>) et OnEntry(Action<TParameter>).public StateBuilder<TState, TEvent, TRecipient> OnEntry<TParameter>(Action<TRecipient, TParameter> action);/// Détermine une action à exécuter à la sortie de cet état.public StateBuilder<TState, TEvent, TRecipient> OnExit(Action action);/// Identique à OnExit( Action) mais transmettez le destinataire en tant que paramètre.public StateBuilder<TState, TEvent, TRecipient> OnExit(Action<TRecipient> action);/// Identique à OnExit(Action) mais transmet au délégué tout paramètre passé lors de l'appel qui correspond au type de paramètre générique./// Si aucun paramètre passé avec le paramètre générique spécifié n'est trouvé, il est ignoré.public StateBuilder<TState, TEvent, TRecipient > OnExit<TParameter>(Action<TParameter> action);/// Version combinée de OnExit(Action<TRecipient>) et OnExit(Action<TParameter>).public StateBuilder<TState, TEvent, TRecipient> OnExit<TParameter>(Action<TRecipient, TParameter> action);/// Détermine une action à exécuter lors de la mise à jour de cet état.public StateBuilder<TState, TEvent, TRecipient> OnUpdate(Action action) ;/// Identique à OnUpdate(Action) mais transmet le destinataire en tant que paramètre.public StateBuilder<TState, TEvent, TRecipient> OnUpdate(Action<TRecipient> action);/// Identique à OnUpdate(Action) mais transmet au délégué tout paramètre passé lors de l'appel qui correspond au paramètre générique type.public StateBuilder<TState, TEvent, TRecipient> OnUpdate<TParameter>( Action<TParameter> action);/// Version combinée de OnUpdate(Action<TRecipient>) et OnUpdate(Action<TParameter>)./// Si aucun paramètre n'est passé avec le paramètre générique spécifié est trouvé, il est ignoré.public StateBuilder<TState, TEvent, TRecipient> OnUpdate<TParameter>(Action<TRecipient, TParameter> action);/// Ajoute un comportement qui est exécuté lors du déclenchement de l'événement spécifié. public TransitionBuilder<TState, TEvent, TRecipient, StateBuilder<TState, TEvent, TRecipient>> On(TEvent @event);/// Ignore l'événement spécifié./// Si aucun comportement n'est ajouté à un événement et qu'il est déclenché, il sera lancé. Cela empêche le lancement en ignorant l'appel du tout.public StateBuilder<TState, TEvent, TRecipient> Ignore(TEvent @event);}classe scellée publique TransitionBuilder<TState, TEvent, TRecipient, TParent> : IFinalisable, ITransitionBuilder<TState>where TState : notnullwhere TEvent : notnull{/// Ajoute une sous-transition qui est exécutée lorsque le délégué renvoie true.public TransitionBuilder<TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If(Func<bool> guard);/// Identique à If(Func<bool>) mais transmet le destinataire en tant que paramètre.public TransitionBuilder< TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If(Func<TRecipient, bool> guard);/// Identique à If(Func<bool>) mais transmet au délégué tout paramètre passé lors de l'appel qui correspond au paramètre générique type.public TransitionBuilder<TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If<TParameter>(Func<TParameter, bool> guard);/// Version combinée de If(Func<TRecipient, bool>) et If(Func<TParameter, bool>).public TransitionBuilder<TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If<TParameter>(Func<TParameter, bool > guard);/// Détermine une action à exécuter lorsque l'événement est déclenché.public TransitionBuilder<TState, TEvent, TRecipient, TParent> Do(Action action);/// Identique à Do(Action) mais transmet le destinataire en tant que paramètre.public TransitionBuilder<TState, TEvent, TRecipient, TParent> Do(Action<TRecipient> action);/// Identique comme Do(Action) mais transmettez au délégué tout paramètre passé lors de l'appel qui correspond au type de paramètre générique./// Si aucun paramètre passé avec le paramètre générique spécifié n'est trouvé, il est ignoré.public TransitionBuilder<TState, TEvent, TRecipient, TParent> Do<TParameter>(Action<TParameter> action);/// Version combinée de Do(Action<TRecipient>) et Do(Action<TParameter>).public TransitionBuilder<TState, TEvent , TRecipient, TParent> Do<TParameter>(Action<TRecipient, TParameter> action);/// Configure la politique de façon dont les délégués abonnés au hook d'entrée doit être exécuté./// Si cette méthode n'est pas exécutée, la politique par défaut est TransitionPolicy.ParentFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnEntryPolicy(TransitionPolicy Policy);/// Configure la politique de comment les délégués abonnés au hook de sortie doivent être exécutés./// Si cette méthode n'est pas exécutée, la politique par défaut est TransitionPolicy.ChildFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnExitPolicy(TransitionPolicy Policy);/// Détermine l'état vers lequel passe cette transition./// Ceci est équivalent à : OnEntryPolicy(TransitionPolicy.ChildFirstWithCulling).OnExitPolicy(TransitionPolicy .ParentFirstWithCulling).Goto(state).public TParent Goto(TState state);/// Détermine le transit vers l'état actuel./// Si runParentsActions est vrai : les actions OnExit et OnEntry de l'état actuel (mais pas les états parents dans le cas où l'état actuel est un sous-état) seront exécutées ./// Ceci équivaut à OnEntryPolicy(TransitionPolicy.ChildFirstWithCullingInclusive).OnExitPolicy(TransitionPolicy.ParentFirstWithCullingInclusive).Goto(currentState)./// Si runParentActions est faux : les actions OnExit et OEntry de l'état actuel (et des parents dans le cas où l'état actuel est un sous-état) seront exécutées ./// Ceci équivaut à OnEntryPolicy(TransitionPolicy.ChildFirst).OnExitPolicy(TransitionPolicy.ParentFirst).Goto(currentState).public TParent GotoSelf(bool runParentsActions = false);/// Détermine qu'il n'y aura aucune transition vers un état, donc aucun événement OnEntry ni OnExit ne sera soulevé./// Cela équivaut à OnEntryPolicy(TransitionPolicy.Ignore).OnExitPolicy(TransitionPolicy.Ignore).GotoSelf().public TParent StaySelf();}classe scellée publique GotoBuilder<TState, TEvent, TRecipient, TParent> : IGoto<TState>where TState : notnullwhere TEvent : notnull {/// Configure la politique selon laquelle les délégués abonnés au hook d'entrée doivent être exécuté./// Si cette méthode n'est pas exécutée, la politique par défaut est TransitionPolicy.ParentFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnEntryPolicy(TransitionPolicy Policy);/// Configure la politique de façon dont les délégués abonnés à le hook de sortie doit être exécuté./// Si cette méthode n'est pas exécutée, la politique par défaut est TransitionPolicy.ChildrenFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnExitPolicy(TransitionPolicy Policy);/// Détermine vers quel état cette transition va.public TParent Goto(TState state);/// Détermine le passage à l'état actuel./// Ceci est un raccourci de Goto(currentState).public TParent GotoSelf();}/// Détermine la politique de transition entre deux états./// Ceci configure la façon dont les délégués abonnés sur les états sont exécutés pendant la transition entre les états.public enum TransitionPolicy{/// Détermine que les délégués abonnés ne doivent pas s'exécuter.Ignore = 0,/// Détermine que les délégués abonnés sur les parents sont exécutés en premier.ParentFirst = 1,/// Détermine que les délégués abonnés sur les enfants sont exécutés en premier.ChildFirst = 2,/// Détermine que les délégués abonnés sur les parents sont exécutés en premier à partir (à l'exclusion) du dernier parent commun entre les deux states.ParentFirstWithCulling = 3,/// Détermine que les délégués abonnés sur les enfants sont exécutés en premier jusqu'à atteindre (à l'exclusion) le dernier parent commun entre les deux états.ChildFirstWithCulling = 4,/// Détermine que les délégués abonnés sur les parents sont exécutés en premier à partir de ( incluant) le dernier parent commun entre les deux états.ParentFirstWithCullingInclusive = 5,/// Détermine que les délégués abonnés sur les enfants sont exécutés en premier jusqu'à atteindre (y compris) le dernier parent commun entre les deux états.ChildFirstWithCullingInclusive = 6,}/// Représente une tranche de data.public readonly struct ReadOnlySlice<T> : IReadOnlyList<T>{/// Récupère l'élément spécifié à l'index.public T this[int index] { obtenir; }/// Récupère le nombre de tranches.public int Count { get; }/// Obtenez un <see cref="ReadOnlyMemory{T}"/> de cette tranche.public ReadOnlyMemory<T> Memory { get; }/// Obtenez un <see cref="ReadOnlySpan{T}"/> de cette tranche.public ReadOnlySpan<T> Span { get; }/// Récupère l'énumérateur du slice.public Enumerator GetEnumerator();/// Énumérateur de <see cref="ReadOnlySlice{T}"/>.public struct Enumerator : IEnumerator<T>{/// Récupère l'élément actuel de l'énumérateur.public T Actuel { get; }/// Passe à l'élément suivant de enumeration.public bool MoveNext();/// Réinitialise le enumeration.public void Reset();}}