Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions addons/behaviour_toolkit/finite_state_machine/fsm.gd
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,16 @@ signal state_changed(state: FSMState)
## Whether the FSM should print debug messages.
@export var verbose: bool = false


## The list of states in the FSM.
var states: Array[FSMState]
## The current active state.
var active_state: FSMState
## The list of current events.
var current_events: Array[String]
## The list of nested FSMs in the FSM.
var nested_state_machines: Array[FiniteStateMachine]
## Nesting Depth (0 = not nested)
var nest_level = 0
## Current BT BTStatus
var current_bt_status: BTBehaviour.BTStatus

Expand Down Expand Up @@ -95,7 +98,8 @@ func start() -> void:
for state in get_children():
if state is FSMState:
states.append(state)



if verbose: BehaviourToolkit.Logger.say("Setting up " + str(states.size()) + " states.", self)

active = true
Expand Down Expand Up @@ -202,5 +206,10 @@ func _get_configuration_warnings() -> PackedStringArray:
for child in children:
if not child is FSMState:
warnings.append("Node '" + child.get_name() + "' is not a FSMState.")


if initial_state:
# check if initial_state is a descendant of this FSM
if not is_ancestor_of(initial_state):
warnings.append("Don't select initial state outside of this FSM")

return warnings
2 changes: 1 addition & 1 deletion addons/behaviour_toolkit/finite_state_machine/fsm_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func _get_configuration_warnings() -> PackedStringArray:
var warnings: Array = []

var parent: Node = get_parent()
if not parent is FiniteStateMachine:
if not (parent is FiniteStateMachine or parent is NestedFSM):
warnings.append("FSMState should be a child of a FiniteStateMachine node.")

return warnings
38 changes: 35 additions & 3 deletions addons/behaviour_toolkit/finite_state_machine/fsm_transition.gd
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,48 @@ func get_next_state() -> FSMState:


func _get_configuration_warnings() -> PackedStringArray:
var warnings: Array = []
var warnings: PackedStringArray = []

var parent: Node = get_parent()
if not parent is FSMState:
warnings.append("FSMTransition should be a child of FSMState.")

if not next_state:
warnings.append("FSMTransition has no next state.")

return warnings

if use_event and event == "":
warnings.append("FSMTransition has no event set.")

#HACK: Could benefit from caching fsm
var fsm := _find_fsm()
if fsm:
# Get paths directly
var our_path := fsm.get_path_to(get_parent())
var their_path := fsm.get_path_to(next_state)

# Debug prints
print(name, " Our Path: ", our_path.get_name_count())
print(name, " Next Path: ", their_path.get_name_count())

# Compare the number of node names in the node paths
var our_size := our_path.get_name_count()
var their_size := their_path.get_name_count()

# Check if they have different nesting levels
if our_size != their_size:
warnings.append("FSMTransition should not transition outside of this NestedFSM.")
# Check if they're at the same level but in different branches (different immediate parents)
elif our_size >= 2 and their_size >= 2:
if our_path.get_name(our_size - 2) != their_path.get_name(their_size - 2):
warnings.append("FSMTransition should not transition outside of this NestedFSM.")

return warnings

func _find_fsm() -> FiniteStateMachine:
var current: Node = get_parent()
while current:
if current is FiniteStateMachine:
return current as FiniteStateMachine
current = current.get_parent()
return null
109 changes: 109 additions & 0 deletions addons/behaviour_toolkit/finite_state_machine/nested_fsm.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
@tool
extends FSMState
class_name NestedFSM

# TODO:
# Make Template
# Integrate into UI
# Add to the documentation

## An implementation of a simple finite state machine.
##
## The Nested Finite State Machine is a state that contains a FiniteStateMachine insde
## This Instanced FiniteStateMachine inherits values from the parent FiniteStateMachine.
## On ready, the NestedFSM will reparent each child that is an FSMState or NestedFSM to the new FiniteStateMachine.
## To implement your logic you can override the [code]_on_enter, _on_update and
## _on_exit[/code] methods when extending the node's script.


## The signal emitted when the fsm's state changes.
signal nested_state_changed(state: FSMState)

@export var initial_state : FSMState
@export var verbose : bool

# Create a child FSM instance to manage the nested states
@onready var fsm = FiniteStateMachine.new()

func _ready() -> void:
super()

if Engine.is_editor_hint():
return

