Divide and Conquer - Using FSM in Unity

A competent architecture plays a key role in the development of any software product. Most common performance, extensibility, or understandability issues are rooted in its absence. The lack of a well-defined project structure deprives developers of the ability to think in abstractions, understand the code written by a colleague at a glance, and predict where an error occurs. And in some cases, a person can get confused even in their own code, oversaturated with entities and components. But almost every programmer sooner or later, whether on his own or with the help of a clever book, gets acquainted with solutions that are good regardless of the context. They are so effective and versatile that they find a place in solving many problems, and ... Yes, I know, you can not continue, everyone already understood that I was talking aboutdesign patterns. Some pray for them, others found their bicycles among them. Some claim in the interview that they studied them inside and out and were caught in complete uselessness. But everyone, one way or another, heard about them. Today we will talk about one of the patterns - "State" . More precisely, about finite state machines. Even if you belong to the last of the groups listed above, you have probably come across the following tool:





Minimal main character animator in platformer
Minimal main character animator in platformer

Animators in Unity are built on state machines. Each animation of a group of objects is represented as a state. The conditions and order of transitions between them are determined in the animator, which is a state machine. Also, the topic of using finite state machines to describe the logic of work of objects with complex behavior has been repeatedly raised. AI bots, control of the main character, that's all.





. . , - , . , , , . , . , - :





  • . , , . , Play . - , . , isPaused, , .





  • , . , , , .





  • , , . , , , , , AI.





  • , . Play, WaitMatch "match_ready", , , "room_left" .





  • . , , , . , , "" .





. , . . . . , .





- . , . , .





, . :





FSM





AState





- public FSM(AState initState)





- public void Signal(string name, object data = null)





- private void ChangeState(AState newState)





- void Enter()





- void Exit()





- AState Signal()





, 2 :





. . , , . , . Exit



Enter



. , :





 public class FSM
 {
   private AState currentState;

   public FSM(AState initState) => ChangeState(initState);
   
   private void ChangeState(AState newState)
   {
     if (newState == null) return;
     currentState?.Exit();
     currentState = newState;
     currentState.Enter();
   }

   public void Signal(string name, object arg = null)
   {
     var result = currentState.Signal(name, arg);
     ChangeState(result);
   }
 }
      
      



. , , .





public class AState
{
  public virtual void Enter() => null;
  public virtual void Exit() => null;
  public virtual AState Signal(string name, object arg) => null;
}
      
      







public class SLoad : AState
{
    public override void Enter()
    {
        Game.Data.Set("loader_visible",true);
        var load = SceneManager.LoadSceneAsync("SceneGameplay");
        load.completed+=a=>Game.Fsm.Signal("scene_loaded");
    }

    public override void Exit()
    {
        Game.Data.Set("loader_visible",false);
    }
    
    public override AState Signal(string name, object arg)
    {
        if (name == "scene_loaded")
            return new SLobby();
        return null;
    }
    
}
      
      



, , . , , . 3 , . - , - . ! , , . , , ,





public class SMessage : AState
{
    private string msgText;
    private AState next;
    public SMessage(string messageText, AState nextState)
    {
        msgText = messageText;
        btnText = buttonText;
        next = nextState;
    }
    
    public override void Enter()
    {
        Game.Data.Set("message_text", msgText);
        Game.Data.Set("window_message_visible",true);
    }

    public override void Exit()
    {
        Game.Data.Set("window_message_visible",false);
    }
    
    public override AState Signal(string name, object arg)
    {
        if (name == "message_btn_ok") 
            return next;
        return null;
    }
}
      
      



, c , .





...
case "iap_ok":
	return new SMessage("Item purchased! Going back to store.", new SStore());
...
      
      



Game.Data



, , , , "". , , UI, . , . , .





public class ButtonFSM : MonoBehaviour, IPointerClickHandler
{
    public string key;
    
    public override void OnPointerClick(PointerEventData eventData)
    {
        Game.Fsm.Signal(key);
    }
}
      
      



In other words, when we click on the button (in fact, to any CanvasRenderer) we send the corresponding signal to the machine. When transitioning between states, we can turn on and off different Canvas in any way convenient for us, change the masks used in Physics.Raycast



and even sometimes change Time.timeScale! No matter how awful and uncultured it may seem at first glance, as long as what is done in Enter



is canceled in Exit



, it is guaranteed not to cause any inconvenience, so go ahead! The main thing is not to overdo it.








All Articles