📜 ⬆️ ⬇️

The condition is

Hello again, Habrovchane! In the last article I talked about the teams and how to use them, but today I will develop the topic and tell you how to tie the team to the state machine. The topic on Habré is not new, so I will not delve into explaining what a state machine is and why it is used, but focus on implementation. Immediately, I’ll make a reservation that for understanding it’s better to read the previous article, because the teams will be used as states without any changes. Before starting, I want to say thanks to OnionFan for his comment - not all habits are good and his question allowed us to make typing finite automata more convenient, about which I will tell, simply by adding the params keyword (I corrected in the previous article).

Problem
In the comments to the previous article, there was a thought that the example was not chosen very well and not everyone took it seriously, so now, having a little thought, I decided to choose an example with a more practical tinge. And so, today's example will be a little higher level and will relate to the game process, and more specifically to the states through which most game scenes pass.
Offhand, you can immediately name at least three stages through which each game scene passes without fail: initialization of resources and models, the game state itself (it can be divided into several different states if, for example, there is a change in game mechanics or there is a cut scene) and the state of completion of the game (save progress and release resources). I have often seen situations when it was solved either through the quortenants in the manager, who postponed the call of certain methods, or through fine-tuning the call order of the Awake () method through the editor, or simply in each Update () the scene was checked. But, as it would have been easy to guess, I will offer you a method that is much nicer and more elegant with the use of finite automata. Already at this stage, you can easily notice that each stage can be formed as a team (in which you can even use subcommands) and move on to the next stage only after the full completion of the current one. And we will immediately agree that the states will be typed commands, since they will almost always need access to the controller. Let's write the code, and then somehow a lot of water.
Let's start with a simple, but already typed, state machine class.
Code
public class StateMachine<T> where T : MonoBehaviour { private readonly T _stateMachineController; private Command _currentState; public StateMachine (T stateMachineController) { this._stateMachineController = stateMachineController; } public TCommand ApplyState<TCommand> (params object[] args) where TCommand : CommandWithType<T> { if (_currentState != null) _currentState.Terminate (); _currentState = Command.ExecuteOn<TCommand> (_stateMachineController.gameObject, _stateMachineController, args); return _currentState as TCommand; } } 


Nothing unusual: we stop the previous state if there is one, start a new one on the controller's object, remember it as the current one and return it just in case.
Now the controller
 public class SceneController : StateMachineHolder { public StateMachine<SceneController> StateMachine { get; private set; } public SceneController () { StateMachine = new StateMachine<SceneController> (this); } private void Start() { StateMachine.ApplyState<InitializeState> (); } } 


