From 2eeffbd723557413f8fd6204b2fe1e8aca2c23cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Go=C5=82=C4=85b=20=28SirPigeonz=29?= Date: Fri, 1 Dec 2023 02:09:42 +0100 Subject: [PATCH 1/4] Makes Behaviors more dynamic at runtime This PR makes both FSM and BTree systems work properly when their nodes are moved, removed, added at runtime. On top of that many nodes are now fine when they don't have important for them nodes as a child or property references. This allows for preparing "empty" slots for intended behaviours or branches and modeling main behaviours with them, that is "slots", in mind. Upcomming warning system can be used instead for important configuration warnings when behaviours are created by hand in the Editor. Changes: - `BTRoot`, `FiniteStateMachine`, `BTComposite`, `BTDecorator`, `FSMStateIntegratedBT` and `BTIntegratedFSM` nodes now can reconfigure themselves when their children are modified at run-time - allow `BTRoot` to not have any BTBehaviour as a child - allow `FiniteStateMachine` to not have any FSMStates as a children - allow `FiniteStateMachine` to not have a initial state - allow `FSMStateIntegratedBT` to not have any BTRoot as a child - allow `BTIntegratedFSM` to not have any FSM as a child - `BTIntegratedFSM` has now default_status property enum for case where it doesn't have FSM child - new method in `BTRoot` - `swap_entry_point()` - new method in `FSMStateIntegratedBT` - `swap_behaviour_tree()` - new method in `BTIntegratedFSM` - `swap_finite_state_machine()` - make `FSMStates` and extended nodes update their transitions list when children are added/removed - `FSMStateIntegratedBT` disables autostart of its BTRoot - `BTIntegratedFSM` disables autostart of its FSM - made `FSMState` extensible without need to call super() on _ready() - made `BTRandom` extensible without need to call super() on _ready() - Added few missing documentation comments, to outline how nodes are intended to work and how they should be edited also at run-time. Changes after review 1 - removed `_disable_autostart()` in FSM and BTRoot - made `BTRoot` and `FSM `set their `autostart` to `false` and hide them in Editor Inspector if their parent is a integration type node - made `BTRoot` and `FSM` a @tool - added Engine.is_engine_hint guards to ready, callbacks and processes for `BTRoot` and `FSM` - added `keep_group` optional property to `swap_'nodetype'()` methods, it allows to preserve original nodes groups from swapped node in the new node Changes after review 2 If you wish to add, remove, move `FSMState` nodes at run-time first add new `FSMStates` stop the FSM with method `FiniteStateMachine.exit_active_state_and_stop` and re-start it with method method `FiniteStateMachine.start` providing one of the new states either as start method property or change member `FiniteStateMachine.initial_state` before running `start()`. After this procedure you can delete unused states. - made `active` property read-only - modified `start()` in fsm.gd to accept `FSMState` property as a start point - new method exit_active_state_and_stop() to pair with `start()` - above two changes make FSM startable and stoppable for example to safely modify `FSMStates` and resume running of the FSM - removed some `if` guards from proccess function and made checks when something changes in the setup - made BTRoot cleanups and made it to not check for entry point in processing - changed some configuration warnings. Mostly changed statement that state "nodes must have child nodes", to "nodes SHOULD have child nodes to work" to inform user that they wont work but nothing bad will happen if they don't - removed warning for `BTLeaf` that "BTLeaf node must not have any children.". Reason is that there is no issue if it has one, it can prevent user to use some nice composition on top of `BTLeaf` for no reason :) --- .../behaviour_tree/bt_behaviour.gd | 1 + .../behaviour_tree/bt_composite.gd | 18 +- .../behaviour_tree/bt_decorator.gd | 22 ++- .../behaviour_tree/bt_leaf.gd | 3 - .../behaviour_tree/bt_root.gd | 82 +++++++-- .../composites/bt_integrated_fsm.gd | 72 +++++++- .../behaviour_tree/composites/bt_random.gd | 7 +- .../finite_state_machine/fsm.gd | 162 ++++++++++++------ .../finite_state_machine/fsm_state.gd | 31 ++-- .../fsm_state_integrated_bt.gd | 64 ++++++- examples/behaviour_example/NPCs/villager.tscn | 37 ++-- examples/behaviour_example/actor/actor.gd | 1 + 12 files changed, 386 insertions(+), 114 deletions(-) diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd b/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd index 15ae654..e84f675 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd @@ -7,6 +7,7 @@ class_name BTBehaviour extends BehaviourToolkit ## which control the flow of the behaviours in Behaviour Tree system. +## Status enum returned by nodes executing behaviours. enum BTStatus { SUCCESS, FAILURE, diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd b/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd index 3f171a8..c79a90e 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd @@ -5,12 +5,28 @@ class_name BTComposite extends BTBehaviour ## ## Composites can hold multiple behaviour nodes and evalute/execute them ## based on custom logic based on their return values. +## [br][br] +## By itself is not doing much but is aware of it's children. You can use it +## to implement custom composite behaviours. + + +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_child_order_changed: int = \ + child_order_changed.connect(_on_child_order_changed) ## The leaves under the composite node. @onready var leaves: Array = get_children() +func _on_child_order_changed() -> void: + if Engine.is_editor_hint(): + return + + leaves = get_children() + + func _get_configuration_warnings() -> PackedStringArray: var warnings: Array = [] @@ -21,7 +37,7 @@ func _get_configuration_warnings() -> PackedStringArray: warnings.append("BTComposite node must be a child of BTComposite or BTRoot node.") if children.size() == 0: - warnings.append("BTComposite node must have at least one child.") + warnings.append("BTComposite node should have at least one child to work.") if children.size() == 1: warnings.append("BTComposite node should have more than one child.") diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd b/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd index 4db5f42..7c89e6e 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd @@ -5,10 +5,19 @@ class_name BTDecorator extends BTBehaviour ## ## Decorators are used to augment the behaviour of a leaf.[br] ## Think of it as another layer of logic that is executed before the leaf. +## [br][br] +## By itself is not doing much but is aware of it's children and holds reference +## to its first child (index 0 child). You can use it to implement custom +## decorators. +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_child_order_changed: int = \ + child_order_changed.connect(_on_child_order_changed) + ## The leaf the decorator is decorating. -@onready var leaf: BTBehaviour = _get_leaf() +@onready var leaf: BTBehaviour = _get_leaf() func _get_leaf() -> BTBehaviour: @@ -18,6 +27,13 @@ func _get_leaf() -> BTBehaviour: return get_child(0) +func _on_child_order_changed() -> void: + if Engine.is_editor_hint(): + return + + leaf = _get_leaf() + + func _get_configuration_warnings() -> PackedStringArray: var warnings: Array = [] @@ -30,8 +46,8 @@ func _get_configuration_warnings() -> PackedStringArray: if children.size() == 0: warnings.append("Decorator node should have a child.") elif children.size() > 1: - warnings.append("Decorator node should have only one child.") + warnings.append("Decorator node has more than one child. Only the first child will be used, other sibilings will be ingored.") elif not children[0] is BTBehaviour: - warnings.append("Decorator node should have a BTBehaviour node as a child.") + warnings.append("Decorator nodes first child must be a BTBehaviour node.") return warnings diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd b/addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd index bf548db..e34035c 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd @@ -23,7 +23,4 @@ func _get_configuration_warnings() -> PackedStringArray: if not parent is BTBehaviour and not parent is BTRoot: warnings.append("BTLeaf node must be a child of BTBehaviour or BTRoot node.") - if children.size() > 0: - warnings.append("BTLeaf node must not have any children.") - return warnings diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd index d619e7b..986efcc 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd @@ -18,7 +18,7 @@ enum ProcessType { @export var autostart: bool = false ## Can be used to select if Behaviour Tree tick() is calculated on -## rendering (IDLE) frame or physics (PHYSICS) frame. +## rendering (IDLE) frame or physics (PHYSICS) frame. ## [br] ## More info: [method Node._process] and [method Node._physics_process] @export var process_type: ProcessType = ProcessType.PHYSICS: @@ -30,9 +30,30 @@ enum ProcessType { @export var blackboard: Blackboard -var active: bool = false +var active: bool = false: + set(value): + active = value + if value and entry_point != null: + _setup_processing() + else: + set_physics_process(false) + set_process(false) + var current_status: BTBehaviour.BTStatus -var entry_point: Node = null + + +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_child_order_changed: int = \ + child_order_changed.connect(_on_child_order_changed) + +@onready var entry_point: BTBehaviour = get_entry_point() + + +func _validate_property(property: Dictionary) -> void: + if property.name == "autostart" and get_parent() is FSMStateIntegratedBT: + autostart = false + property.usage = PROPERTY_USAGE_NO_EDITOR func _ready() -> void: @@ -46,14 +67,31 @@ func _ready() -> void: if blackboard == null: blackboard = _create_local_blackboard() - - if autostart: + + if entry_point == null: + return + elif autostart: active = true - if not process_type: - process_type = ProcessType.PHYSICS - _setup_processing() +## Swap this [BTRoot] nodes current entry point with the provided one. +## If root has no [BTBehaviour] as a child the provided one will be added. +## [br][br] +## Old behaviour nodes are freed and the new behaviour will be started on the +## next [code]tick()[/code] callback call. +func swap_entry_point(behaviour: BTBehaviour, + force_readable_name: bool = false, keep_groups: bool = false) -> void: + + if keep_groups == true and entry_point != null: + for g in entry_point.get_groups(): + if not behaviour.is_in_group(g): + behaviour.add_to_group(g, true) + + if entry_point == null: + add_child(behaviour, force_readable_name) + else: + entry_point.queue_free() + add_child(behaviour, force_readable_name) func _physics_process(delta: float) -> void: @@ -65,6 +103,8 @@ func _process(delta: float) -> void: func _process_code(delta: float) -> void: + # TODO Would be nice to remove it in future and make use of set_process() + # and set_physics_process() if not active: return @@ -78,21 +118,41 @@ func _create_local_blackboard() -> Blackboard: # Configures process type to use, if BTree is not active both are disabled. func _setup_processing() -> void: + if Engine.is_editor_hint(): + set_physics_process(false) + set_process(false) + return + set_physics_process(process_type == ProcessType.PHYSICS) set_process(process_type == ProcessType.IDLE) +func get_entry_point() -> BTBehaviour: + var first_child := get_child(0) + if first_child is BTBehaviour: + return first_child + else: + return null + + +func _on_child_order_changed() -> void: + if Engine.is_editor_hint(): + return + + entry_point = get_entry_point() + + func _get_configuration_warnings() -> PackedStringArray: var warnings: Array = [] var children = get_children() if children.size() == 0: - warnings.append("Behaviour Tree needs to have one Behaviour child.") + warnings.append("Behaviour Tree needs to have a Behaviour child to work.") elif children.size() == 1: if not children[0] is BTBehaviour: - warnings.append("The child of Behaviour Tree needs to be a Behaviour.") + warnings.append("The child of Behaviour Tree needs to be a BTBehaviour.") elif children.size() > 1: - warnings.append("Behaviour Tree can have only one Behaviour child.") + warnings.append("Behaviour Tree has more than one child. Only the first child will be used, other sibilings will be ingored.") return warnings diff --git a/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd b/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd index 37b8600..7a96ec7 100644 --- a/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd +++ b/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd @@ -12,9 +12,37 @@ class_name BTIntegratedFSM extends BTComposite ## [enum BTBehaviour.BTStatus.RUNNING]. If FSM return ## [enum BTBehaviour.BTStatus.SUCCESS] or [enum BTBehaviour.BTStatus.FAILURE] ## the child FSM is stopped. +## After that, the final status of the FSM will be returned +## as the final status of the [BTIntegratedFSM] node. +## [br][br] +## When [BTIntegratedFSM] finds node of type [FiniteStateMachine] as it's first +## child it starts the state machine and runs it on every +## [code]tick()[/code] until the FSM node itself will stop returning +## [enum BTBehaviour.BTStatus.RUNNING]. +## [br][br] +## In case where [BTComposite] can't find [FiniteStateMachine] as it's first +## child [enum BTBehaviour.BTStatus.FAILURE] will be returned. + + +## Default status in case [BTIntegratedFSM] will not find [FiniteStateMachine] +## child as a first node. +@export_enum("SUCCESS", "FAILURE") var default_status: String = "FAILURE": + set(value): + if value == "SUCCESS": + _default_status = BTStatus.SUCCESS + else: + _default_status = BTStatus.FAILURE + +var _default_status: BTStatus = BTStatus.FAILURE -var state_machine: FiniteStateMachine = null + +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_finite_state_machine_changed: int = \ + child_order_changed.connect(_finite_state_machine_changed) + +@onready var state_machine: FiniteStateMachine = _get_machine() func _ready(): @@ -24,6 +52,9 @@ func _ready(): func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus: + if state_machine == null: + return _default_status + if state_machine.active == false: state_machine.start() @@ -33,11 +64,42 @@ func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus: return state_machine.current_bt_status +## Swap this composite nodes current state machine with the provided one. +## If state has no [FiniteStateMachine] as a child the provided one will be added. +## [br][br] +## Old state machine is freed and the new machine will be started on the next +## [code]tick()[/code] callback call. +func swap_finite_state_machine(finite_state_machine: FiniteStateMachine, + force_readable_name: bool = false, keep_groups: bool = false) -> void: + + if keep_groups == true and state_machine != null: + for g in state_machine.get_groups(): + if not finite_state_machine.is_in_group(g): + finite_state_machine.add_to_group(g, true) + + if state_machine == null: + add_child(finite_state_machine, force_readable_name) + else: + state_machine.queue_free() + add_child(finite_state_machine, force_readable_name) + + func _get_machine() -> FiniteStateMachine: if get_child_count() == 0: return null else: - return get_child(0) + if get_child(0) is FiniteStateMachine: + return get_child(0) + + return null + + +func _finite_state_machine_changed() -> void: + if Engine.is_editor_hint(): + return + + state_machine = _get_machine() + state_machine.autostart = false func _get_configuration_warnings() -> PackedStringArray: @@ -45,13 +107,13 @@ func _get_configuration_warnings() -> PackedStringArray: var children = get_children() if children.size() == 0: - warnings.append("BTIntegratedFSM must have a child node. The first child will be used as the state machine.") + warnings.append("BTIntegratedFSM should have a child node to work. The first child will be used as the state machine.") if children.size() > 1: - warnings.append("BTIntegratedFSM can only have one child node. The first child will be used as the state machine.") + warnings.append("BTIntegratedFSM has more than one child node. Only the first child will be used as the state machine.") if children.size() == 1: if not children[0] is FiniteStateMachine: - warnings.append("BTIntegratedFSM's child node must be a FiniteStateMachine. The first child will be used as the state machine.") + warnings.append("BTIntegratedFSM's first child node must be a FiniteStateMachine. The first child will be used as the state machine.") return warnings diff --git a/addons/behaviour_toolkit/behaviour_tree/composites/bt_random.gd b/addons/behaviour_toolkit/behaviour_tree/composites/bt_random.gd index 20a0fdc..8b545ed 100644 --- a/addons/behaviour_toolkit/behaviour_tree/composites/bt_random.gd +++ b/addons/behaviour_toolkit/behaviour_tree/composites/bt_random.gd @@ -11,7 +11,12 @@ var rng = RandomNumberGenerator.new() var active_leave: BTBehaviour -func _ready(): +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_hash_seed: int = ready.connect(_hash_seed) + + +func _hash_seed(): if use_seed: rng.seed = hash(seed) diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm.gd b/addons/behaviour_toolkit/finite_state_machine/fsm.gd index f0a2e55..129b98f 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm.gd @@ -3,11 +3,23 @@ class_name FiniteStateMachine extends BehaviourToolkit ## An implementation of a simple finite state machine. ## -## The Finite State Machine is composed of states and transitions. +## The Finite State Machine is composed of states [FSMState] and transitions +## [FSMTransition] nodes. ## This is the class to handle all states and their transitions. ## On ready, all FSMTransition child nodes will be set up as transitions. ## To implement your logic you can override the [code]_on_enter, _on_update and ## _on_exit[/code] methods when extending the node's script. +## [br][br] +## The current active state in FSM is usually changed by [FSMTransition] node, +## but you can control state change from THE code using +## [method FiniteStateMachine.change_state]. +## [br][br] +## If you wish to add, remove, move [FSMState] nodes at run-time first add new +## [FSMStates] stop the FSM with [method FiniteStateMachine.exit_active_state_and_stop] +## and re-start it with method [method FiniteStateMachine.start] providing one +## of the new states either as start method property or change +## [member FiniteStateMachine.initial_state] before running [code]start()[/code]. +## After this procedure you can delete unused states. enum ProcessType { @@ -16,7 +28,7 @@ enum ProcessType { } -const ERROR_INITIAL_STATE_NULL: String = "The initial cannot be null when starting the State Machine." +const ERROR_START_STATE_NULL: String = "The started state is a null." ## The signal emitted when the state changes. @@ -35,8 +47,6 @@ signal state_changed(state: FSMState) process_type = value _setup_processing() -## Whether the FSM is active or not. -@export var active: bool = true ## The initial state of the FSM. @export var initial_state: FSMState: set(value): @@ -48,6 +58,9 @@ signal state_changed(state: FSMState) @export var blackboard: Blackboard +## Whether the FSM is active or not.[br] +## To activate use the [code]start()[/code] method. +var active: bool = false ## The list of states in the FSM. var states: Array[FSMState] ## The current active state. @@ -58,48 +71,43 @@ var current_events: Array[String] var current_bt_status: BTBehaviour.BTStatus +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_child_order_changed: int = \ + child_order_changed.connect(_update_states_list) + + +func _validate_property(property: Dictionary) -> void: + if property.name == "autostart" and get_parent() is BTIntegratedFSM: + autostart = false + property.usage = PROPERTY_USAGE_NO_EDITOR + elif property.name == "active": + property.usage = PROPERTY_USAGE_READ_ONLY + + func _ready() -> void: + set_physics_process(false) + set_process(false) + # Don't run in editor if Engine.is_editor_hint(): - set_physics_process(false) - set_process(false) return - - connect("state_changed", _on_state_changed) - + + _update_states_list() + if blackboard == null: blackboard = _create_local_blackboard() - + + if states.is_empty() or initial_state == null: + return + if autostart: - start() - else: - active = false - + start(initial_state) + if not process_type: process_type = ProcessType.PHYSICS - - _setup_processing() - - -func start() -> void: - current_bt_status = BTBehaviour.BTStatus.RUNNING - # Check if the initial state is valid - assert(initial_state != null, ERROR_INITIAL_STATE_NULL) - - # Get all the states - for state in get_children(): - if state is FSMState: - states.append(state) - - active = true - - # Set the initial state - active_state = initial_state - active_state._on_enter(actor, blackboard) - - # Emit the state changed signal - emit_signal("state_changed", active_state) + _setup_processing() func _physics_process(delta: float) -> void: @@ -111,23 +119,16 @@ func _process(delta: float) -> void: func _process_code(delta: float) -> void: - if not active: - return - - # Check if there are states - if states.size() == 0: - return - # The current event var event: String = "" - + # Check if there are events if current_events.size() > 0: # Get the first event event = current_events[0] # Remove the event from the list current_events.remove_at(0) - + # Check if the current state is valid for transition in active_state.transitions: if transition.is_valid(actor, blackboard) or transition.is_valid_event(event): @@ -136,33 +137,84 @@ func _process_code(delta: float) -> void: # Change the current state change_state(transition.get_next_state()) - + break # Process the current state active_state._on_update(delta, actor, blackboard) -## Changes the current state and calls the appropriate methods like _on_exit and _on_enter. +## Start FSM with given state, if no state is provided [code]initial_state[/code] +## property will be used. +func start(state: FSMState = initial_state) -> void: + assert(state != null, ERROR_START_STATE_NULL) + + current_bt_status = BTBehaviour.BTStatus.RUNNING + + active = true + + active_state = state + active_state._on_enter(actor, blackboard) + + # Enable processing + _setup_processing() + + # Emit the state changed signal + emit_signal("state_changed", active_state) + + + +## Changes the current state and calls the appropriate methods like +## [code]_on_exit()[/code] and [code]_on_enter()[/code] for respective states. func change_state(state: FSMState) -> void: # Exit the current state active_state._on_exit(actor, blackboard) - + # 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) + +## Exits currenlty active state effectively stopping FSM, makes +## [code]active_state[/code] property [code]null[/code]. +func exit_active_state_and_stop() -> void: + # Exit the current state + active_state._on_exit(actor, blackboard) + + active_state = null + active = false + + # Stop processing + set_physics_process(false) + set_process(false) + # Emit the state changed signal emit_signal("state_changed", active_state) -## Fires an event in the FSM. +## Fires an event in the FSM if the FSM is active. func fire_event(event: String) -> void: + if active == false: + return + current_events.append(event) +func _update_states_list() -> void: + if Engine.is_editor_hint(): + return + + states.clear() + for state in get_children(): + if state is FSMState: + states.append(state) + + func _create_local_blackboard() -> Blackboard: var blackboard: Blackboard = Blackboard.new() return blackboard @@ -170,8 +222,14 @@ func _create_local_blackboard() -> Blackboard: # Configures process type to use, if FSM is not active both are disabled. func _setup_processing() -> void: - set_physics_process(process_type == ProcessType.PHYSICS) - set_process(process_type == ProcessType.IDLE) + if Engine.is_editor_hint(): + set_physics_process(false) + set_process(false) + return + + if active: + set_physics_process(process_type == ProcessType.PHYSICS) + set_process(process_type == ProcessType.IDLE) func _on_state_changed(state: FSMState) -> void: diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd index cd70fe7..3669873 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd @@ -15,14 +15,12 @@ class_name FSMState extends BehaviourToolkit var transitions: Array[FSMTransition] = [] -func _ready() -> void: - # Don't run in editor - if Engine.is_editor_hint(): - return - - for transition in get_children(): - if transition is FSMTransition: - transitions.append(transition) +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_update_transitions: int = \ + child_order_changed.connect(_update_transitions) +@onready var __connect_update_transition_on_ready: int = \ + ready.connect(_update_transitions) ## Executes after the state is entered. @@ -40,11 +38,22 @@ func _on_exit(_actor: Node, _blackboard: Blackboard) -> void: pass -func _get_configuration_warnings() -> PackedStringArray: - var warnings: Array = [] +func _update_transitions() -> void: + # Don't run in editor + if Engine.is_editor_hint(): + return + + transitions.clear() + for transition in get_children(): + if transition is FSMTransition: + transitions.append(transition) + + +func _get_configuration_warnings(): + var warnings = [] var parent: Node = get_parent() if not parent is FiniteStateMachine: - warnings.append("FSMState should be a child of a FiniteStateMachine node.") + warnings.append("FSMState must be a child of a FiniteStateMachine node.") return warnings diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd b/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd index ff96a76..c69b646 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd @@ -13,9 +13,10 @@ class_name FSMStateIntegratedBT extends FSMState ## This allows you to trigger transitions based on the status of the ## Behaviour Tree. You can also use the [FSMEvent] leaf to trigger custom ## events inside the nested State Machine. - - -@onready var behaviour_tree: BTRoot = _get_behaviour_tree() +## [br][br] +## If its [BTRoot] is swaped for another [BTRoot] at run-time, the new child +## will be deactivated by default and, if not activated manually, will activate +## automatically next time this state is entered. @export var fire_event_on_status: bool = false: @@ -29,20 +30,56 @@ class_name FSMStateIntegratedBT extends FSMState update_configuration_warnings() +# Connecting signal using @onready to omit the need to use super() call +# in _ready() of extended nodes if they override _ready(). +@onready var __connect_child_order_changed: int = \ + child_order_changed.connect(_on_child_order_changed) + +@onready var behaviour_tree: BTRoot = _get_behaviour_tree() + + +## Swap this states current Behaviour Tree with the provided one. +## If state has no [BTRoot] as a child the provided one will be added. +## [br][br] +## Old BTree is freed and the new BTree will be active next time the state will +## be entered. +func swap_behaviour_tree(bt_root: BTRoot, + force_readable_name: bool = false, keep_groups: bool = false) -> void: + + if keep_groups == true and behaviour_tree != null: + for g in behaviour_tree.get_groups(): + if not bt_root.is_in_group(g): + bt_root.add_to_group(g, true) + + if behaviour_tree == null: + add_child(bt_root, force_readable_name) + else: + behaviour_tree.queue_free() + add_child(bt_root, force_readable_name) + + ## Executes after the state is entered. func _on_enter(_actor: Node, _blackboard: Blackboard) -> void: + if behaviour_tree == null: + return + behaviour_tree.active = true ## Executes every process call, if the state is active. func _on_update(_delta: float, _actor: Node, _blackboard: Blackboard) -> void: - if behaviour_tree.current_status == on_status: - if fire_event_on_status: - get_parent().fire_event(event) + if behaviour_tree == null: + return + + if behaviour_tree.current_status == on_status and fire_event_on_status: + get_parent().fire_event(event) ## Executes before the state is exited. func _on_exit(_actor: Node, _blackboard: Blackboard) -> void: + if behaviour_tree == null: + return + behaviour_tree.active = false @@ -61,7 +98,15 @@ func _get_behaviour_tree() -> BTRoot: return null -func _get_configuration_warnings() -> PackedStringArray: +func _on_child_order_changed() -> void: + if Engine.is_editor_hint(): + return + + behaviour_tree = _get_behaviour_tree() + behaviour_tree.autostart = false + + +func _get_configuration_warnings(): var warnings: Array = [] warnings.append_array(super._get_configuration_warnings()) @@ -76,11 +121,12 @@ func _get_configuration_warnings() -> PackedStringArray: warnings.append("FSMStateIntegratedBT can only have BTRoot and FSMTransition children.") if not has_root: - warnings.append("FSMStateIntegratedBT must have a BTRoot child node.") - + warnings.append("FSMStateIntegratedBT should have a BTRoot child node to work.") + if fire_event_on_status and event == "": warnings.append( "FSMStateIntegratedBT has fire_event_on_status enabled, but no event is set." ) return warnings + diff --git a/examples/behaviour_example/NPCs/villager.tscn b/examples/behaviour_example/NPCs/villager.tscn index df204fd..cc48f0b 100644 --- a/examples/behaviour_example/NPCs/villager.tscn +++ b/examples/behaviour_example/NPCs/villager.tscn @@ -154,7 +154,7 @@ navigation_agent = NodePath("NavigationAgent2D") animation_player = NodePath("AnimationPlayer") state_machine = NodePath("AnimationStateMachine") behaviour_tree = NodePath("Behaviour") -ghost_state_machine = NodePath("Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine") +ghost_state_machine = NodePath("Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine") [node name="PlayerTick" type="Timer" parent="."] autostart = true @@ -198,75 +198,75 @@ script = ExtResource("8_rjqba") [node name="BehaveLikeAGhost" type="Node" parent="Behaviour/Select/BeAGhost"] script = ExtResource("7_eoyf0") -[node name="FiniteStateMachine" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost" node_paths=PackedStringArray("initial_state", "actor")] +[node name="GhostStateMachine" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost" node_paths=PackedStringArray("initial_state", "actor")] script = ExtResource("8_5ft4q") initial_state = NodePath("TransformIntoGhost") actor = NodePath("../../../../..") blackboard = ExtResource("4_opmou") -[node name="TransformIntoGhost" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine"] +[node name="TransformIntoGhost" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine"] script = ExtResource("13_har5q") -[node name="onFinishedTransformation" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TransformIntoGhost" node_paths=PackedStringArray("next_state")] +[node name="onFinishedTransformation" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TransformIntoGhost" node_paths=PackedStringArray("next_state")] script = ExtResource("17_nswga") next_state = NodePath("../../TryToRevive") use_event = true event = "fully_transformed" -[node name="TryToRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine"] +[node name="TryToRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine"] script = ExtResource("13_rq5d2") fire_event_on_status = true event = "revive" -[node name="RevivalBehaviour" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive"] +[node name="RevivalBehaviour" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive"] script = ExtResource("3_5mjdm") -[node name="BTSelector" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive/RevivalBehaviour"] +[node name="BTSelector" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive/RevivalBehaviour"] script = ExtResource("5_1bdaa") -[node name="ReviveIfPossible" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive/RevivalBehaviour/BTSelector"] +[node name="ReviveIfPossible" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive/RevivalBehaviour/BTSelector"] script = ExtResource("5_ndext") -[node name="CanOnlyReviveOnce" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible"] +[node name="CanOnlyReviveOnce" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible"] script = ExtResource("9_ac00x") -[node name="CanRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible/CanOnlyReviveOnce"] +[node name="CanRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible/CanOnlyReviveOnce"] script = ExtResource("16_igrlt") -[node name="BTLeaf" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible/CanOnlyReviveOnce/CanRevive"] +[node name="BTLeaf" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible/CanOnlyReviveOnce/CanRevive"] script = ExtResource("9_fbuja") -[node name="Cooldown" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible"] +[node name="Cooldown" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive/RevivalBehaviour/BTSelector/ReviveIfPossible"] script = ExtResource("7_ff4kr") wait_for_ticks = 500 -[node name="FireEventFailedRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive/RevivalBehaviour/BTSelector" node_paths=PackedStringArray("state_machine")] +[node name="FireEventFailedRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive/RevivalBehaviour/BTSelector" node_paths=PackedStringArray("state_machine")] script = ExtResource("18_45wr7") event = &"failed_revive" state_machine = NodePath("../../../..") -[node name="onRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive" node_paths=PackedStringArray("next_state")] +[node name="onRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive" node_paths=PackedStringArray("next_state")] script = ExtResource("15_ahoqg") next_state = NodePath("../../Revive") use_event = true event = "revive" -[node name="onFailedRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/TryToRevive" node_paths=PackedStringArray("next_state")] +[node name="onFailedRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/TryToRevive" node_paths=PackedStringArray("next_state")] script = ExtResource("17_nswga") next_state = NodePath("../../DoGhostThings") use_event = true event = "failed_revive" -[node name="DoGhostThings" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine"] +[node name="DoGhostThings" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine"] script = ExtResource("14_k3bdi") -[node name="onRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine/DoGhostThings" node_paths=PackedStringArray("next_state")] +[node name="onRevive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine/DoGhostThings" node_paths=PackedStringArray("next_state")] script = ExtResource("15_ahoqg") next_state = NodePath("../../Revive") use_event = true event = "revive" -[node name="Revive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/FiniteStateMachine"] +[node name="Revive" type="Node" parent="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine"] script = ExtResource("13_bsy2u") return_value = 1 @@ -375,3 +375,4 @@ event = "start_refreshing" [connection signal="input_event" from="." to="." method="_on_input_event"] [connection signal="timeout" from="PlayerTick" to="." method="_on_player_tick_timeout"] +[connection signal="state_changed" from="Behaviour/Select/BeAGhost/BehaveLikeAGhost/GhostStateMachine" to="." method="_on_ghost_state_machine_state_changed"] diff --git a/examples/behaviour_example/actor/actor.gd b/examples/behaviour_example/actor/actor.gd index 676ee4d..4b1ed48 100644 --- a/examples/behaviour_example/actor/actor.gd +++ b/examples/behaviour_example/actor/actor.gd @@ -10,6 +10,7 @@ extends CharacterBody2D @export_category("Utility") @export var navigation_agent: NavigationAgent2D @export var animation_player: AnimationPlayer + @export_category("BehaviourToolkit") @export var state_machine: FiniteStateMachine @export var behaviour_tree: BTRoot From acf826b86b2368115606112e759f8fafcfd651c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Go=C5=82=C4=85b=20=28SirPigeonz=29?= Date: Wed, 13 Dec 2023 19:34:24 +0100 Subject: [PATCH 2/4] Removed line breaks in @onready connects Formatting will format it :) --- addons/behaviour_toolkit/behaviour_tree/bt_composite.gd | 3 +-- addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd | 3 +-- addons/behaviour_toolkit/behaviour_tree/bt_root.gd | 3 +-- .../behaviour_tree/composites/bt_integrated_fsm.gd | 3 +-- addons/behaviour_toolkit/finite_state_machine/fsm.gd | 3 +-- addons/behaviour_toolkit/finite_state_machine/fsm_state.gd | 6 ++---- .../finite_state_machine/fsm_state_integrated_bt.gd | 3 +-- 7 files changed, 8 insertions(+), 16 deletions(-) diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd b/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd index c79a90e..29da370 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd @@ -12,8 +12,7 @@ class_name BTComposite extends BTBehaviour # Connecting signal using @onready to omit the need to use super() call # in _ready() of extended nodes if they override _ready(). -@onready var __connect_child_order_changed: int = \ - child_order_changed.connect(_on_child_order_changed) +@onready var __connect_child_order_changed: int = child_order_changed.connect(_on_child_order_changed) ## The leaves under the composite node. diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd b/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd index 7c89e6e..6569053 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd @@ -13,8 +13,7 @@ class_name BTDecorator extends BTBehaviour # Connecting signal using @onready to omit the need to use super() call # in _ready() of extended nodes if they override _ready(). -@onready var __connect_child_order_changed: int = \ - child_order_changed.connect(_on_child_order_changed) +@onready var __connect_child_order_changed: int = child_order_changed.connect(_on_child_order_changed) ## The leaf the decorator is decorating. @onready var leaf: BTBehaviour = _get_leaf() diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd index 986efcc..19ad216 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd @@ -44,8 +44,7 @@ var current_status: BTBehaviour.BTStatus # Connecting signal using @onready to omit the need to use super() call # in _ready() of extended nodes if they override _ready(). -@onready var __connect_child_order_changed: int = \ - child_order_changed.connect(_on_child_order_changed) +@onready var __connect_child_order_changed: int = child_order_changed.connect(_on_child_order_changed) @onready var entry_point: BTBehaviour = get_entry_point() diff --git a/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd b/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd index 7a96ec7..3a3a1f2 100644 --- a/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd +++ b/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd @@ -39,8 +39,7 @@ var _default_status: BTStatus = BTStatus.FAILURE # Connecting signal using @onready to omit the need to use super() call # in _ready() of extended nodes if they override _ready(). -@onready var __connect_finite_state_machine_changed: int = \ - child_order_changed.connect(_finite_state_machine_changed) +@onready var __connect_finite_state_machine_changed: int = child_order_changed.connect(_finite_state_machine_changed) @onready var state_machine: FiniteStateMachine = _get_machine() diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm.gd b/addons/behaviour_toolkit/finite_state_machine/fsm.gd index 129b98f..c6cbab6 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm.gd @@ -73,8 +73,7 @@ var current_bt_status: BTBehaviour.BTStatus # Connecting signal using @onready to omit the need to use super() call # in _ready() of extended nodes if they override _ready(). -@onready var __connect_child_order_changed: int = \ - child_order_changed.connect(_update_states_list) +@onready var __connect_child_order_changed: int = child_order_changed.connect(_update_states_list) func _validate_property(property: Dictionary) -> void: diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd index 3669873..136c319 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd @@ -17,10 +17,8 @@ var transitions: Array[FSMTransition] = [] # Connecting signal using @onready to omit the need to use super() call # in _ready() of extended nodes if they override _ready(). -@onready var __connect_update_transitions: int = \ - child_order_changed.connect(_update_transitions) -@onready var __connect_update_transition_on_ready: int = \ - ready.connect(_update_transitions) +@onready var __connect_update_transitions: int = child_order_changed.connect(_update_transitions) +@onready var __connect_update_transition_on_ready: int = ready.connect(_update_transitions) ## Executes after the state is entered. diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd b/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd index c69b646..245533a 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_state_integrated_bt.gd @@ -32,8 +32,7 @@ class_name FSMStateIntegratedBT extends FSMState # Connecting signal using @onready to omit the need to use super() call # in _ready() of extended nodes if they override _ready(). -@onready var __connect_child_order_changed: int = \ - child_order_changed.connect(_on_child_order_changed) +@onready var __connect_child_order_changed: int = child_order_changed.connect(_on_child_order_changed) @onready var behaviour_tree: BTRoot = _get_behaviour_tree() From 80a610210be821145aee90c3276dcdfda5722a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Go=C5=82=C4=85b=20=28SirPigeonz=29?= Date: Sun, 7 Jan 2024 22:24:46 +0100 Subject: [PATCH 3/4] Small fix of conf warnings --- addons/behaviour_toolkit/finite_state_machine/fsm_state.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd index 136c319..af2f7a7 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd @@ -47,7 +47,7 @@ func _update_transitions() -> void: transitions.append(transition) -func _get_configuration_warnings(): +func _get_configuration_warnings() -> PackedStringArray: var warnings = [] var parent: Node = get_parent() From 9be4bff0d5c82e5d214cbc783cd87fb63bdf3035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Go=C5=82=C4=85b=20=28SirPigeonz=29?= Date: Sun, 7 Jan 2024 22:29:52 +0100 Subject: [PATCH 4/4] Forgot to commit this small bugfix --- addons/behaviour_toolkit/behaviour_tree/bt_root.gd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd index 19ad216..f1bf2a7 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd @@ -127,6 +127,9 @@ func _setup_processing() -> void: func get_entry_point() -> BTBehaviour: + if not get_child_count(): + return null + var first_child := get_child(0) if first_child is BTBehaviour: return first_child