Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

Discussion Hierarchical State Machine structure

Discussion in 'Scripting' started by GuirieSanchez, Jul 24, 2022.

  1. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Hi guys, I hope you all are doing great. I'm bringing this subject to get some feedback regarding the organization of a hierarchical state machine.

    I've been looking at some references and I found this very interesting "nested (or hierarchical) state machine" diagram

    Bardent Nested FSM diagram.png
    Diagram by youtuber "Bardent"

    So, as you can see, the yellow boxes are the superstates, while the blue and green boxes are the "regular" or sub-states. The arrows represent the transitions and their white boxes represent the conditions for such transitions.

    I know it can be a little chaotic at first glance, but after reviewing it carefully, I think it's very well organized (Let me know if you think otherwise, or if you think of any improvements for it).

    Now the issue I want to discuss here is the following: All the movement is being read and applied in the "Grounded Superstate", specifically in the "move sub-state". That means that, outside this superstate, there's no going to be movement: therefore, it implies that the jump might be keeping the momentum but you won't be able to control the character as to change directions; same with the attack: you won't be able to move while attacking or to change directions in mid-combo. Of course, you can implement some movement in these particular sub-states if you wanted to, but I'm thinking in terms of reusability.

    So, a question I want to throw out:
    - If I wanted to have movement control while attacking and jumping, would it be possible (I guess so) or recommended to upgrade the attack and jump sub-states to a superstate? So that, for example, when I transition from the Grounded Superstate to the (new) Jump Superstate I would have a "move sub-state" inside that allows my input to be read, and the player will be able to move in mid-air.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    37,245
    This paragraph is the poster child for why I maintain my position on generic FSMs:

    1. they don't actually solve any problem I have, and
    2. they greatly obfuscate and complicate the problem

    I posted this to you a few days ago, but in case you forgot, here is my position again:

    FSM finite state machines:

    This is my position on finite state machines (FSMs):

    https://forum.unity.com/threads/state-machine-help.1080983/#post-6970016

    I'm kind of more of a "get it working first" guy.

    Your mileage may vary.
     
    GuirieSanchez and Kreshi like this.
  3. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    You're indeed right, they may not solve any problem for you, but to me, they got plenty of advantages (code organization, clean separation between different actions, less prone to bugs, easier to debug, etc.)
     
    Kurt-Dekker likes this.
  4. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Easier to maintain, easier to expand, modularity (and I could keep going). The alternative if we're not using something like this, or a behavior tree (or a system that allows you to organize), I would guess it'd be to update each function with a growing bunch of if statements and hope for the best.
     
  5. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    And again, I understand this is not for everybody. At the end of the day, it is as you @Kurt-Dekker once mentioned, a code or game that works and sells = good code, and I completely agree :)
    As long as you could keep organized your code and work with it, anything would be possible.
     
  6. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    It could work; I can't guarantee that it would be easier to maintain than just implementing movement in those substates. These kind of scenarios can be very complex; what's best depends on a lot of details. What I can do is suggest an alternative that may also work; then you can decide for yourself. Have you considered what some call Layered State Machine? In its simplest form (and my favorite form), it just means multiple state machines that communicate between themselves through data or events.

    Cases like this may be understood as a superposition of states. For example, a basic character can be Moving and NotMoving; and at the same time it can be Jumping, Attacking or Crouching. It can be done by having one State Machine for Movement, and one for Actions. That could be simpler than having an HFSM with Jumping.Moving, Jumping.NotMoving, Attacking.Moving, Attacking.NotMoving, Crouching.Moving and Crouching.NotMoving states.

    The more layers of states, the more it could be beneficial use multiple state machines, as there are more possible combinations between states. Another advantage of using layers is the possibility of reusing and remixing FSMs. Some character could have a different Actions FSM for a different set of actions, but use the same Movement FSM.

    The main disadvantage of using a layered approach is that the communication between State Machines can get very complex for cases that need very tight integration. In our example, if the character moves faster when Jumping, the jumping state could set a
    movementSpeed
    variable that the Movement FSM uses to move; and if a special Attack prevented the character from moving, it could set that variable to 0. This example is kind of simple, but the more one layer depends on stuff from another layer, the more complex things can be. There are always ways of communication between FSMs that are cleaner than others, but it depends on each particular case.

    The best part is that Hierarchical and Layered approaches are not mutually exclusive; you can combine both. The more experience you get with these tools, the easier it is to decide when to use which one. I'd say it's worth it to spend some time making these choices for projects of certain scale; YMMV.

    Finally, I want to say that these kinds of problems exist whether you use a generic state machine, or a bespoke state machine designed for your character. They even exist if you don't use a state machine at all. There's always some challenge in handling a character that can do multiple things at the same time.
     
  7. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Thank you so much for the contribution, I highly appreciate it!

    I think I've heard of it before, but not as layered. It'd be equivalent to the term "Concurrent FSM" if I'm not wrong. In fact, my original FSM in Visual Scripting is a Layered State Machine in which each active state machine is either hierarchical or regular/vanilla. But there's a big BUT: I don't know how to write multiple concurrent states in c# yet, but I'll try to learn about it. Also, your division into Movement and Actions is really really interesting, you opened a nice door here for me. And as you mentioned, your division could be very beneficial for expansion.

    Regarding the disadvantages you said: Could it be a possible solution to this to tie together very strongly both Movement and Actions and leave other state machines aside? Because there's only one movement state and one action state active at any given time, their connection should be pretty straightforward. By this, I mean preventing other active StateMachines from accessing Movement and Actions State Machines' parameters (so that you won't create a mess).
     
  8. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387

    @oscarAbraham if I wanted to implement your suggestion for the case above, the first thing that comes to my mind would be to leave the above hierarchical machine as is, and then add your concurrent layer machine with "MovingState" and "NotMovingState" for instance.

    Then, to give some examples of how this would work (so you can confirm whether I'm understanding this in the wrong way or not):

    GROUNDED SUPERSTATE -> Move sub-state -------(communicates with)-------- MOVE SUPERSTATE -> Moving state
    GROUNDED SUPERSTATE -> Idle sub-state -------(communicates with)-------- MOVE SUPERSTATE -> NotMoving state

    then for instance, if I want to be able to change directions while attacking:
    ABILITY SUPERSTATE -> Attack1 sub-state -------(communicates with)-------- MOVE SUPERSTATE -> Moving state (and set its movementSpeed to 0) (or if I wanted to be able to move but slower then change it to a lower amount)

    And so on...

    Edit: Now that I think about it, there's no necessity for a Move superstate: it could be just 2 independent states (MoveState) and (NotMoveState) in which just 1 of them is active at any given time, and they will be independent but at the same time subordinates of the Action StateMachine, as the movement parameters of the MovementStateMachine will be driven by each of the ActionStateMachine's states.

    Also, if we keep expanding the actions (for example we add "stunned State", or "death State") there's always room for the MovementState (for these cases will be NotMoveState, of course).
     
    Last edited: Jul 24, 2022
  9. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    No problem :). I think "Concurrent FSM" is an even better name for what I was talking about.

    I'd say you don't really need to do a lot to code this; you can just make multiple FSMs and add a way for them to share data and messages. That way could be a simple object that I'll call "Character" for now. This Character object would allow multiple FSMs and scripts to communicate between themselves without needing to know about each other.

    So each state in each FSM that belongs to a Character would get a reference to it. The
    movementSpeed
    variable would belong to the Character, not to the Moving state. So there's no need for any state in the Actions FSM to know about the Movement FSM and its states.

    Other scripts could also use a reference to the Character. For example, we could have an InputHandler that sets variables in the character according to Inputs. It could set bools to know if a button is pressed, and Vector2s for analog sticks. If you want more encapsulation, you could put those variables in the InputHandler and make some properties to read them in the Character; so only the InputHandler can write their values, while FSMs still don't need to know that the InputHandler even exists.

    This is the gist of what I'd use for "Concurrent FSMs". If you want to power up this concept, you could implement a messaging system in the Character. A very basic approach would have a method to send a message like this
    character.SendMessage("JumpButtonPressed")
    . FSMs and Scripts could receive those messages by implementing some interface. It could look something like this:

    Code (CSharp):
    1. public interface IMessageReceiver
    2. {
    3.     void OnReceiveMessage(string message);
    4.     // Or you could also send/receive data with the message with something like this:
    5.     void OnReceiveMessage(string message, object data);
    6. }
    This way, our InputHandler could send a message to the character when a button is pressed, and our FSMs could react to those messages without having to check every frame whether that input was pressed. A message system could also allow characters to send messages between themselves. A character could send an "Attack" message to another character without knowing anything about it. Different characters could react to that "Attack" message in different ways; a zombie dies, a tree gets chopped, a rock does nothing.

    Mind you, this message system is very basic and has a lot of room for improvement. For example, I dislike using strings, so I'd normally use ScriptableObjects to identify messages. But that's all there is to it: data and messages shared through an intermediary. The more data and more messages used, the more complex things can get; so it can be worth it to take time thinking the best way to organize them, and to consider when something may be better as nested FSM or even have a tighter coupling between modules in some special cases.
     
    GuirieSanchez likes this.
  10. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    @oscarAbraham Thank you for the insight!
    Actually, this pointed me in the right direction bc I was a bit lost. I just made a second FSM belonging to the Character, and it gets all the data from it. Now I can manage the Character movement from this second FSM and both Movement and Action FSMs are running simultaneously (while not knowing about each other), so it's working great!

    Apart from the
    movementSpeed
    variable that I want to implement, would you recommend making a bool parameter in the Character's Data regarding the player's movement condition (for example
    isMoving
    ) that the Action FSM can control? And likewise, a
    canMove
    bool controlled by the Action FMS that drives the transition between the Movement FSM's states.(The alternative, which is the one I have right now, is to read directly from the InputHandler).

    And yes, you pointed out something that I'm seeking to do: encapsulation. I'm still learning how to implement those, but eventually, I want to have every bit of logic separated. So far, I have an InputHandler that reads and sets the values, with a getter and private setter so that the Character script can only read the values. I also have the player Data in a scriptable object, which I think it's a good practice in general.

    But I still need to improve them if I want them to work as you described. There are some features that you mentioned that I want to implement, especially the messaging system with the interface looks awesome. Would delegates work for a messaging system like the one you're describing? So far I've only worked with
    public static event Action MyEvent;
    and I'm not sure if they fit okay (I believe they don't, so there's work for me here, got to study those).

    Anyhow, as you said, I'll spend the whole day tomorrow organizing and also studying this messaging system (because I have 0 idea about using SObjs to identify messages, to say one thing :D). Nonetheless, having that interface you showed sending messages or events that states can listen to seems a better alternative to make booleans or parameters that states need to be checking in an Update. I'll be researching it and will let you know in case I get hard stuck or something.

    And thanks again!
     
    Last edited: Jul 25, 2022
  11. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    :) I'm happy to help.

    I wouldn't dare to recommend what kind of variables you should need without knowing a lot more about your project; it really depends on the details. That said, I'd probably shouldn't have called it
    movementSpeed
    ; it works more like a
    maxMovementSpeed
    in our example. In some cases, you may want to replace it with a
    canMove
    variable. If you need to know whether a character is moving outside a Movement FSM, yeah, a
    isMoving
    could work; or you could have a
    currentMovementSpeed
    variable that is 0 when a character isn't moving.

    This is the kind of thing where you may know better than anyone else what fits you better. The only thing I'd say is that reading directly from the InputHandler may create an extra coupling that can make things harder to maintain or modify in the future; but if you think it's not too bad for your case, it may be okay.

    Encapsulation with this kind of thing has its limits. Sometimes it's clear that a value should only be written as part of some very specific events, but other times that's not practical or realistic.
    Health
    , for example, could be written by taking damage, by drinking a potion, by changing a Vitality stat, etc. The most you can do in those cases, is to define getters and setters for the data; so if you need to send an event when
    Health
    changes or clamp its value, you can add that code without having to change code anywhere else.

    I think having Data in a ScriptableObject is nice for defining initial values, but if those values change, you might want to copy them to another place before using them. Modifying ScriptableObjects assets from game logic can actually modify the assets in the Editor, plus it makes it harder to reuse those assets. But I do think it can be good; sometimes I create multiple ScriptableObjects just for the data of a single character, as it facilitates modularity and it's friendlier to Version Control.

    Yes, you could totally use delegates or C# events for messages. I don't know why I didn't suggest that instead. I was thinking of the way I've been implementing these things for the last couple of years. I've been designing my systems in a way that our Character class doesn't know the data and the messages it contains; it's nice for scale and team work, specially if you want to be able to define data and messages from the Editor, without code, but it isn't the simplest thing to code and you probably can live without it.

    The Action example you gave seems good enough. If you want to make your code more resilient to changes, and easier to refactor, you could encapsulate your delegates in a couple of methods. Something like
    Charater.RegisterOnJumpListener(Action listener)
    and
    Charater.UnregisterOnJumpListener(Action listener)
    .

    You might want to have FSMs listen to events instead of states, and then propagate the received event to the active state. Adding and removing listeners can be performance heavy, specially for C# events and delegates, as they generate garbage every time a listener is added or removed. If you add and remove listeners every time an FSM changes state, it could have a noticeable performance impact. An alternative is to design your own listener system without C# events, but then you'd have to handle some complex stuff yourself, like what happens when a listener is removed in the middle of sending a message.

    I'd say, don't worry too much about the ScriptableObjects for messages thing. Like I said, lately my systems need this kind of stuff to decouple things from the Charater and to be more editor-friendly, but to program that from scratch is not worthwhile in every case.

    If you are curious, you can decouple data and messages from Character by using a couple of Dictionaries. The data Dictionary is of the pseudo type
    Dictionary<id, valueContainer>
    where
    valueContainer
    is an object that contains a value and
    id
    is some key used to access different valueContainers. The
    id
    can be a
    string
    , an
    int
    , or an
    Object
    (a
    ScriptableObject
    in this case). Like I said, I don't like strings; they are prone to errors and bad for performance. I use both ints and Objects. The Dictionary for messages would be like the data Dictionary, but it'd use delegates instead of valueContainers. All this is really a variation of what some call blackboards.

    You are probably fine without programming all that, though. It's the kind of feature that is friendly when being used, but requires a considerable amount of time and experience to be programmed. I wouldn't worry too much about it.
     
    Last edited: Jul 25, 2022
    GuirieSanchez likes this.
  12. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Basically, I've been reading everything and I was like: This is exactly what I want to be able to do for my project! (by this I mean decouple the code as much as you described).

    Sadly enough, the whole messaging system is really professional, and because of it, it is much harder to achieve (especially for me that I just started learning recently, and also it's harder to look for info or examples of blackboards out there). But of course, now that I know about its existence, I reeeeallly want to get to learn about it. Thanks for opening the gates of knowledge to me! :D

    I'm taking it easy, though slowly but surely, I'm sure I'll end up implementing a cool messaging system like the one you mentioned (not out of neccessity ofc, but because I like it and I think it could be beneficial to know about it for me in the future).

    Right now, I'm focusing on a couple of things you mentioned:
    First of all, I've never seen this expression
    The way I was doing this was to invoke a delegate by using
    MyEvent?.Invoke(myParameters);

    and then subscribing to it by using
    Character.MyEvent += CallMyFunction;

    Should I assume that I should be subscribing to it by using
    Charater.RegisterCallMyFunction(MyEvent listener)
    instead? (I might just made a mess out of your syntax, I have the feeling that I got it totally wrong :D).
    Just out of curiosity: what would be the benefit of subscribing to the events this way over the other? This is totally a personal thing, but I get really excited whenever I find new ways of implementing things that are more performant than others.

    And speaking of which, I didn't know that subscribing and unsubscribing to events was a performant heavy task, thanks for the info. The game architecture that I'm trying to implement is event-based, so this is good to know. It makes me think that it should be better to subscribe to the events when loading a scene and unsubscribe when changing scenes as much as possible. Although I wonder what would happen when spawning a lot of enemies whose systems are also "event-based" (in which each enemy has to subscribe to a lot of events in mid-game).
    Another question is, what happens to the garbage generated from subscriptions and un-subscriptions? would they get recycled automatically or should one implement a custom system to get rid of it?

    And the last thing I want to ask (and I won't bother you with further questions, promised xD) is about having the FSM subscribe to events, how would this be possible? In the case of a regular state, I could simply subscribe on its EnterState and unsubscribe on its ExitState, but the FSM is always active and doesn't have Awake or OnEnterState functions since it doesn't inherit from monobehaviour (Maybe it's a silly question with a very straightforward answer, but I didn't come up with anything yet). For example, I have my Character class that inherits from monobehaviour, a CharacterActionStateMachine that manages the
    Initialze
    and the
    Change
    of states, and the ActionsFSM with a constructor that gets references for the Character class, the CharacterActionStateMachine and the PlayerData(SObj). I believe I should be subscribing to the pertinent events for the action FSM in this script (ActionsFSM), but I'm not sure exactly where.

    And finally, regarding the messaging system, I will spend the next weeks learning about it and trying to improve it. I'll come back around to drop a few questions every now and then (in case I get hard-stack), and I'll appreciate it if you could give me a couple of tips if you happen to drop by.
     
  13. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Happy to help :D. Hmm, I've been slowly putting all these kinds of ideas in a plugin that I plan to put in the Asset Store. It's still a couple of months away from being ready. I you want, I think I could give you a license key when it's released in exchange for some feedback; I could really use it. At the very least, it could help your learning journey, as it'll include the full source code.

    Okay, about your first question about the
    RegisterOnJump
    method idea. It's not very common, but there are a good amount of examples for this pattern in Unity. For example, the API for drop handlers.

    The way you've been using events is totally fine. The idea of putting that exact code inside methods is to make some changes to your code a bit easier. For example, there was a time where a project needed to be able to ensure that some listeners always received a message before some other listeners; encapsulating listener registration inside a method made the change smoother because I could add an extra parameter to set the order of the message. I had something like
    Charater.RegisterOnJump(Action listener)
    , and I added an overload that looked like this:
    Charater.RegisterOnJump(Action listener, int priority)
    .

    Oh, it's not that heavy. I think you'll be fine doing it from time to time, specially if you have the incremental GC enabled. The gist of allocation in events (and multicast delegates) is that they are backed by some special lists of delegates. They are designed so that, once you invoke them, they will execute all the calls registered in that event at the time of invocation, and only those calls. The thing is, what happens if you add or remove a call while invoking an event? Addition or removal of a call shouldn't modify the execution of a previous invocation, so to cover that case, every time a delegate is added or removed, a new list of delegates is generated for use in future invocations.

    So, yes, every
    +=
    or
    -=
    allocates memory, and the more delegates in a single event, the bigger the allocation can be. But doing that from time to time is probably fine. It could be another story if you have hundreds of characters adding and removing delegates every second because their FSMs changed state.

    About what happens to the garbage generated... Well, the same thing that happens to all garbage in Unity. There are some pages about this topic in the manual.

    You could do it in the ActionFSM's constructor or the CharacterActionStateMachine's Start if it already knows which messages it needs. Or you could do it after adding all the states to the FSM. Or, if knowing which messages should be listened to is too complicated, you could register all FSMs to a single event that sends some message info as a parameter. Every state would receive all messages, but it would only react to the messages it cares about; that can be heavier for performance, but it should be okay if there aren't lots of FSMs responding to the same invocation.

    :) That's fine. I'll try to help if I can. You can also drop me a DM if you need to. Good luck.
     
    Last edited: Jul 26, 2022
    GuirieSanchez likes this.
  14. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Omg!! this is exactly what I'm lacking right now: real examples and real implementation. I'll be the number one guy waiting for it. Whenever I get unity packages I always download the samples and dive into the source code to see the inner works and learn from it. Don't forget to hit me up when you release it! and ofc I appreciate it if you could hand me a key, although I would like to purchase it as a way to thank you for the feedback and guidance in the construction of the FSM.

    Btw I've been continuing working on the FSM and Jesus... this is harder than I imagined. While I know the "theory" of how things should be done, I don't know how to put it into practice. For instance, I know that states shouldn't know anything about the others and they should limit themselves to receiving data, running the code, and passing data back. It'd be like this quote by Lisranda:
    On the other hand, the logic of transitioning and doing actions should be done by the Character class.

    When I tried to implement this myself I ended up having something similar to a one-script Character controller, which is the very first reason why I moved into working out an FSM in the first place. So for now, unless I could see a real example (even if it's a small sample of a real interaction between the logic/Character class and one state (so that I can extrapolate that method and expand it on my own)), I have to keep doing it in the "ugly" way (in which state transitions are done by the states).
     
  15. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    I have to admit .. I did not read all of the posts carefully.. However, I think I might be able to add a little value anyways.

    Based on my skimming, it sounds like you are suffering a bit from
    1. trying to rigidly follow theory without fully understanding it
    2. trying to fit everything into one box, or pattern

    Regarding 1., I'm not trying to be harsh. It's very difficult to really understand theory without experience. Pattern recognition always follows the pattern.
    I'll reiterate @Kurt-Dekker 's sentiment: focus on creating something that works first, and extract from that how you might improve things.

    Regarding 2: for example, here's a snippit from my copy of the GOF's Design Patterns chapter on the State pattern:
    Also, I saw plenty of discussion about events, notifications, etc... This is essentially the Observer pattern, or PubSub, as it's sometimes called. Trying to understand how events from a purely FSM perspective probably isn't productive. The patterns exist independently, but their power is in composing them together.

    ---------------------------

    Personally, I don't think an FSM is the right tool for controlling a character. FSMs create very very strict behaviour. This can be useful when you're creating something you need to have perfect control over at all times. The quintessential example is a network connection class. You absolutely can't have this object receiving bytes before it is in the Open state. Likewise, you can't accept a request to close a connection if one hasn't been established in the first place.

    However, a character, particularly one controlled by a player, should be more flexible. It is the combination of actions a character can take, and importantly, the simultaneous mixing of those actions that make a character feel interesting, alive and responsive. IMO, this is a better use case for a composition of objects that accept input and interact on the character in different ways. There is no FSM here.
     
    Last edited: Jul 27, 2022
    GuirieSanchez likes this.
  16. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Thank you for the feedback! :)

    I have to say that my struggle is entirely due to my lack of knowledge and experience in this area, and I don't think my struggle is due to the fact that FSMs might not be good enough or suitable for a character controller; in any case, they are more difficult to learn than learning single scripts with a bunch of if statements.

    I explained to Kurt in another post that I have a working CharacterController that I made in VisualScripting that makes use of a concurrent-hierarchical state machine and it works as intended and pretty nicely. I just wanted to learn c# in order to make a translation of my Character Controller from Visual-Scripting to c# (because, just for the record, if it works in VS it absolutely means that it also works in c# (at least for my purposes)).
    So, to sum up, my intention is more of "I want to learn how this works in c# and get experience" rather than "make the best Character-Controller in theory".


    Also, I'm open to learning new stuff along the way. I'm already using the Observer pattern in my game architecture (for instance, sending events to the AudioManager to play audio clips, or sending events to the AchivementManager when something happens, and stuff of the like), and we were discussing the messaging system because I think it is a great tool for communication even if applied to an FSM. It's been done before and has proven to be fine, so again, I want to reiterate that my struggle with the Observer pattern when applied to an FSM is purely out of my lack of exp in this area. And I don't see anything wrong with it, it's part of the learning process, especially when learning abstract or more complicated patterns.


    I don't want to come off as an FSM defensor here, I just think FSM, as well as behavior trees, push automata and other patterns are just tools that we can make use of if they happen to be suitable for whatever purposes we may have. I do think, however, that a Layered Hierarchical State Machine is perfectly suitable for a character controller. It is difficult to plan and organize at the beginning, but once you got the setup, it should be very straightforward to implement. Also, it also gives you a lot of advantages, as I said before:
    "code organization, clean separation between different actions, less prone to bugs, easier to debug, easier to maintain, easier to expand, it gives you modularity..."​

    You said FSMs are too rigid or less flexible than (other methods I guess, so let's say regular Character Controller scripts). But we should remember that, if well implemented, an FSM should work exactly the same as a very flexible regular Character Controller script (and if you don't get the behavior you're expecting it would very likely be because you haven't implemented it correctly for your purposes).

    But again, I'm open-minded to learning and implementing new methods or patterns if they happen to be good (and I dismissed traditional methods for the issues already mentioned: mainly they are hard to maintain and expand). I just want to say that denying a method without providing or suggesting an alternative is not totally helpful (not saying this because of you @eisenpony, of course). Your alternative of using a Character Controller as a composition of objects is perfectly valid. I think that, even if the character uses an FSM, it could very much have the same features, I don't see how they wouldn't be compatible.

    PS: If anyone knows of a better alternative to this that I might not be aware of, please let me know. Same for improvements on the FSM to what we have already discussed.
     
    Last edited: Jul 27, 2022
  17. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    You can definitely write a character controller with an FSM, I don't mean to "deny" this technique. My point was that FSM is good at specifying precise behaviour, but less good at creating complex, mixed behaviours. Like with all patterns, there is a tradeoff. If you want fine control, then FSM makes sense. If you want emerging combinations of behaviours, FSM is not a great choice.

    One of the reasons is because an FSM requires you to code each state and transition explicitly. There is no natural combining of behaviours.

    You mentioned a few times you are looking for code that is easier to maintain, easier to expand, and provides modularity. Because you need to code out explicitly each state and transition, extension and modularity are areas where FSMs are slightly weaker than other alternatives.
     
    GuirieSanchez likes this.
  18. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    After reading my post twice I realized I came across as a bit harsh. Sorry about that.

    I think you're most likely totally right. I want to point out that what I said above it's just my opinion (that it is, to be honest, less valid than yours or than almost everybody else on this platform that has more experience than me).

    One of the reasons why I'm sticking with FSMs (apart from what has been said multiple times :D) is because I don't know of other methods.

    If you know of alternatives that let one combine multiple behaviors and at the same time they let you have an encapsulated and decoupled code, definitely let me know! I may not end up implementing that alternative now (it might not be a hundred percent suitable or perhaps it might be even more complicated to learn) but it'll be on my bucket list for future things to experiment with.
     
  19. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    So, I'm going to get a bit abstract here. We programmers tend to get a bit dogmatic when talking about ways and patterns. There are good reasons for this; when teams work on software that should be maintainable on the long run, it's very useful to be lead by strong principles that help everything fit together, now and in the future. I think it's still important to keep the reasons behind those principles in mind, so we know how they fit our task at hand, or whether they fit it at all.

    In the end, the right way of doing things is the way that accomplishes your goals while doing the less work. Time spend analyzing theories is work too. That's why a big part of trying to be efficient sometimes has to do with making code that's easy to refactor; it's easier to not worry about about what's the right way when you know it won't be too hard to change things if something goes wrong. Also, everyone finds some things could have been better in the end, that's why there are postmortems, but just because a way is not the best way, it doesn't mean it's the wrong way; if it works, it works.

    Code architecture is such deep topic, that maybe the best way of learning it is by doing. I think the most important thing I've learnt through the years about architecture is to take everything with a grain of salt. That's why I try to give my advice on these sort of questions as "things you can do", instead of "things you should do". People don't program in an ideal world with standard conditions; the right way depends on things like knowledge, deadlines, team composition, budget, or type and size of the project.

    -----

    Okay, now, I don't know the context of the Lisranda quote. Saying that states are like cartridges, seems to leave room for having code inside the states. I do think there's a benefit on having the logic for transitioning between states outside the states; I find it more solid to trigger transitions with conditions and events instead of doing it from inside the states. But it's probably fine to do it from inside states if things aren't too complex; plus, this is the kind of feature that's easy to add by refactoring.

    About having logic inside a CharacterController: It doesn't sound to me, from the quote, that they are talking about a character controller. It sounds like they are talking about a controller for the state machine; a controller that decides which state should be active. Again, I don't know the context, but I'd usually put the responsibility of the controller into the state machine (it seems like unnecessary separation). I do think it's good to remove that responsibility from the states. This is tangential to our concept of "Concurrent State Machines", though. You can still have multiple state machines designed that way, and have them share data/messages.

    There's a common pattern for character movement where there's a class called "CharacterController" that handles the basic capabilities of a character, specially those related to physics, usually only those related to physics. Unity has one of those in the engine. The idea is that you still have other components that tell the character controller what to do, where to move, etc. Some of those other components can very well be FSMs if the complexity deserves it. It's a bit like the engine of a car; it can move the car, but you still need stuff around it to tell it where to go. Sometimes you also have other scripts related to moving the character that don't do much in there own; we talked about an InputHandler, there are also things like a NavMesh agent, or an Animator. It's okay for one module to affect another, that's kind of the point of code architecture.

    -----
    About the messages that were posted after the post I quoted. I'll be honest, the vast majority of the time, I wouldn't do all the movement logic of a character with state machines. Each layer of logic may make it a bit easier to add new ideas, but it can also make thinking about the whole code a bit harder. For example, I'd probably just go with a script for a basic platformer. For a very complex character that can move in all sorts of ways, like in Breath of the Wild, I'd probably would use things like Behavior Trees and State Machines, but it's very possible that I'd have some core scripts used by those logic structures for basic control.

    That said, I do think FSMs can be one useful tool when creating complex behavior, specially when combined with other FSMs and Behavior Trees. I find they make it easier to reason about what's happening some times. But you don't have to use them, and you don't have to do everything with them.
     
    Last edited: Jul 27, 2022
    GuirieSanchez likes this.
  20. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I though of some other things:

    I think the GOF are not right about this. Defining transitions in the code of a State Subclass does allow more control of when they happen; but it makes it much harder to reuse that subclass, and it creates a rigid relation between states that complicates modifying the structure of a State Machine. I'll use Unity's Animator as an example; I don't love its implementation, but it's probably something most people in this forum know. If each state in the Animator had to know in code of the existence of all the states it can transition to, it wouldn't be usable at all.

    I don't think it's more productive to introduce a completely new concept while trying to explain another one... It's true that the observer pattern describes some of what we were talking about, though, at least in the abstract.

    My recommendation to the OP is to be careful when applying the usual example implementations of the Observer Pattern to a system of Concurrent State Machines; in those implementations they use Lists of Observers to implement Observables so, if one Observer causes another Observer to unsubscribe while their Observable is propagating a notification, things will break awfully. This is something that is not too common, but also not that rare when State Machines are used this way.

    I didn't know that you were using VisualScripting before. I haven't used it a lot and in a long while, but I understand that it's possible to add custom nodes and states defined in code. Maybe a possible learning strategy that you could use is to replace portions of your visual scripts, little by little, with your own custom nodes and states?
     
    Last edited: Jul 27, 2022
    GuirieSanchez likes this.
  21. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    Their point was just that the State pattern can be implemented either way. Both have advantages and disadvantages. Your point about coupling these types is germane, but the alternative is to couple them to some controller. Your state transition map must ultimately be coded somewhere.

    My concern was just that the we may be trying to understand the messaging technique through the lens of an FSM. A controller built as a composition of "simple controllers" could also make excellent use of the messaging technique.
     
  22. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I agree that both have pros and cons. Still, the coupling is a lot harder when some states are referenced in code by some state subclasses, isn't it? There's a lot more room for abstraction when transitions are handled outside state subclasses. Also, not only it removes the dependency of state subclasses on other subclasses, it also removes the dependency of those subclasses on the controller; while the controller already needs a dependency on states either way.

    It surprised me a lot that they said coding transitions inside subclasses is more flexible; like, one can't even do good editor tooling with that strategy, and a big part of that is because the coupling is a lot more rigid. So much of the GOF's book is about patterns that are easy to reuse; it feels weird that they went in the opposite direction when considering flexibility here. Then again, it's been a while since that came out; things have changed.
     
    GuirieSanchez likes this.
  23. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    Perhaps it is just a difference in opinion on what they are describing.

    Personally, I think this approach does make the FSM "flexible". The context to make the changes are likely available to the code in the State objects. Transitioning from there means you can make pretty sophisticated decisions and easily move from any state to any other state.

    So in other words, the FSM is flexible: can easily (without complexity) change (move from any state to any other).

    On the other hand, the code is probably not flexible: cannot easily be extended to adapt to changing circumstances or requirements.
     
    GuirieSanchez likes this.
  24. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Yes, I agree with this. For now, I don't know how to apply the logic out of the states, but once I manage to do it a single time, I'll be able to apply it to all states. I'm still doing my research about nice methods to get the logic out of the states. I'll keep you updated once I make any advancements and also I'll share the methods found for future wanderers that could use a hand in this post.

    Yep. The problem is that the logic of the state transitions in my VS FSM is applied in the states :D, so this is definitely something I want to improve from my previous implementation.

    Just out of curiosity,
    What can happen in this scenario? I'm a little concerned now since I'm using the OP already, I wouldn't want to end up doing exactly this by accident.

    That makes sense, I was misunderstanding you. Until I read this, I was like: how could you even think that a hardcoded implementation could be flexible.
    Just a question (since I'm still not sure):
    By comparing the GOF approach (in which the logic of the transitions is done by states, so that they can transition to any state) and the more modern approach in which the logic is done out of the states, I was wondering why wouldn't it be possible for the latter approach just to be able to transition to any state as well? (Maybe I'm overlooking something)
     
    Last edited: Jul 28, 2022
  25. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    It should be possible, I think it would simply be a little more complicated. I expect the context needed to make the decisions would most easily be available to the State .. though that might change from situation to situation. Moving the responsibility of state change would then require some additional moving of context. Certainly possible, but perhaps not as "easy", so less "flexible" by definition.

    And just to be clear, I think GOF was primarily saying that the State pattern is agnostic to this particular implementation detail. Maybe they shouldn't have coloured it with the flexible comment, but they also needed a transition to point out the additional interface requirement of such an approach.

    We're focusing quite a lot on a small detail which may have been just a writing consideration .. the book is written in natural language rather than a formal one, after all.
     
  26. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    If you are using a library for this, like Rx, you're probably fine. They should already cover these scenarios. Yet, if you make your own implementation (because of performance, simplicity, technical requirements, etc.) for something that's very dynamic, and you code it following available guides from the internet (even Microsoft's own guide), you could get in some ugly situations.

    I made an example to explain it, because I can't think of another way. Let us have a very basic implementation of of a MessageTrigger Script that's an Observable, and a LaserGun script that observes that MessageTrigger:

    Code (CSharp):
    1. public class MessageTrigger : MonoBehaviour, IObservable<object>
    2. {
    3.     private List<IObserver<object>> m_Observers = new List<IObserver<object>>();
    4.  
    5.     public void Trigger(object messageData)
    6.     {
    7.         for (int i = 0; i < m_Observers.Count; i++)
    8.         {
    9.             m_Observers[i]?.OnNext(messageData);
    10.         }
    11.     }
    12.  
    13.     public IDisposable Subscribe(IObserver<object> observer)
    14.     {
    15.         if (!m_Observers.Contains(observer))
    16.             m_Observers.Add(observer);
    17.  
    18.         return new Subscription(this, observer);
    19.     }
    20.  
    21.     private void Unsubscribe(IObserver<object> observer)
    22.     {
    23.         m_Observers.Remove(observer);
    24.     }
    25.  
    26.     private class Subscription : IDisposable
    27.     {
    28.         private MessageTrigger m_Trigger;
    29.         private IObserver<object> m_Observer;
    30.  
    31.         public Subscription(MessageTrigger trigger, IObserver<object> observer)
    32.         {
    33.             m_Trigger = trigger;
    34.             m_Observer = observer;
    35.         }
    36.  
    37.         public void Dispose()
    38.         {
    39.             m_Trigger.Unsubscribe(m_Observer);
    40.         }
    41.     }
    42. }
    43.  
    44. public class LaserGun : MonoBehaviour, IObserver<object>
    45. {
    46.     public MessageTrigger trigger;
    47.  
    48.     private IDisposable m_Subscription;
    49.  
    50.     private void OnEnable()
    51.     {
    52.         if (trigger)
    53.             m_Subscription = trigger.Subscribe(this);
    54.     }
    55.  
    56.     private void OnDisable()
    57.     {
    58.         m_Subscription?.Dispose();
    59.     }
    60.  
    61.     public void OnNext(object messageData)
    62.     {
    63.         //Destroy everything in the laser's path.
    64.     }
    65.  
    66.     public void OnCompleted() { }
    67.     public void OnError(Exception error) { }
    68. }
    Then, imagine we have three LaserGuns named A, B, C. that all observe the same Trigger. A subscribes first, then B, then C. Finally, let's say that B is pointing at A, so A will be destroyed when B fires. When we call the Trigger method this will happen:
    1. A fires, kills a zombie or something
    2. B fires, kills A.
    3. The enumerator backing the foreach iteration throws an exception because the list was modified.
    We still have problems if we iterate with a for loop instead of a foreach like this:
    Code (CSharp):
    1.         for (int i = 0; i < m_Observers.Count; i++)
    2.         {
    3.             m_Observers[i]?.OnNext(messageData);
    4.         }
    Now we'll skip firing C because it's moved to the index 1, which we already passed when firing B. And if we iterate in reverse, we'll fire B twice for similar reasons.

    There are many solutions to this problem, here's one that's not the prettiest, but it's relatively simple and cost efficient. Basically, when unsubscribing, we'll set the entry in the list to null; then, when we know it's safe to modify the list (i.e. a Singleton's LateUpdate), we remove those null entries.

    First add this variable and method in MessageTrigger to remove all null observers. It'll be used later:
    Code (CSharp):
    1.     private bool m_CleanupQueued;
    2.  
    3.     public void CleanUp()
    4.     {
    5.         m_CleanupQueued = false;
    6.         m_Observers.RemoveAll(observer => observer == null);
    7.     }
    Then add this class as a singleton Monobehavior to call that CleanUp method when it's safe to do so:
    Code (CSharp):
    1. // Implement this as a singleton however you like.
    2. public class TriggerCleanner : MonoBehaviour
    3. {
    4.     static Queue<MessageTrigger> s_TriggersToClean = new Queue<MessageTrigger>();
    5.  
    6.     public static void QueueForCleanUp(MessageTrigger trigger)
    7.     {
    8.         s_TriggersToClean.Enqueue(trigger);
    9.     }
    10.  
    11.     private void LateUpdate()
    12.     {
    13.         while (s_TriggersToClean.Count > 0)
    14.             s_TriggersToClean.Dequeue().CleanUp();
    15.     }
    16. }
    Finally, we change MessageTrigger.Unsubscribe to set the observer entry to null and Queue a clean up of null entries:
    Code (CSharp):
    1.     private void Unsubscribe(IObserver<object> observer)
    2.     {
    3.         for (int i = 0; i < m_Observers.Count; i++)
    4.         {
    5.             if (m_Observers[i] == observer)
    6.             {
    7.                 m_Observers[i] = null;
    8.        
    9.                 // Check this bool to avoid unnecessary CleanUp calls.
    10.                 if (!m_CleanupQueued)
    11.                 {
    12.                     TriggerCleanner.QueueForCleanUp(this);
    13.                     m_CleanupQueued = true;
    14.                 }
    15.  
    16.                 break;
    17.             }
    18.         }
    19.     }
    Of course, all this is very basic and can be improved. Here's an extra: Now we can avoid triggering observers that were added while the Trigger method was running by caching the observers list count. It's safe because we now know that the list's count won't be reduced while it's being triggered:
    Code (CSharp):
    1.         var count = m_Observers.Count;
    2.         for (int i = 0; i < count; i++)
    3.         {
    4.             m_Observers[i]?.OnNext(messageData);
    5.         }
     
    Last edited: Jul 29, 2022