Again, everything is simple: a public get-er for the object of the machine and the transition to the initialization state in Start (). Thus, the Start () controller became the entry point into the scene, which gives full confidence in the correct sequence of calls of all states.
And immediately a blank for the state of the scene and almost empty classes of the first two states:
You can not even watch
 public class SceneState: CommandWithType<SceneController> { } class InitializeState : SceneState { protected override void OnStart (object[] args) { base.OnStart (args); //test UnityEngine.Debug.Log(string.Format("{0}", "Initialize state")); Controller.StateMachine.ApplyState<ReadyState> (); } } class ReadyState : SceneState { protected override void OnStart (object[] args) { base.OnStart (args); //test UnityEngine.Debug.Log(string.Format("{0}", "ready state")); } } 


It is easy to believe that the game state with this approach will start to be executed only after the initialization is fully completed, which is what we wanted.

Somehow it turned out a little
No brainer that the game states themselves can not be as simple as in the examples above. For example, in the game state, you need to count points, update the state of the UI, create opponents and coins, move the camera and the like. And if we write all this code right in the game state class, then why am I here?
Take for example scoring. For this we will write a separate command and we will launch it in the game state (until we are not familiar with MVC, we will write the account directly to the controller).
Primitive counting
 public class UpdateScoreCommand : SceneState { protected override void OnStart (object[] args) { base.OnStart (args); StartCoroutine (UpdateScore()); } private IEnumerator UpdateScore () { while (true) { if (!IsRunning) yield break; yield return new WaitForSeconds (1); Controller.Score++; } } } 


Game state
  class ReadyState : SceneState { private UpdateScoreCommand _updateScoreCommand; protected override void OnStart (object[] args) { base.OnStart (args); //test UnityEngine.Debug.Log(string.Format("{0}", "ready state")); _updateScoreCommand = Command.ExecuteOn<UpdateScoreCommand> (Controller.gameObject, Controller); } protected override void OnReleaseResources () { base.OnReleaseResources (); _updateScoreCommand.Terminate (); } } 


I am already confused by the cumbersome start of the counting command compared to the launch of the state. Also, the need to constantly keep all references to all the running commands at least depresses me and clutters up the state class. Of course, links to some teams will have to be kept, but in the case of scoring, the team should simply work until the end of the game state and stop executing at the moment of transition of states in order not to overcharge. You can easily force the state machine itself to follow such commands, telling it to simply stop all the commands launched from the state at the end of it. Let's take this responsibility on him:
StateMachine vol. 2.0
 public class StateMachine<T> where T : MonoBehaviour { private readonly T _stateMachineController; private Command _currentState; private List<CommandWithType<T>> _commands; public StateMachine (T stateMachineController) { this._stateMachineController = stateMachineController; _commands = new List<CommandWithType<T>> (); } public TCommand ApplyState<TCommand> (params object[] args) where TCommand : CommandWithType<T> { if (_currentState != null) _currentState.Terminate (true); StopAllCommands (); _currentState = Command.ExecuteOn<TCommand> (_stateMachineController.gameObject, _stateMachineController, args); return _currentState as TCommand; } public TCommand Execute<TCommand> (params object[] args) where TCommand : CommandWithType<T> { TCommand command = Command.ExecuteOn<TCommand> (_stateMachineController.gameObject, _stateMachineController, args); _commands.Add (command); return command as TCommand; } private void StopAllCommands() { for (int i = 0; i < _commands.Count; i++) { _commands [i].Terminate (); } } } 


Now the ApplyState () method will be used to start the states, and the Execute () method to run the commands in this state and at the completion of the states, we will automatically end all running commands. And it made a much nicer call for auxiliary commands.
Call subcommands
  class ReadyState : SceneState { protected override void OnStart (object[] args) { base.OnStart (args); //test UnityEngine.Debug.Log(string.Format("{0}", "ready state")); Controller.StateMachine.Execute<UpdateScoreCommand> (); } } 


Now, the auxiliary commands can be simply launched and forgotten, the machine gun will remember them when the time comes.
Everything turned out simply and beautifully, the minimum attention needs to be paid to the management of calls and command stops and everything is guaranteed to take place at the right moment.

Small pleasures
The state machine is completely ready to use, it remains only to tell about one small convenience. With this implementation, state transitions should be written in the states themselves and this is very convenient for branching or decision making systems. But there are situations where the state tree may not be very complex and, in this case, it is convenient to register the entire chain of states in one place.
Before adding this feature, let's remember that a state is nothing but a command, and a team in our implementation can have two outcomes: successful and not successful execution. This is quite enough to build simple trees of behavior and even with the possibility of looping (shoot, reload, shoot and then ask who it is).
Due to the method of invoking a command, we cannot immediately make instances of all the commands we need and use them when necessary. Therefore, let us dwell on the fact that we will store the entire chain (or tree) in the form of a list of types of necessary commands. But for starters, such a system will have to slightly correct the class of the team so that it has not only a typed method of calling, but also a method in which you can pass the type of the command you need and the flag for the success of the command completion.
I will give only changes in the team
  public bool FinishResult { get; private set; } public static T ExecuteOn<T>(GameObject target, params object[] args) where T : Command { return ExecuteOn (typeof(T), target, args) as T; } public static Command ExecuteOn(Type type, GameObject target, params object[] args) { Command command = (Command)target.AddComponent (type); command._args = args; return command; } protected void FinishCommand(bool result = true) { if (!IsRunning) return; OnReleaseResources (); OnFinishCommand (); FinishResult = result; if (result) CallbackToken.FireSucceed (); else CallbackToken.FireFault (); Destroy (this, 1f); } 


There is nothing to explain, because I will not. Now let's write a container that will contain the type of the target command and the type of the following commands for cases with successful and not successful completion of the target:
Steam container
  public sealed class CommandPair { public readonly Type TargetType; public readonly Type SuccesType; public readonly Type FaultType; public CommandPair (Type targetType, Type succesType, Type faultType) { this.TargetType = targetType; this.SuccesType = succesType; this.FaultType = faultType; } public CommandPair (Type targetType, Type succesType) { this.TargetType = targetType; this.SuccesType = succesType; this.FaultType = succesType; } 


Note that if only one type of the following command is passed to the constructor, there will be no branching and the command of the appropriate type will be called for any outcome of the target command.
Now the queue goes to the container of our pairs:
Container Container
  public sealed class CommandFlow { private List<CommandPair> _commandFlow; public CommandFlow () { this._commandFlow = new List<CommandPair>(); } public void AddCommandPair(CommandPair commandPair) { _commandFlow.Add (commandPair); } public Type GetNextCommand(Command currentCommand) { CommandPair nextPair = _commandFlow.FirstOrDefault (pair => pair.TargetType.Equals (currentCommand.GetType ())); if (nextPair == null) return null; if (currentCommand.FinishResult) return nextPair.SuccesType; return nextPair.FaultType; } } 


In addition to storing pairs of commands in itself, this container will still search for the next registered state according to the current one. It remains only to tie our execution order to the finite state machine so that it itself can change states.
StateMachine vol. 3.0
 public class StateMachine<T> where T : MonoBehaviour { private readonly T _stateMachineController; private readonly CommandFlow _commandFlow; private Command _currentState; private List<CommandWithType<T>> _commands; public StateMachine (T stateMachineController) { this._stateMachineController = stateMachineController; _commands = new List<CommandWithType<T>> (); } public StateMachine (T _stateMachineController, CommandFlow _commandFlow) { this._stateMachineController = _stateMachineController; this._commandFlow = _commandFlow; _commands = new List<CommandWithType<T>> (); } public TCommand ApplyState<TCommand> (params object[] args) where TCommand : CommandWithType<T> { return ApplyState (typeof(TCommand), args) as TCommand; } public Command ApplyState(Type type, params object[] args) { if (_currentState != null) _currentState.Terminate (true); StopAllCommands (); _currentState = Command.ExecuteOn (type ,_stateMachineController.gameObject, _stateMachineController, args); _currentState.CallbackToken.AddCallback (new Callback<Command>(OnStateFinished, OnStateFinished)); return _currentState; } private void OnStateFinished (Command command) { if (_commandFlow == null) return; Type nextCommand = _commandFlow.GetNextCommand (command); if (nextCommand != null) ApplyState (nextCommand); } public TCommand Execute<TCommand> (params object[] args) where TCommand : CommandWithType<T> { TCommand command = Command.ExecuteOn<TCommand> (_stateMachineController.gameObject, _stateMachineController, args); _commands.Add (command); return command as TCommand; } private void StopAllCommands() { for (int i = 0; i < _commands.Count; i++) { _commands [i].Terminate (); } } } 


Let the machine itself keep the sequence in itself and change the state itself as we indicate to it, but leave the opportunity to start it without the sequence prepared earlier.
Now it remains to learn how to use it all:
Using
  public class SceneController : StateMachineHolder { public int Score = 0; public StateMachine<SceneController> StateMachine { get; private set; } public SceneController () { CommandFlow commandFlow = new CommandFlow (); commandFlow.AddCommandPair (new CommandPair(typeof(InitializeState), typeof(ReadyState), typeof(OverState))); StateMachine = new StateMachine<SceneController> (this, commandFlow); } private void Start() { StateMachine.ApplyState<InitializeState> (); } } class InitializeState : SceneState { protected override void OnStart (object[] args) { base.OnStart (args); //test UnityEngine.Debug.Log(string.Format("{0}", "Initialize state")); FinishCommand (Random.Range (0, 100) < 50); } } 


Voila! Now, for convenient use of state branching, we only need to register a sequence of commands, transfer it to the state machine and run the first state, then everything will happen without our participation. Now the topic is fully disclosed. After all that was written, we got a good, flexible and easy-to-manage state machine. Thanks for attention.

')

Source: https://habr.com/ru/post/281545/


All Articles