# Find all direct child nodes that are FSMStates
var nested_states : Array[Node] = get_children().filter(func(n): return n is FSMState)
# Find all direct child nodes that are NestedFSMs (for hierarchical nesting)
var nested_fsms : Array[Node] = get_children().filter(func(n): return n is NestedFSM)

# Add the FSM container as a child node, and rename it for clarity in scene tree
add_child(fsm)
fsm.name = "NestedFSM"

# Move all FSMState children to the internal FSM container
for state in nested_states:
state.reparent(fsm)

# Move all nested FSMs to the internal FSM container
for nested_fsm in nested_fsms:
nested_fsm.reparent(fsm)

# Configure the internal FSM
fsm.initial_state = initial_state # Set the starting state
fsm.verbose = verbose # Set debug output preference

# Get reference to the parent state machine
# After reparenting, our parent should always be a FiniteStateMachine
# (either the root FSM or another NestedFSM's internal FSM)
var parent_state_machine : FiniteStateMachine = get_parent()

# Share resources with the parent FSM
fsm.actor = parent_state_machine.actor # Share the actor reference
fsm.process_type = parent_state_machine.process_type # Share process type (physics/idle)
fsm.blackboard = parent_state_machine.blackboard # Share the blackboard (shared data)

# Connect the internal FSM's state_changed signal to our nested_state_changed signal
fsm.state_changed.connect(func(state: FSMState): nested_state_changed.emit(state))

# Executes after the state is entered.
func _on_enter(_actor: Node, _blackboard: Blackboard) -> void:
fsm.start()

# Executes before the state is exited.
func _on_exit(_actor: Node, _blackboard: Blackboard) -> void:
fsm.active = false


# TODO: Improve configuration warnings for Nested FSM
# Now that NestedFSMs exist, there should be one to account for when the user selects a state from another FSM as an intitial state


# Add custom configuration warnings
# Note: Can be deleted if you don't want to define your own warnings.
func _get_configuration_warnings() -> PackedStringArray:
var warnings: Array = []
warnings.append_array(super._get_configuration_warnings())

var parent: Node = get_parent()
if not parent is FiniteStateMachine:
warnings.append("NestedFSM should be a child of a FiniteStateMachine node.")

if not initial_state:
warnings.append("FSM needs an initial state")
return warnings # Return early if no initial state

var fsm := _find_fsm()
if fsm and initial_state:
# check if initial_state is a descendant of this FSM
if not is_ancestor_of(initial_state):
warnings.append("Don't select initial state outside of this FSM")

return warnings

func _find_fsm() -> FiniteStateMachine:
var current: Node = self
while current:
if current is FiniteStateMachine:
return current as FiniteStateMachine
current = current.get_parent()
return null
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cffv6g18yifit
1 change: 1 addition & 0 deletions addons/behaviour_toolkit/ui/toolkit_ui.gd
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func _ready():
# Connect buttons
%ButtonAddFSM.connect("pressed", _on_button_pressed.bind(FiniteStateMachine, "FiniteStateMachine"))
%ButtonState.connect("pressed", _on_button_pressed.bind(FSMState, "FSMState"))
%ButtonNestedFSM.connect("pressed", _on_button_pressed.bind(NestedFSM, "NestedFSM"))
%ButtonTransition.connect("pressed", _on_button_pressed.bind(FSMTransition, "FSMTransition"))
%ButtonStateIntegratedBT.connect("pressed", _on_button_pressed.bind(FSMStateIntegratedBT, "FSMStateIntegratedBT"))
%ButtonStateIntegrationReturn.connect("pressed", _on_button_pressed.bind(FSMStateIntegrationReturn, "FSMStateIntegrationReturn"))
Expand Down
6 changes: 6 additions & 0 deletions addons/behaviour_toolkit/ui/toolkit_ui.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ layout_mode = 2
text = "New State"
icon = ExtResource("1_hqqj5")

[node name="ButtonNestedFSM" type="Button" parent="ScrollContainer/MarginContainer/VBoxContainer/Panel/ScrollContainer/Toolbox/FiniteStateMachine"]
unique_name_in_owner = true
layout_mode = 2
text = "New NestedFSM"
icon = ExtResource("1_hqqj5")

[node name="ButtonTransition" type="Button" parent="ScrollContainer/MarginContainer/VBoxContainer/Panel/ScrollContainer/Toolbox/FiniteStateMachine"]
unique_name_in_owner = true
layout_mode = 2
Expand Down
Loading