The condition is

    Hello again, Khabrovsk! In the last article, I talked about teams and how to use them, but today I will develop the topic and tell you how to attach a team to a state machine. The topic on the hub is not new, so I will not go into the explanations of what a finite state machine is and why it is used, but I will focus on implementation. Immediately make a reservation that for understanding it is better to read the previous article, because the teams will be used almost without change as states. Before starting, I want to say thank you to OnionFan for his comment - not all habits are good and his question made typing of state machines more convenient, which I will talk about just by adding the params keyword (I already corrected in the previous article).

    Problem
    In the comments to the last article, the idea was met that the example was not chosen very well and not everyone took it seriously, so now, after a little reflection, I decided to choose an example with a more practical connotation. And so, today's example will be a little higher level and will relate to the gameplay, 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, a game mechanic changes or there are cut scenes) and the state of completion of the game (saving progress and freeing resources). I have often seen situations where this was solved either through the coroutines in the manager who postponed the call of certain methods, or through fine-tuning the order of calling the Awake () method through the editor, or just in each Update () the scene was ready. But, as it would be easy to guess, I will offer you a way much more pleasant and elegant using finite state machines. Already at this stage you can easily notice that each stage can be arranged as a team (in which you can even use subcommands) and move on to the next stage only after the completion of the current one. And we’ll immediately agree that the states will be typed commands, since they will almost always need access to the controller. Let’s write the code already, or somehow there is a lot of water.
    Let's start with a simple but already typed, state machine class
    Code
    publicclassStateMachine<T>
            whereT : MonoBehaviour
        {
            privatereadonly T _stateMachineController;
            private Command _currentState;
            publicStateMachine (T stateMachineController)
            {
                this._stateMachineController = stateMachineController;
            }
            public TCommand ApplyState<TCommand> (paramsobject[] 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 object, remember it as the current one and return it just in case.
    Controller now
    publicclassSceneController : StateMachineHolder
        {
            public StateMachine<SceneController> StateMachine
            {
                get;
                privateset;
            }
            publicSceneController ()
            {
                StateMachine = new StateMachine<SceneController> (this);
            }
            privatevoidStart()
            {
                StateMachine.ApplyState<InitializeState> ();
            }
        }
    


    Everything is simple again: a public get-er for the automaton object and transition to the initialization state in Start (). Thus, the Start () of the 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
    publicclassSceneState: CommandWithType<SceneController>
        {
        }
        classInitializeState : SceneState
        {
            protectedoverridevoidOnStart (object[] args)
            {
                base.OnStart (args);
                //test
                UnityEngine.Debug.Log(string.Format("{0}", "Initialize state"));
                Controller.StateMachine.ApplyState<ReadyState> ();
            }
        }
        classReadyState : SceneState
        {
            protectedoverridevoidOnStart (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 begin to be fulfilled only after the initialization is complete, which is what we wanted.

    Somehow it turned out a little
    Hedgehog it is clear that the game states themselves cannot 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 directly in the game state class, then why am I here?
    Take scoring for example. We will write a separate command for this and will launch it in the game state (until we are familiar with MVC, we will record the score directly to the controller).
    Primitive counting
    publicclassUpdateScoreCommand : SceneState
        {
            protectedoverridevoidOnStart (object[] args)
            {
                base.OnStart (args);
                StartCoroutine (UpdateScore());
            }
            private IEnumerator UpdateScore ()
            {
                while (true)
                {
                    if (!IsRunning)
                        yieldbreak;
                    yield return new WaitForSeconds (1);
                    Controller.Score++;
                }
            }
        }
    


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


    I am already embarrassed by the cumbersome start of the count command compared to the start of the state. Also, the need to constantly keep all the links to all running commands makes me at least depressing and cluttering the state class. Of course, links to some teams will have to be kept, but in the case of scoring, the team should just work until the end of the game state and stop executing at the moment of state transition so as not to add too much. You can easily keep track of such commands by the state machine itself, telling it to simply stop all running commands from the state when it completes. Let us put this responsibility on him:
    StateMachine vol. 2.0
    publicclassStateMachine<T>
            whereT : MonoBehaviour
        {
            privatereadonly T _stateMachineController;
            private Command _currentState;
            private List<CommandWithType<T>> _commands;
            publicStateMachine (T stateMachineController)
            {
                this._stateMachineController = stateMachineController;
                _commands = new List<CommandWithType<T>> ();
            }
            public TCommand ApplyState<TCommand> (paramsobject[] 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> (paramsobject[] args)
                where TCommand : CommandWithType<T>
            {
                TCommand command = Command.ExecuteOn<TCommand> (_stateMachineController.gameObject, _stateMachineController, args);
                _commands.Add (command);
                return command as TCommand;
            }
            privatevoidStopAllCommands()
            {
                for (int i = 0; i < _commands.Count; i++)
                {
                    _commands [i].Terminate ();
                }
            }
        }
    


    Now the ApplyState () method will be used to start states, and the Execute () method to run commands in this state and when the states are completed, we will automatically end all running commands. And it made a lot nicer calling auxiliary commands
    Call Subcommands
    classReadyState : SceneState
        {
            protectedoverridevoidOnStart (object[] args)
            {
                base.OnStart (args);
                //test
                UnityEngine.Debug.Log(string.Format("{0}", "ready state"));
                Controller.StateMachine.Execute<UpdateScoreCommand> ();
            }
        }
    


    Now you can simply run and forget auxiliary commands, the machine will remember them when the time comes.
    Everything turned out simply and beautifully, a minimum of attention should be paid to the management of calls and team stops, and everything is guaranteed to pass at the right time.

    Little joys
    The state machine is completely ready for use, it remains only to talk about one small convenience. With this implementation, transitions between states must be recorded in the states themselves and this is very convenient for branching or decision making systems. But there are situations when the state tree may not be very complicated and, in this case, it is convenient to register the entire chain of states in one place.
    Before adding this feature, let's recall that a state is nothing but a team, and a team in our implementation can have two outcomes: successful and not successful. This is enough to build simple trees of behavior, and even with the possibility of looping (shoot, reload, shoot, and then ask who is there).
    Due to the method of invoking a command, we cannot immediately instantiate all the commands we need and use them when needed. Therefore, we dwell on the fact that we will store the entire chain (or tree) in the form of a list of types of the necessary commands. But first, for such a system, you will have to slightly correct the command class so that it has not only a typed invocation method, but also a method into which you can pass the type of the desired command and the flag of success of the completion of the command.
    I will give only changes in the team
    publicbool FinishResult
        {
            get;
            privateset;
        }
        publicstatic T ExecuteOn<T>(GameObject target, paramsobject[] args)
            where T : Command
        {
            return ExecuteOn (typeof(T), target, args) as T;
        }
        publicstatic Command ExecuteOn(Type type, GameObject target, paramsobject[] args)
        {
            Command command = (Command)target.AddComponent (type);
            command._args = args;
            return command;
        }
        protectedvoidFinishCommand(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, therefore 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 unsuccessful completion of the target:
    Steam container
    publicsealedclassCommandPair
        {
            publicreadonly Type TargetType;
            publicreadonly Type SuccesType;
            publicreadonly Type FaultType;
            publicCommandPair (Type targetType, Type succesType, Type faultType)
            {
                this.TargetType = targetType;
                this.SuccesType = succesType;
                this.FaultType = faultType;
            }
            publicCommandPair (Type targetType, Type succesType)
            {
                this.TargetType = targetType;
                this.SuccesType = succesType;
                this.FaultType = succesType;
            }
    


    Please note that if only one type of the next command is passed to the constructor, there will be no branching and a command of the corresponding type will be called at any outcome of the target command.
    Now the queue goes to the container of our pairs:
    Container container
    publicsealedclassCommandFlow
        {
            private List<CommandPair> _commandFlow;
            publicCommandFlow ()
            {
                this._commandFlow = new List<CommandPair>();
            }
            publicvoidAddCommandPair(CommandPair commandPair)
            {
                _commandFlow.Add (commandPair);
            }
            public Type GetNextCommand(Command currentCommand)
            {
                CommandPair nextPair = _commandFlow.FirstOrDefault (pair => pair.TargetType.Equals (currentCommand.GetType ()));
                if (nextPair == null)
                    returnnull;
                if (currentCommand.FinishResult)
                    return nextPair.SuccesType;
                return nextPair.FaultType;
            }
        }
    


    In addition to storing a pair of commands, this container will also search for the next registered state in the current one. It remains only to bind our execution order to the state machine so that it can itself change states.
    StateMachine vol. 3.0
    publicclassStateMachine<T>
            whereT : MonoBehaviour
        {
            privatereadonly T _stateMachineController;
            privatereadonly CommandFlow _commandFlow;
            private Command _currentState;
            private List<CommandWithType<T>> _commands;
            publicStateMachine (T stateMachineController)
            {
                this._stateMachineController = stateMachineController;
                _commands = new List<CommandWithType<T>> ();
            }
            publicStateMachine (T _stateMachineController, CommandFlow _commandFlow)
            {
                this._stateMachineController = _stateMachineController;
                this._commandFlow = _commandFlow;
                _commands = new List<CommandWithType<T>> ();
            }
            public TCommand ApplyState<TCommand> (paramsobject[] args)
                where TCommand : CommandWithType<T>
            {
                return ApplyState (typeof(TCommand), args) as TCommand;
            }
            public Command ApplyState(Type type, paramsobject[] 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;
            }
            privatevoidOnStateFinished (Command command)
            {
                if (_commandFlow == null)
                    return;
                Type nextCommand = _commandFlow.GetNextCommand (command);
                if (nextCommand != null)
                    ApplyState (nextCommand);
            }
            public TCommand Execute<TCommand> (paramsobject[] args)
                where TCommand : CommandWithType<T>
            {
                TCommand command = Command.ExecuteOn<TCommand> (_stateMachineController.gameObject, _stateMachineController, args);
                _commands.Add (command);
                return command as TCommand;
            }
            privatevoidStopAllCommands()
            {
                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 show it, but we will leave the opportunity to start it without the sequence prepared earlier.
    Now it remains only to learn how to use it all:
    Using
    publicclassSceneController : StateMachineHolder
        {
            publicint Score = 0;
            public StateMachine<SceneController> StateMachine
            {
                get;
                privateset;
            }
            publicSceneController ()
            {
                CommandFlow commandFlow = new CommandFlow ();
                commandFlow.AddCommandPair (new CommandPair(typeof(InitializeState), typeof(ReadyState), typeof(OverState)));
                StateMachine = new StateMachine<SceneController> (this, commandFlow);
            }
            privatevoidStart()
            {
                StateMachine.ApplyState<InitializeState> ();
            }
        }
        classInitializeState : SceneState
        {
            protectedoverridevoidOnStart (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 has been written, we have a solid, flexible and easy-to-manage finite state machine. Thanks for attention.

    Also popular now: