BehaviourToolkit
BehaviourToolkit copied to clipboard
Redesign feature request: Simplify transitions
Problem description
So I have a pretty simple FSM with 4 states: Idle, Walk, FollowUntil (follow enemy until it's not in attack range), Attack.
I want to have an opportunity to make transitions almost from every state to every other state. And sometimes I want to configure some transition using scripts. But it turns out I have to make 4 (states amount) * 3 (other states amount) = 12 transitions. Looks pretty bad... And I have to make even more when I make new states. And it turns out we have geometry progression of transitions...
Yes, I know we can call switch_state() on FSM to switch states. But it makes transitions uncustomizable. And mixing switch_state() and events are kinda bad decision
Possible solutions
Remove transitions
First thing that comes to mind is to remove transitions at all and make function like _handle_transition(to_state: String) in FSMState template. So you can process needed transitions using code
Smart solution - notify transitions during switch_state()
Ok. So the problem is "I want to change states from one to another and also have opportunity to customize some transitions". To solve this we can make switch_state() the desired way to change states instead of fire_event(). And in switch_state() function we will seek for child transition that satisfies our logic and call _on_transition() of this node if found
Hey! I get what you are trying to say. Behaviours can become REALLY complex very fast, but there are some ways to optimize your stay machine.
- Reuse transition scripts: If you are not working that way already, you can reuse scripts again, if you need the same transition.
- Use more literal names. Instead of something like "toSprint", I'd suggest to use "onStartSprinting" to give more visual clarity. Name it after the event, rather than the state.
- Cut down on the transitions: If you draw your state machine as a chart on paper, you also get a huge mess. You can cut down on transitions by working with them in a smart way. For example: Do you really need a transition from ATTACK to MOVE? If there isn't anything special in this change of state in particular, ATTACK -> IDLE -> MOVE is more than enough. You can checkout the
2.0.0branch, as I already added an example for a simple character controller there.
Regarding your possible solutions:
Remove transitions
I don't think this solutions fits into the whole node based approach of the plugin. Yes, trees will get messy and big, but that's just FSMs and BTs for you. For more complex behaviours, I always suggest keeping your machines in their own scene, so you can reuse them aswell. Something like _handle_transition(to_state) will only result in big match statements and confusing code.
notify transitions during switch_state()
I am not sure, if I am understanding this correctly? Right now you can trigger a transition in 3 ways.
change_statewithout any logic and hardcodedfire_eventif you use any event on the transition- Basic transition logic in the
ìs_validmethod
Could you maybe give an example on how your idea would work and what it would change?
I am not sure, if I am understanding this correctly? Right now you can trigger a transition in 3 ways.
Exactly. The problem is you have to choose one of three ways. And each way has some limitations.
change_statewithout any logic and hardcoded
This way lacks customization
fire_eventif you use any event on the transition
This way needs too much nodes in simple constructions. For example if you want to have basic transition from one state to another you must have state_count * (state_count - 1) transitions.
My idea here is to make transition nodes more "optional".
So we use first way with change_state function. But we also check state we are transitioning from for transition nodes. If we find transition node that refers to state passed to change_state function, we call its code.
Just for better understanding let me show what I mean in pseudocode:
## Changes the current state and calls the appropriate methods like _on_exit and _on_enter.
func change_state(state: FSMState) -> void:
# Exit the current state
active_state._on_exit(actor, blackboard)
+ for child in active_state.get_children():
+ if child.next_state == state:
+ child._on_transition()
+ break
# Change the current state
active_state = state
# Enter the new state
active_state._on_enter(actor, blackboard)
# Emit the state changed signal
emit_signal("state_changed", active_state)
Idea here is to connect change_state and transition nodes functionality
I see what you mean. Let me think about these changes and I'll give you some feedback tomorrow. Generally I don't consider many nodes a problem, because the whole plugin is designed around the idea of state machines having actual componenst you can drag around instead of abstract transitions in code, but adding more functionality can't hurt. As said, I'll try some things out and get back to you!
But it turns out I have to make 4 (states amount) * 3 (other states amount) = 12 transitions. Looks pretty bad... And I have to make even more when I make new states. And it turns out we have geometry progression of transitions...
I ended up writing a make shift plugin to edit the transitions as a table. Makes it easier to get a bird's eye view of the machine when you have a lot of states and events.
I ended up writing a make shift plugin to edit the transitions as a table. Makes it easier to get a bird's eye view of the machine when you have a lot of states and events.
If this is in a usable state (or even a good starting point), feel free to open a PR draft or something :)
If this is in a usable state (or even a good starting point), feel free to open a PR draft or something :)
Done