From 7adf554c1415141eac0f13d6a7abe4f807b0e7ca 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/2] 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 :) --- addons/behaviour_toolkit/behaviour_toolkit.gd | 4 +- .../behaviour_tree/bt_behaviour.gd | 4 + .../behaviour_tree/bt_composite.gd | 19 +- .../behaviour_tree/bt_decorator.gd | 23 ++- .../behaviour_tree/bt_leaf.gd | 5 +- .../behaviour_tree/bt_root.gd | 82 +++++++-- .../composites/bt_integrated_fsm.gd | 75 +++++++- .../behaviour_tree/composites/bt_random.gd | 7 +- .../finite_state_machine/fsm.gd | 162 ++++++++++++------ .../finite_state_machine/fsm_state.gd | 27 ++- .../fsm_state_integrated_bt.gd | 65 ++++++- examples/behaviour_example/NPCs/villager.tscn | 44 +++-- examples/behaviour_example/actor/actor.gd | 1 + 13 files changed, 401 insertions(+), 117 deletions(-) diff --git a/addons/behaviour_toolkit/behaviour_toolkit.gd b/addons/behaviour_toolkit/behaviour_toolkit.gd index f56b7de..993a3f2 100644 --- a/addons/behaviour_toolkit/behaviour_toolkit.gd +++ b/addons/behaviour_toolkit/behaviour_toolkit.gd @@ -1,3 +1,5 @@ @icon("res://addons/behaviour_toolkit/icons/Gear.svg") class_name BehaviourToolkit extends Node -## The main node of the BehaviourToolkit plugin. +## The main node of the Behaviour Toolkit plugin. +## +## All nodes of Behaviour Toolkit pluign extend it. diff --git a/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd b/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd index 585f923..a84b9be 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd @@ -1,7 +1,11 @@ @icon("res://addons/behaviour_toolkit/icons/BTBehaviour.svg") class_name BTBehaviour extends BehaviourToolkit +## Base class for Behaviours used in the behaviour tree. +## +## TODO +## 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 e598abf..fa9d4e7 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd @@ -1,12 +1,29 @@ @tool @icon("res://addons/behaviour_toolkit/icons/BTComposite.svg") class_name BTComposite extends BTBehaviour +## Basic Composite node for Behaviour Tree. +## +## 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 = [] @@ -17,7 +34,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 7e8f109..08ac111 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd @@ -1,10 +1,20 @@ @tool @icon("res://addons/behaviour_toolkit/icons/BTDecorator.svg") class_name BTDecorator extends BTBehaviour +## Basic Decorator node for Behaviour Tree. +## +## 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: @@ -14,6 +24,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 = [] @@ -26,8 +43,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 e2414e5..d7a181c 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd @@ -1,6 +1,8 @@ @tool @icon("res://addons/behaviour_toolkit/icons/BTLeaf.svg") class_name BTLeaf extends BTBehaviour +## Basic Leaf node used as a base to model and write custom behaviours for +## [BTRoot] func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus: @@ -15,8 +17,5 @@ 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 8d13551..8967ba0 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd @@ -2,6 +2,8 @@ @icon("res://addons/behaviour_toolkit/icons/BTRoot.svg") class_name BTRoot extends BehaviourToolkit ## Node used as a base parent (root) of a Behaviour Tree +## +## TODO enum ProcessType { @@ -13,7 +15,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: @@ -25,11 +27,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 -@onready var entry_point = get_child(0) +# 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: @@ -41,14 +62,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: @@ -60,6 +98,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 @@ -73,21 +113,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 99a5004..25b7f4f 100644 --- a/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd +++ b/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd @@ -1,9 +1,40 @@ @tool @icon("res://addons/behaviour_toolkit/icons/BTCompositeIntegration.svg") class_name BTIntegratedFSM extends BTComposite +## [BTComposite] node of Behaviour Tree that can handle [FiniteStateMachine] +## +## 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] +## After FSM returns either of [enum BTBehaviour.BTStatus.SUCCESS] or +## [enum BTBehaviour.BTStatus.FAILURE] then the final status of the FSM will be returned +## as the final status of the [BTIntegratedFSM] node. +## [br][br] +## In case where [BTComposite] can't find [FiniteStateMachine] as it's first +## child [enum BTBehaviour.BTStatus.FAILURE] will be returned. -var state_machine: FiniteStateMachine = null +## 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 + + +# 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(): if not Engine.is_editor_hint(): @@ -11,6 +42,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() @@ -20,11 +54,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(): @@ -32,13 +97,13 @@ func _get_configuration_warnings(): 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 ed254ef..2bdf321 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm.gd @@ -3,7 +3,19 @@ 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. +## [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 { @@ -12,7 +24,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. @@ -31,8 +43,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): @@ -44,6 +54,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. @@ -54,48 +67,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: @@ -107,23 +115,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): @@ -132,33 +133,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 @@ -166,8 +218,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 c95eb95..1a7862a 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd @@ -8,14 +8,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. @@ -33,11 +31,22 @@ func _on_exit(_actor: Node, _blackboard: Blackboard) -> void: pass +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 c2add34..dd3a588 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 @@ -1,9 +1,14 @@ @tool @icon("res://addons/behaviour_toolkit/icons/FSMStateIntegration.svg") class_name FSMStateIntegratedBT extends FSMState - - -@onready var behaviour_tree: BTRoot = _get_behaviour_tree() +## [FSMState] that can handle and activate [BTRoot] as it's child. +## +## When this [FSMStateIntegratedBT] state is entered it activates it's first +## child of type [BTRoot]. +## [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: @@ -17,20 +22,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 @@ -48,6 +89,15 @@ func _get_behaviour_tree() -> BTRoot: return null + +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 = [] @@ -63,9 +113,10 @@ func _get_configuration_warnings(): 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 4e588f1..813735f 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,79 +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") -autostart = null -process_type = null -active = null 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 -on_status = null 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" +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 @@ -329,7 +325,6 @@ wait_for_ticks = 150 script = ExtResource("8_5ft4q") autostart = true process_type = 0 -active = null initial_state = NodePath("Idle") actor = NodePath("..") blackboard = ExtResource("4_opmou") @@ -381,3 +376,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 c7396b3..ad251ae 100644 --- a/examples/behaviour_example/actor/actor.gd +++ b/examples/behaviour_example/actor/actor.gd @@ -10,6 +10,7 @@ class_name Actor 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 88c631d5874804649129d829a8038f485ac0df21 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/2] 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 fa9d4e7..e502281 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_composite.gd @@ -9,8 +9,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 08ac111..3dfd6e9 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd @@ -10,8 +10,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 8967ba0..dec4e4f 100644 --- a/addons/behaviour_toolkit/behaviour_tree/bt_root.gd +++ b/addons/behaviour_toolkit/behaviour_tree/bt_root.gd @@ -41,8 +41,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 25b7f4f..94c4530 100644 --- a/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd +++ b/addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd @@ -31,8 +31,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 2bdf321..7f74771 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm.gd @@ -69,8 +69,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 1a7862a..9b67e4c 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_state.gd @@ -10,10 +10,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 dd3a588..abc8301 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 @@ -24,8 +24,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()