Skip to content

Commit 2eeffbd

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 5e381cf commit 2eeffbd

File tree

12 files changed

+386
-114
lines changed

12 files changed

+386
-114
lines changed

addons/behaviour_toolkit/behaviour_tree/bt_behaviour.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class_name BTBehaviour extends BehaviourToolkit
77
## which control the flow of the behaviours in Behaviour Tree system.
88

99

10+
## Status enum returned by nodes executing behaviours.
1011
enum BTStatus {
1112
SUCCESS,
1213
FAILURE,

addons/behaviour_toolkit/behaviour_tree/bt_composite.gd

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@ class_name BTComposite extends BTBehaviour
55
##
66
## Composites can hold multiple behaviour nodes and evalute/execute them
77
## based on custom logic based on their return values.
8+
## [br][br]
9+
## By itself is not doing much but is aware of it's children. You can use it
10+
## to implement custom composite behaviours.
11+
12+
13+
# Connecting signal using @onready to omit the need to use super() call
14+
# in _ready() of extended nodes if they override _ready().
15+
@onready var __connect_child_order_changed: int = \
16+
child_order_changed.connect(_on_child_order_changed)
817

918

1019
## The leaves under the composite node.
1120
@onready var leaves: Array = get_children()
1221

1322

23+
func _on_child_order_changed() -> void:
24+
if Engine.is_editor_hint():
25+
return
26+
27+
leaves = get_children()
28+
29+
1430
func _get_configuration_warnings() -> PackedStringArray:
1531
var warnings: Array = []
1632

@@ -21,7 +37,7 @@ func _get_configuration_warnings() -> PackedStringArray:
2137
warnings.append("BTComposite node must be a child of BTComposite or BTRoot node.")
2238

2339
if children.size() == 0:
24-
warnings.append("BTComposite node must have at least one child.")
40+
warnings.append("BTComposite node should have at least one child to work.")
2541

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

addons/behaviour_toolkit/behaviour_tree/bt_decorator.gd

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ class_name BTDecorator extends BTBehaviour
55
##
66
## Decorators are used to augment the behaviour of a leaf.[br]
77
## Think of it as another layer of logic that is executed before the leaf.
8+
## [br][br]
9+
## By itself is not doing much but is aware of it's children and holds reference
10+
## to its first child (index 0 child). You can use it to implement custom
11+
## decorators.
812

913

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_child_order_changed: int = \
17+
child_order_changed.connect(_on_child_order_changed)
18+
1019
## The leaf the decorator is decorating.
11-
@onready var leaf: BTBehaviour = _get_leaf()
20+
@onready var leaf: BTBehaviour = _get_leaf()
1221

1322

1423
func _get_leaf() -> BTBehaviour:
@@ -18,6 +27,13 @@ func _get_leaf() -> BTBehaviour:
1827
return get_child(0)
1928

2029

30+
func _on_child_order_changed() -> void:
31+
if Engine.is_editor_hint():
32+
return
33+
34+
leaf = _get_leaf()
35+
36+
2137
func _get_configuration_warnings() -> PackedStringArray:
2238
var warnings: Array = []
2339

@@ -30,8 +46,8 @@ func _get_configuration_warnings() -> PackedStringArray:
3046
if children.size() == 0:
3147
warnings.append("Decorator node should have a child.")
3248
elif children.size() > 1:
33-
warnings.append("Decorator node should have only one child.")
49+
warnings.append("Decorator node has more than one child. Only the first child will be used, other sibilings will be ingored.")
3450
elif not children[0] is BTBehaviour:
35-
warnings.append("Decorator node should have a BTBehaviour node as a child.")
51+
warnings.append("Decorator nodes first child must be a BTBehaviour node.")
3652

3753
return warnings

addons/behaviour_toolkit/behaviour_tree/bt_leaf.gd

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,4 @@ func _get_configuration_warnings() -> PackedStringArray:
2323
if not parent is BTBehaviour and not parent is BTRoot:
2424
warnings.append("BTLeaf node must be a child of BTBehaviour or BTRoot node.")
2525

26-
if children.size() > 0:
27-
warnings.append("BTLeaf node must not have any children.")
28-
2926
return warnings

addons/behaviour_toolkit/behaviour_tree/bt_root.gd

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ enum ProcessType {
1818
@export var autostart: bool = false
1919

2020
## Can be used to select if Behaviour Tree tick() is calculated on
21-
## rendering (IDLE) frame or physics (PHYSICS) frame.
21+
## rendering (IDLE) frame or physics (PHYSICS) frame.
2222
## [br]
2323
## More info: [method Node._process] and [method Node._physics_process]
2424
@export var process_type: ProcessType = ProcessType.PHYSICS:
@@ -30,9 +30,30 @@ enum ProcessType {
3030
@export var blackboard: Blackboard
3131

3232

33-
var active: bool = false
33+
var active: bool = false:
34+
set(value):
35+
active = value
36+
if value and entry_point != null:
37+
_setup_processing()
38+
else:
39+
set_physics_process(false)
40+
set_process(false)
41+
3442
var current_status: BTBehaviour.BTStatus
35-
var entry_point: Node = null
43+
44+
45+
# Connecting signal using @onready to omit the need to use super() call
46+
# in _ready() of extended nodes if they override _ready().
47+
@onready var __connect_child_order_changed: int = \
48+
child_order_changed.connect(_on_child_order_changed)
49+
50+
@onready var entry_point: BTBehaviour = get_entry_point()
51+
52+
53+
func _validate_property(property: Dictionary) -> void:
54+
if property.name == "autostart" and get_parent() is FSMStateIntegratedBT:
55+
autostart = false
56+
property.usage = PROPERTY_USAGE_NO_EDITOR
3657

3758

3859
func _ready() -> void:
@@ -46,14 +67,31 @@ func _ready() -> void:
4667

4768
if blackboard == null:
4869
blackboard = _create_local_blackboard()
49-
50-
if autostart:
70+
71+
if entry_point == null:
72+
return
73+
elif autostart:
5174
active = true
5275

53-
if not process_type:
54-
process_type = ProcessType.PHYSICS
5576

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

5896

5997
func _physics_process(delta: float) -> void:
@@ -65,6 +103,8 @@ func _process(delta: float) -> void:
65103

66104

67105
func _process_code(delta: float) -> void:
106+
# TODO Would be nice to remove it in future and make use of set_process()
107+
# and set_physics_process()
68108
if not active:
69109
return
70110

@@ -78,21 +118,41 @@ func _create_local_blackboard() -> Blackboard:
78118

79119
# Configures process type to use, if BTree is not active both are disabled.
80120
func _setup_processing() -> void:
121+
if Engine.is_editor_hint():
122+
set_physics_process(false)
123+
set_process(false)
124+
return
125+
81126
set_physics_process(process_type == ProcessType.PHYSICS)
82127
set_process(process_type == ProcessType.IDLE)
83128

84129

130+
func get_entry_point() -> BTBehaviour:
131+
var first_child := get_child(0)
132+
if first_child is BTBehaviour:
133+
return first_child
134+
else:
135+
return null
136+
137+
138+
func _on_child_order_changed() -> void:
139+
if Engine.is_editor_hint():
140+
return
141+
142+
entry_point = get_entry_point()
143+
144+
85145
func _get_configuration_warnings() -> PackedStringArray:
86146
var warnings: Array = []
87147

88148
var children = get_children()
89149

90150
if children.size() == 0:
91-
warnings.append("Behaviour Tree needs to have one Behaviour child.")
151+
warnings.append("Behaviour Tree needs to have a Behaviour child to work.")
92152
elif children.size() == 1:
93153
if not children[0] is BTBehaviour:
94-
warnings.append("The child of Behaviour Tree needs to be a Behaviour.")
154+
warnings.append("The child of Behaviour Tree needs to be a BTBehaviour.")
95155
elif children.size() > 1:
96-
warnings.append("Behaviour Tree can have only one Behaviour child.")
156+
warnings.append("Behaviour Tree has more than one child. Only the first child will be used, other sibilings will be ingored.")
97157

98158
return warnings

addons/behaviour_toolkit/behaviour_tree/composites/bt_integrated_fsm.gd

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,37 @@ class_name BTIntegratedFSM extends BTComposite
1212
## [enum BTBehaviour.BTStatus.RUNNING]. If FSM return
1313
## [enum BTBehaviour.BTStatus.SUCCESS] or [enum BTBehaviour.BTStatus.FAILURE]
1414
## the child FSM is stopped.
15+
## After that, the final status of the FSM will be returned
16+
## as the final status of the [BTIntegratedFSM] node.
17+
## [br][br]
18+
## When [BTIntegratedFSM] finds node of type [FiniteStateMachine] as it's first
19+
## child it starts the state machine and runs it on every
20+
## [code]tick()[/code] until the FSM node itself will stop returning
21+
## [enum BTBehaviour.BTStatus.RUNNING].
22+
## [br][br]
23+
## In case where [BTComposite] can't find [FiniteStateMachine] as it's first
24+
## child [enum BTBehaviour.BTStatus.FAILURE] will be returned.
25+
26+
27+
## Default status in case [BTIntegratedFSM] will not find [FiniteStateMachine]
28+
## child as a first node.
29+
@export_enum("SUCCESS", "FAILURE") var default_status: String = "FAILURE":
30+
set(value):
31+
if value == "SUCCESS":
32+
_default_status = BTStatus.SUCCESS
33+
else:
34+
_default_status = BTStatus.FAILURE
35+
1536

37+
var _default_status: BTStatus = BTStatus.FAILURE
1638

17-
var state_machine: FiniteStateMachine = null
39+
40+
# Connecting signal using @onready to omit the need to use super() call
41+
# in _ready() of extended nodes if they override _ready().
42+
@onready var __connect_finite_state_machine_changed: int = \
43+
child_order_changed.connect(_finite_state_machine_changed)
44+
45+
@onready var state_machine: FiniteStateMachine = _get_machine()
1846

1947

2048
func _ready():
@@ -24,6 +52,9 @@ func _ready():
2452

2553

2654
func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus:
55+
if state_machine == null:
56+
return _default_status
57+
2758
if state_machine.active == false:
2859
state_machine.start()
2960

@@ -33,25 +64,56 @@ func tick(_delta: float, _actor: Node, _blackboard: Blackboard) -> BTStatus:
3364
return state_machine.current_bt_status
3465

3566

67+
## Swap this composite nodes current state machine with the provided one.
68+
## If state has no [FiniteStateMachine] as a child the provided one will be added.
69+
## [br][br]
70+
## Old state machine is freed and the new machine will be started on the next
71+
## [code]tick()[/code] callback call.
72+
func swap_finite_state_machine(finite_state_machine: FiniteStateMachine,
73+
force_readable_name: bool = false, keep_groups: bool = false) -> void:
74+
75+
if keep_groups == true and state_machine != null:
76+
for g in state_machine.get_groups():
77+
if not finite_state_machine.is_in_group(g):
78+
finite_state_machine.add_to_group(g, true)
79+
80+
if state_machine == null:
81+
add_child(finite_state_machine, force_readable_name)
82+
else:
83+
state_machine.queue_free()
84+
add_child(finite_state_machine, force_readable_name)
85+
86+
3687
func _get_machine() -> FiniteStateMachine:
3788
if get_child_count() == 0:
3889
return null
3990
else:
40-
return get_child(0)
91+
if get_child(0) is FiniteStateMachine:
92+
return get_child(0)
93+
94+
return null
95+
96+
97+
func _finite_state_machine_changed() -> void:
98+
if Engine.is_editor_hint():
99+
return
100+
101+
state_machine = _get_machine()
102+
state_machine.autostart = false
41103

42104

43105
func _get_configuration_warnings() -> PackedStringArray:
44106
var warnings: Array = []
45107
var children = get_children()
46108

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

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

53115
if children.size() == 1:
54116
if not children[0] is FiniteStateMachine:
55-
warnings.append("BTIntegratedFSM's child node must be a FiniteStateMachine. The first child will be used as the state machine.")
117+
warnings.append("BTIntegratedFSM's first child node must be a FiniteStateMachine. The first child will be used as the state machine.")
56118

57119
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)