Skip to content

Commit 7adf554

Browse files
committed
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 :)
1 parent 8ee66a0 commit 7adf554

File tree

13 files changed

+401
-117
lines changed

13 files changed

+401
-117
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
@icon("res://addons/behaviour_toolkit/icons/Gear.svg")
22
class_name BehaviourToolkit extends Node
3-
## The main node of the BehaviourToolkit plugin.
3+
## The main node of the Behaviour Toolkit plugin.
4+
##
5+
## All nodes of Behaviour Toolkit pluign extend it.

addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
@icon("res://addons/behaviour_toolkit/icons/BTBehaviour.svg")
22
class_name BTBehaviour extends BehaviourToolkit
3+
## Base class for Behaviours used in the behaviour tree.
4+
##
5+
## TODO
36

47

8+
## Status enum returned by nodes executing behaviours.
59
enum BTStatus {
610
SUCCESS,
711
FAILURE,

addons/behaviour_toolkit/behaviour_tree/bt_composite.gd

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
@tool
22
@icon("res://addons/behaviour_toolkit/icons/BTComposite.svg")
33
class_name BTComposite extends BTBehaviour
4+
## Basic Composite node for Behaviour Tree.
5+
##
6+
## By itself is not doing much but is aware of it's children. You can use it
7+
## to implement custom composite behaviours.
8+
9+
10+
# Connecting signal using @onready to omit the need to use super() call
11+
# in _ready() of extended nodes if they override _ready().
12+
@onready var __connect_child_order_changed: int = \
13+
child_order_changed.connect(_on_child_order_changed)
414

515

616
## The leaves under the composite node.
717
@onready var leaves: Array = get_children()
818

919

20+
func _on_child_order_changed() -> void:
21+
if Engine.is_editor_hint():
22+
return
23+
24+
leaves = get_children()
25+
26+
1027
func _get_configuration_warnings() -> PackedStringArray:
1128
var warnings: Array = []
1229

@@ -17,7 +34,7 @@ func _get_configuration_warnings() -> PackedStringArray:
1734
warnings.append("BTComposite node must be a child of BTComposite or BTRoot node.")
1835

1936
if children.size() == 0:
20-
warnings.append("BTComposite node must have at least one child.")
37+
warnings.append("BTComposite node should have at least one child to work.")
2138

2239
if children.size() == 1:
2340
warnings.append("BTComposite node should have more than one child.")

addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
@tool
22
@icon("res://addons/behaviour_toolkit/icons/BTDecorator.svg")
33
class_name BTDecorator extends BTBehaviour
4+
## Basic Decorator node for Behaviour Tree.
5+
##
6+
## By itself is not doing much but is aware of it's children and holds reference
7+
## to its first child (index 0 child). You can use it to implement custom
8+
## decorators.
49

510

11+
# Connecting signal using @onready to omit the need to use super() call
12+
# in _ready() of extended nodes if they override _ready().
13+
@onready var __connect_child_order_changed: int = \
14+
child_order_changed.connect(_on_child_order_changed)
15+
616
## The leaf the decorator is decorating.
7-
@onready var leaf: BTBehaviour = _get_leaf()
17+
@onready var leaf: BTBehaviour = _get_leaf()
818

919

1020
func _get_leaf() -> BTBehaviour:
@@ -14,6 +24,13 @@ func _get_leaf() -> BTBehaviour:
1424
return get_child(0)
1525

1626

27+
func _on_child_order_changed() -> void:
28+
if Engine.is_editor_hint():
29+
return
30+
31+
leaf = _get_leaf()
32+
33+
1734
func _get_configuration_warnings() -> PackedStringArray:
1835
var warnings: Array = []
1936

@@ -26,8 +43,8 @@ func _get_configuration_warnings() -> PackedStringArray:
2643
if children.size() == 0:
2744
warnings.append("Decorator node should have a child.")
2845
elif children.size() > 1:
29-
warnings.append("Decorator node should have only one child.")
46+
warnings.append("Decorator node has more than one child. Only the first child will be used, other sibilings will be ingored.")
3047
elif not children[0] is BTBehaviour:
31-
warnings.append("Decorator node should have a BTBehaviour node as a child.")
48+
warnings.append("Decorator nodes first child must be a BTBehaviour node.")
3249

3350
return warnings

addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@tool
22
@icon("res://addons/behaviour_toolkit/icons/BTLeaf.svg")
33
class_name BTLeaf extends BTBehaviour
4+
## Basic Leaf node used as a base to model and write custom behaviours for
5+
## [BTRoot]
46

57

68
func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus:
@@ -15,8 +17,5 @@ func _get_configuration_warnings() -> PackedStringArray:
1517

1618
if not parent is BTBehaviour and not parent is BTRoot:
1719
warnings.append("BTLeaf node must be a child of BTBehaviour or BTRoot node.")
18-
19-
if children.size() > 0:
20-
warnings.append("BTLeaf node must not have any children.")
2120

2221
return warnings

addons/behaviour_toolkit/behaviour_tree/bt_root.gd

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
@icon("res://addons/behaviour_toolkit/icons/BTRoot.svg")
33
class_name BTRoot extends BehaviourToolkit
44
## Node used as a base parent (root) of a Behaviour Tree
5+
##
6+
## TODO
57

68

79
enum ProcessType {
@@ -13,7 +15,7 @@ enum ProcessType {
1315
@export var autostart: bool = false
1416

1517
## Can be used to select if Behaviour Tree tick() is calculated on
16-
## rendering (IDLE) frame or physics (PHYSICS) frame.
18+
## rendering (IDLE) frame or physics (PHYSICS) frame.
1719
## [br]
1820
## More info: [method Node._process] and [method Node._physics_process]
1921
@export var process_type: ProcessType = ProcessType.PHYSICS:
@@ -25,11 +27,30 @@ enum ProcessType {
2527
@export var blackboard: Blackboard
2628

2729

28-
var active: bool = false
30+
var active: bool = false:
31+
set(value):
32+
active = value
33+
if value and entry_point != null:
34+
_setup_processing()
35+
else:
36+
set_physics_process(false)
37+
set_process(false)
38+
2939
var current_status: BTBehaviour.BTStatus
3040

3141

32-
@onready var entry_point = get_child(0)
42+
# Connecting signal using @onready to omit the need to use super() call
43+
# in _ready() of extended nodes if they override _ready().
44+
@onready var __connect_child_order_changed: int = \
45+
child_order_changed.connect(_on_child_order_changed)
46+
47+
@onready var entry_point: BTBehaviour = get_entry_point()
48+
49+
50+
func _validate_property(property: Dictionary) -> void:
51+
if property.name == "autostart" and get_parent() is FSMStateIntegratedBT:
52+
autostart = false
53+
property.usage = PROPERTY_USAGE_NO_EDITOR
3354

3455

3556
func _ready() -> void:
@@ -41,14 +62,31 @@ func _ready() -> void:
4162

4263
if blackboard == null:
4364
blackboard = _create_local_blackboard()
44-
45-
if autostart:
65+
66+
if entry_point == null:
67+
return
68+
elif autostart:
4669
active = true
4770

48-
if not process_type:
49-
process_type = ProcessType.PHYSICS
5071

51-
_setup_processing()
72+
## Swap this [BTRoot] nodes current entry point with the provided one.
73+
## If root has no [BTBehaviour] as a child the provided one will be added.
74+
## [br][br]
75+
## Old behaviour nodes are freed and the new behaviour will be started on the
76+
## next [code]tick()[/code] callback call.
77+
func swap_entry_point(behaviour: BTBehaviour,
78+
force_readable_name: bool = false, keep_groups: bool = false) -> void:
79+
80+
if keep_groups == true and entry_point != null:
81+
for g in entry_point.get_groups():
82+
if not behaviour.is_in_group(g):
83+
behaviour.add_to_group(g, true)
84+
85+
if entry_point == null:
86+
add_child(behaviour, force_readable_name)
87+
else:
88+
entry_point.queue_free()
89+
add_child(behaviour, force_readable_name)
5290

5391

5492
func _physics_process(delta: float) -> void:
@@ -60,6 +98,8 @@ func _process(delta: float) -> void:
6098

6199

62100
func _process_code(delta: float) -> void:
101+
# TODO Would be nice to remove it in future and make use of set_process()
102+
# and set_physics_process()
63103
if not active:
64104
return
65105

@@ -73,21 +113,41 @@ func _create_local_blackboard() -> Blackboard:
73113

74114
# Configures process type to use, if BTree is not active both are disabled.
75115
func _setup_processing() -> void:
116+
if Engine.is_editor_hint():
117+
set_physics_process(false)
118+
set_process(false)
119+
return
120+
76121
set_physics_process(process_type == ProcessType.PHYSICS)
77122
set_process(process_type == ProcessType.IDLE)
78123

79124

125+
func get_entry_point() -> BTBehaviour:
126+
var first_child := get_child(0)
127+
if first_child is BTBehaviour:
128+
return first_child
129+
else:
130+
return null
131+
132+
133+
func _on_child_order_changed() -> void:
134+
if Engine.is_editor_hint():
135+
return
136+
137+
entry_point = get_entry_point()
138+
139+
80140
func _get_configuration_warnings() -> PackedStringArray:
81141
var warnings: Array = []
82142

83143
var children = get_children()
84144

85145
if children.size() == 0:
86-
warnings.append("Behaviour Tree needs to have one Behaviour child.")
146+
warnings.append("Behaviour Tree needs to have a Behaviour child to work.")
87147
elif children.size() == 1:
88148
if not children[0] is BTBehaviour:
89-
warnings.append("The child of Behaviour Tree needs to be a Behaviour.")
149+
warnings.append("The child of Behaviour Tree needs to be a BTBehaviour.")
90150
elif children.size() > 1:
91-
warnings.append("Behaviour Tree can have only one Behaviour child.")
151+
warnings.append("Behaviour Tree has more than one child. Only the first child will be used, other sibilings will be ingored.")
92152

93153
return warnings
Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,50 @@
11
@tool
22
@icon("res://addons/behaviour_toolkit/icons/BTCompositeIntegration.svg")
33
class_name BTIntegratedFSM extends BTComposite
4+
## [BTComposite] node of Behaviour Tree that can handle [FiniteStateMachine]
5+
##
6+
## When [BTIntegratedFSM] finds node of type [FiniteStateMachine] as it's first
7+
## child it starts the state machine and runs it on every
8+
## [code]tick()[/code] until the FSM node itself will stop returning
9+
## [enum BTBehaviour.BTStatus.RUNNING].
10+
## [br][br]
11+
## After FSM returns either of [enum BTBehaviour.BTStatus.SUCCESS] or
12+
## [enum BTBehaviour.BTStatus.FAILURE] then the final status of the FSM will be returned
13+
## as the final status of the [BTIntegratedFSM] node.
14+
## [br][br]
15+
## In case where [BTComposite] can't find [FiniteStateMachine] as it's first
16+
## child [enum BTBehaviour.BTStatus.FAILURE] will be returned.
417

518

6-
var state_machine: FiniteStateMachine = null
19+
## Default status in case [BTIntegratedFSM] will not find [FiniteStateMachine]
20+
## child as a first node.
21+
@export_enum("SUCCESS", "FAILURE") var default_status: String = "FAILURE":
22+
set(value):
23+
if value == "SUCCESS":
24+
_default_status = BTStatus.SUCCESS
25+
else:
26+
_default_status = BTStatus.FAILURE
27+
28+
29+
var _default_status: BTStatus = BTStatus.FAILURE
30+
31+
32+
# Connecting signal using @onready to omit the need to use super() call
33+
# in _ready() of extended nodes if they override _ready().
34+
@onready var __connect_finite_state_machine_changed: int = \
35+
child_order_changed.connect(_finite_state_machine_changed)
36+
37+
@onready var state_machine: FiniteStateMachine = _get_machine()
738

839
func _ready():
940
if not Engine.is_editor_hint():
1041
state_machine = _get_machine()
1142

1243

1344
func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus:
45+
if state_machine == null:
46+
return _default_status
47+
1448
if state_machine.active == false:
1549
state_machine.start()
1650

@@ -20,25 +54,56 @@ func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus:
2054
return state_machine.current_bt_status
2155

2256

57+
## Swap this composite nodes current state machine with the provided one.
58+
## If state has no [FiniteStateMachine] as a child the provided one will be added.
59+
## [br][br]
60+
## Old state machine is freed and the new machine will be started on the next
61+
## [code]tick()[/code] callback call.
62+
func swap_finite_state_machine(finite_state_machine: FiniteStateMachine,
63+
force_readable_name: bool = false, keep_groups: bool = false) -> void:
64+
65+
if keep_groups == true and state_machine != null:
66+
for g in state_machine.get_groups():
67+
if not finite_state_machine.is_in_group(g):
68+
finite_state_machine.add_to_group(g, true)
69+
70+
if state_machine == null:
71+
add_child(finite_state_machine, force_readable_name)
72+
else:
73+
state_machine.queue_free()
74+
add_child(finite_state_machine, force_readable_name)
75+
76+
2377
func _get_machine() -> FiniteStateMachine:
2478
if get_child_count() == 0:
2579
return null
2680
else:
27-
return get_child(0)
81+
if get_child(0) is FiniteStateMachine:
82+
return get_child(0)
83+
84+
return null
85+
86+
87+
func _finite_state_machine_changed() -> void:
88+
if Engine.is_editor_hint():
89+
return
90+
91+
state_machine = _get_machine()
92+
state_machine.autostart = false
2893

2994

3095
func _get_configuration_warnings():
3196
var warnings: Array = []
3297
var children = get_children()
3398

3499
if children.size() == 0:
35-
warnings.append("BTIntegratedFSM must have a child node. The first child will be used as the state machine.")
100+
warnings.append("BTIntegratedFSM should have a child node to work. The first child will be used as the state machine.")
36101

37102
if children.size() > 1:
38-
warnings.append("BTIntegratedFSM can only have one child node. The first child will be used as the state machine.")
103+
warnings.append("BTIntegratedFSM has more than one child node. Only the first child will be used as the state machine.")
39104

40105
if children.size() == 1:
41106
if not children[0] is FiniteStateMachine:
42-
warnings.append("BTIntegratedFSM's child node must be a FiniteStateMachine. The first child will be used as the state machine.")
107+
warnings.append("BTIntegratedFSM's first child node must be a FiniteStateMachine. The first child will be used as the state machine.")
43108

44109
return warnings

addons/behaviour_toolkit/behaviour_tree/composites/bt_random.gd

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ var rng = RandomNumberGenerator.new()
1111
var active_leave: BTBehaviour
1212

1313

14-
func _ready():
14+
# Connecting signal using @onready to omit the need to use super() call
15+
# in _ready() of extended nodes if they override _ready().
16+
@onready var __connect_hash_seed: int = ready.connect(_hash_seed)
17+
18+
19+
func _hash_seed():
1520
if use_seed:
1621
rng.seed = hash(seed)
1722

0 commit comments

Comments
 (0)