Search Unity

Official State Machine

Discussion in 'Open Projects' started by Dustin_00, Sep 29, 2020.

  1. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    That's the point, we would like to participate on writing the foundation too and not just use some given codebase and adapt to it.
    Look at the Inventory System thread, they are actively discussing the approach to use in order to build the system. Here on the other hand it's more something like "I'll write the system and you will like it".

    Anyway, since collaboration is not possible, I will start writing my own implementation of a state machine based on ScriptableObject. I took a look at the Pluggable AI tutorial and I like it.

    Then together we will decide with implementation to use.
     
    Last edited: Oct 3, 2020
    cirocontinisio likes this.
  2. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    It would be cool to use an incremental approach IMO. With "each developer makes a PR, best PR gets approved" some workforce will go wasted.
     
  3. cirocontinisio

    cirocontinisio

    Joined:
    Jun 20, 2016
    Posts:
    884
    Good question, and I think @Proryanator already provided the best answer.

    We don't want to assign a feature to a single person and they own it forever. That's why I encourage everyone to:
    1) Discuss things first, at least get a feeling of whether a direction they want to go towards is desirable.
    2) Make small contributions. Start with a v1 of anything. That will give others the ability to join and expand.

    So to summarise: yes, best PR gets approved (within a reasonable timeframe), and sometimes it will be the simplest one.
    And then, we keep building on it incrementally.
     
    Proryanator and kcastagnini like this.
  4. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
  5. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    Neonage likes this.
  6. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    Proryanator likes this.
  7. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    Can't wait to see yours ;)
     
    kcastagnini likes this.
  8. OTG_JR

    OTG_JR

    Joined:
    Jul 29, 2019
    Posts:
    1
    Here we go, I have a PR up with my stab at this implementation

    https://github.com/UnityTechnologies/open-project-1/pull/57

    Let me know what you think, this would be the first time sharing this as I have been pluggin ;) away at it for a few months.
    This is a short demo of what I was able to piece together



    Thanks
     
    Proryanator likes this.
  9. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    Welcome to the fold of PR's :D I'll take a look!
     
    kcastagnini likes this.
  10. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Hehe, it's the same concept as Pluggable AI, but hard-coded.
    That's a good example of how hard it would be to write and maintain non-generic system :D
     
  11. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    Hello neonage, I had a look at your code.
    I don't like that there's no compile-time safety when you call GetSharedData<T>() or GetData<T>() to try to get the needed data to perform actions or decisions.
    Plus, as you say in your comments, "getting data introduces unboxing to generic on every update" which isn't good for performance.

    So why not implementing the whole system using generics?
    Something like Action<T>, Decision<T>, State<T>, StateMachine<T> etc.

    This is for example how I am implementing the Action class.
    Action.PNG

    And this is how it will be used:
    first the user defines a class that will be used across all actions and decisions.
    dataModel.PNG

    Then the user can create an Action based on that class.
    TestAction.PNG

    An instance of StateMachine<DataModel> holds a reference to an instance of DataModel and passes it to actions and decisions (through the current state) on every frame. No casting and no boxing required.

    This is how a State looks like btw, in our example we would create a State<DataModel> subclass.
    State.PNG

    The hard part now is writing a custom inspector that works with generics so that the user can only drag and drop scriptable objects of the correct type, but I found a way to do it.
    I will need some help to make stuff look good though, I'm not an editor scripting expert and I'll need your help.
     
    Last edited: Oct 4, 2020
    Proryanator and Neonage like this.
  12. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    That's actually a genius idea, I forgot about it! :eek:
    upload_2020-10-5_3-3-35.png

    Now code looks like Entities.ForEach which is what I wanted :D
    It just needs a lot of codegen for every possible case

    I'll refactor it tomorrow and update PR
     
    Last edited: Oct 4, 2020
  13. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    No, we can just include all the dependencies needed inside the generic class we are passing to the actions!
    In my previous example you could include the NavMeshAgent inside the DataModel class and that's it!
    This is why I am building the system this way.
     
  14. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    In my implementation I also took a slightly different design approach.

    I don't really like the fact that transitions are defined inside states. Why do states need to know about transitions at all?
    A state should just be responsible of performing actions when needed at that's all.
    Plus it's hard to keep track of the logic of the FSM because transitions are all scattered around.
    Finally, if I want to edit a transition I need to modify a state too which doesn't make sense at all.

    That's why I am using a TransitionTable in my implementation and it looks like that:
    transitionTableExample.PNG

    As you can see a TransitionTable is a list of, well, transitions (ALL of them)! A state machine will simply hold a reference to a TransitionTable and will change state accordingly.
    You can read the lines like that: "Go from [insert state] to [insert state] if [insert condition] is met".
    By doing so we have all the transition logic in one place, plus we can modify/add/remove transitions without editing states (and now states looks cleaner too as they are just a collection of actions to perform).
    We can also easly test multiple tables by simply swapping the transition table referenced by the state machine!

    This approach has other benefits too, like the possibility to add a "Any state to state transitions" list to the table, where you can say something like "Go from any state to [insert state] if [insert condition] is met".

    By the way @Proryanator , this looks a lot like Jason's solution, where you can call the method AddTransition(state, state, boolFunc) to populate the transition table. This is simply the Editor version of it!
     
    Last edited: Oct 4, 2020
    MUGIK, Neonage and Proryanator like this.
  15. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    @Neonage I like what you're trying to accomplish, with caching of components in the state machine and trying to keep Actions/Decisions/Transitions separate.

    I pulled your branch and have been poking through the inheritance structure and various base classes to truly understand what you're trying to make! It took me a good hour or so to follow the 'OnUpdate' 'BeginUpdate' structure but I see what you're doing now. I do have some thoughts and overall I do think it is a little more complicated up front :)

    I like that you have a StateNode that stores actions/triggers and transitions together, essentially being the 'state' that you'd typically read about in Design Patterns. That's pretty clean to keep that there as opposed to just shoving those into the state machine.

    I did notice that ScriptableState for the most part looks like a wrapper for StateMachine calls as well as defining the 'BeginUpdate' which appears to be similar to the 'Tick' of state machines. Seeing as how StateNode just stubs this call out, it seems like ScriptableState is simply storing a reference to the current StateMachine more than anything, and you could just create method calls directly for StateDecision.OnDecide or StateAction.OnUpdate from within StateNode to be a bit more simple and pass in the state machine reference ;)

    I'm not sure if there's a way around this too, looks like a non programmer can put in a Transition defined by a StateDecision, and have StateDecisions be defined in the place of StateActions. This might be a side-effect with SO's which I'm not too sure on but I found this interesting:

    If Is Hunger, then take the following 2 'actions': Is Hunger and Is Hunger :confused:
    possibility.PNG

    I also haven't gotten to see/read the event code that you wrote too, so I'll be taking a look at that. Some food for thought :insertfoodofchoice:
     
    Neonage likes this.
  16. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    @kcastagnini sounds clean let us know when your code is up! :)
     
    kcastagnini likes this.
  17. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Composition would be much easier to read and work with IMO.
    Instead of:
    StateAction<ChaserDataModel> model.agent, model.chase

    It would be:
    StateAction<NavMeshAgent, ChaseState> agent, chase.
     
  18. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    That creates other problems actually. This list can became monstrous at the end and will be super hard to manage.
    It also kinda accident-prone, super easy to introduce unexpected behaviour just by wrong list order.

    I think having it tupled together makes it easy to composite and understard.

    What I'd like to introduce is StateFlow SO that will have nice GraphView editor with all StateNodes relations.
    It can also store "Any State" node.
    And we will reference this SO instead of single StateNode
     
    Last edited: Oct 5, 2020
  19. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Yeah, it was kinda stupid to make list of anything. :)
    It would be better to add nullable nextState and just list of StateActions
     
    Last edited: Oct 5, 2020
    Proryanator likes this.
  20. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    Why monstrous? Transition tables works with generics too, meaning that we have a transition table for every state machine we define, and the inspector will be type safe, meaning that you can drag only states/traditions of a certain fsm.
    For example inside a TransitionTable<DataModel> you can only have State<DataModel> and Condition<DataModel>.
    Also order doesn't have a meaningful impact on the functionality.

    Having transitions tupled together on the other hand makes the transition logic hard to follow because you have to jump from one state to the other just to understand how the fsm works.
    Instead of having 15 transitions in one place, you have 15 transitions all over the place.
    Plus let's say you want to test a different transition, you would have to duplicate the state that contains that specific transition, modify it and rewire the fsm.
    With a transition table is just a matter of editing a single entry.
    Let's say you want to test a totally different transition logic, you would have to duplicate all the states and modify them.
    With transition tables you would just create a new one and reuse the same states all over again, just rearranged differently.

    Anyway let's keep things separated, it's better to work on different implementation and then we will all decide which to use.
     
    Last edited: Oct 5, 2020
    Neonage likes this.
  21. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    I'm waiting to try out yours while refactoring my own! :)

    Btw, sometimes you would want to use non-generic action.
    Like for ex. how would you place AudioEvent<AudioSource> inside StateMachine<ChaseDataModel> ?
     
    kcastagnini likes this.
  22. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    I would modify the DataModel and add the required dependency.
    In the tutorial you have linked the instructor was passing the same instance of StateController to every state, and actions and decisions worked on StateController by calling methods like GetNavmesh, GetRaycast etc.
    I wanted to take the same approach and to make it simply more generic by substituting StateController with a user defined type.

    I'll try to get out a v1 between today and tomorrow, I understand it's harder without some code on the screen.
     
  23. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Well, it's easy, but requires code changes every time you want to add new dependency.
    In the end it's not flexible for the designers and there would be a lot of boilerplate code.
    That's what I'm trying to avoid as much as possible with my implementation
     
    Proryanator likes this.
  24. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    :p:p:p:p:p
    I can see that yeah it depends on how we want the project to be, more designer friendly or developer friendly.

    I can see both having advantages, where the code way you can source control and better view changes to states through code reviews, whereas making changes to SO's and prefabs via changing their transitions, you'd kinda can't track through git very well the changes made, or viewing a blob essentially in a PR.

    On the other hand, using a designer friendly setup makes it easier for anyone to make additons/changes, which may outweight the afformentioned benefit.

    Hmmmm.
     
    Neonage and kcastagnini like this.
  25. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    I'm trying to find the golden medium between this :p
     
    Proryanator likes this.
  26. fran_m

    fran_m

    Joined:
    Jul 22, 2020
    Posts:
    40
    PR=Pull Request?
     
    Proryanator likes this.
  27. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    I guess so? :confused:
     
    Proryanator likes this.
  28. samlletas

    samlletas

    Joined:
    Jan 2, 2016
    Posts:
    30
    I looked a bit at Neon's code and I wouldn't worry about GetSharedData<T>() since nothing is being unboxed there, its just casting, unboxing only happens when converting from a reference type to value type.

    I'm liking the idea of the transitions being separate from the states themselves, seems to be more designer friendly, it could allow to setup different transitions for NPCs depending on the difficulty level and create entirely new behaviors, or even just quickly test different combinations for a boss fight AI, all by simply using a different transition table.
     
    Neonage likes this.
  29. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    Haha sorry yeah, Pull Request! I say PR more than my wife's name.
     
  30. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    I've just remembered a fancy way to fix this :)

    upload_2020-10-6_19-39-13.png
     
  31. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    And don't forget about disabled domain reload
    upload_2020-10-6_20-43-37.png
     
  32. samlletas

    samlletas

    Joined:
    Jan 2, 2016
    Posts:
    30
    That's very nice! I didn't know that static generic classes were a thing, had to do some research to understand what was going on there. So essentially you are storing a dictionary per TComponent type thus avoiding the casting, it seems like a clean way to do caching, I think we would only need to clear the dictionaries after scene changes.

    By the way, at what moment are components cached?

    Thinking more about it, this could also be useful for implementing other stuff later, such as an object pool system for enemies, bullets, etc;
     
    Proryanator and Neonage like this.
  33. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Good point! Hope gameObject.scene property is not slow to get.
    When you call get and it's not presented in the dictionary :)
     
    samlletas likes this.
  34. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    Proryanator, samlletas and Neonage like this.
  35. samlletas

    samlletas

    Joined:
    Jan 2, 2016
    Posts:
    30
    I like the transitions table idea, its very easy to make changes and provides an overview of all the possible state transitions.
     
    kcastagnini likes this.
  36. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Proryanator likes this.
  37. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    Updates my purely code bases State Machine implementation: https://github.com/UnityTechnologies/open-project-1/pull/33

    Merged in and inserted the UpdateSlide() logic, as well as resolving some comments :)

    I haven't looked into visualising my code in the inspector but, that could be an iteration added later on! #code-junky
     
    Neonage likes this.
  38. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    #Corrupted-by-Design
     
    Proryanator likes this.
  39. Proryanator

    Proryanator

    Joined:
    Sep 22, 2013
    Posts:
    29
    Haha :'D
     
  40. MUGIK

    MUGIK

    Joined:
    Jul 2, 2015
    Posts:
    481
    I have some thoughts about why state machines should be using MonoBehaviour(MO) instead of ScriptableObject(SO).

    Yes, for things like PlayerStateMachine the SO workflow will work fine, despite that system becomes messy and complicated. But if we really want to make a modular system, so designers can assemble their own state machines, we should look at using MO.

    For example, designer wants to make a flower that can be in states: NORMAL, WITHERED, BLOOMING, etc.
    This flower will exist only in one scene and will need references to other objects in the scene to make transitions(for example, a river that could be blocked/unblocked by the player).
    Do we really need to create assets for this kind of behavior?
    And even if you will end up creating a state machine as an asset - it's eazy-pizy! Just make a prefab out of state machine!

    And now that's how it would look in the hierarchy:
    upload_2020-10-9_12-8-39.png
    Each state is an individual GO, that can handle it's own references to scene objects.

    What do you think?
     
    Neonage likes this.
  41. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    StateMachine by itself is actually MonoBehaviour.
    It stores all per-instance references/data, links them to States (assets) and executed their logic :)
     
    kcastagnini likes this.
  42. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    Each StateMachine is already a MonoBehaviour and it stores all the scene references needed in order to evaluate conditions and perform actions.
    In my implementation a StateMachine also stores a reference to a TransitionTable (which is a SO asset) that defines the transition logic of the StateMachine, so that you can simply change behaviour by changing the associated table.
    If you want to have different state machines on the scene to behave the same way, just link them with the same transition table.

    States are assets too and they only contain logic to perform actions and evaluate conditions. They don't store any sort of data that is connected to a specific StateMachine in the scene.
     
    Last edited: Oct 9, 2020
  43. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    Yes, and there's one more advantage which is more important IMO.
    If a designer wants to test a new transition logic he just needs to create a single new transition table instead of duplicating all the states and chancing all the transitions for each states.
     
  44. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Fixed that thing :)
    upload_2020-10-10_3-25-59.png
     
  45. cirocontinisio

    cirocontinisio

    Joined:
    Jun 20, 2016
    Posts:
    884
    I spent some time reviewing the different state machine PRs...
    @Neonage, @OTG_JR and @kcastagnini, you made some nice implementations, but they are HUGE! 138, 111 and 113 file changes :eek:
    Honestly I'm a bit worried to accept such huge structures into the project, meaning now nobody can change how this structure works but you.

    @Proryanator I like yours too, but it's true that having no visual representation is a downside.

    Honestly I'm a bit on the verge on this one. Now I'm thinking, as @TomateSalat suggested, why not use Bolt at least to create the state machine? (Bolt has support for what it calls State Graphs) While the functionality called by it could be a mix of Bolt graphs/nodes, and custom code.

    At least we could benefit from the visualisation as a graph, and we wouldn't have to explain the whole thing to whoever wants to tweak the project (there's an actual manual for that!)
     
    TomateSalat likes this.
  46. kcastagnini

    kcastagnini

    Joined:
    Dec 14, 2019
    Posts:
    61
    @DapperDino has just published a video on how to do that, I don't know if this was planned all along or it's just perfect timing xD

     
  47. cirocontinisio

    cirocontinisio

    Joined:
    Jun 20, 2016
    Posts:
    884
    Haha, he really is on the ball with Bolt... I actually watched this one today, which is a more introductory version I guess.
    Oh wow, actually, the video seem to take from this project, look at the variable names:

    Screenshot 2020-10-12 at 00.11.39.png

    So I guess we're just closing the loop :D
     
  48. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    It's mostly because of the examples, it can be packed nicely into single Tests folder, so it doesn't affect other peoples work for now.

    Objection! Wasn't out initial plan - simplicity and natural evolve over time?

    I've joined this project to have a real experience of making something great as a team from pure ideas, as we might create something unique, yet simple, that anyone could use in their project, without huge libraries like Bolt.

    I want to create GraphView for my implementation, so I'll take a look at it.
    And we don't really need to explain something if it's intuitive :)

    Btw, I dislike some Bolt FSM design, for ex. you can put these visual code snippets inside "Macro" and everything you see in overview is this!
    upload_2020-10-12_6-48-51.png
     
  49. Neonage

    Neonage

    Joined:
    May 22, 2020
    Posts:
    287
    Here's comment under this video on reddit. Some nice food for thinking
    upload_2020-10-12_22-22-49.png
     
    MagdielM likes this.
  50. fran_m

    fran_m

    Joined:
    Jul 22, 2020
    Posts:
    40
    Trying to understand this thread according to my state of knowledge: what can do any of the FSM approaches (Bolt, custom FSM,...) that Animation Controller cannot do?