diff --git a/addons/behaviour_toolkit/finite_state_machine/fsm_transition.gd b/addons/behaviour_toolkit/finite_state_machine/fsm_transition.gd index 42c917e..07630a2 100644 --- a/addons/behaviour_toolkit/finite_state_machine/fsm_transition.gd +++ b/addons/behaviour_toolkit/finite_state_machine/fsm_transition.gd @@ -11,7 +11,6 @@ class_name FSMTransition extends BehaviourToolkit ## [code]use_event[/code] to true and set the event property to the name ## of the event you want to listen for. - ## The state to transition to. @export var next_state: FSMState: set(value): @@ -19,13 +18,34 @@ class_name FSMTransition extends BehaviourToolkit update_configuration_warnings() @export_category("Transition Logic") + +## Always transition, ignoring `is_valid()` or any events. +@export var always_transition: bool = false: + set(value): + always_transition = value + notify_property_list_changed() + update_configuration_warnings() + + +# Rather than using @export on these properties, we include them here so we can show them conditionally. +func _get_property_list() -> Array[Dictionary]: + var property_list: Array[Dictionary] = [] + + if not always_transition: + property_list.append({"name": "use_event", "type": TYPE_BOOL}) + property_list.append({"name": "event", "type": TYPE_STRING}) + + return property_list + + ## If true, the FSM will check for the event to trigger the transition. -@export var use_event: bool = false: +var use_event: bool = false: set(value): use_event = value update_configuration_warnings() + ## The event that triggers the transition. -@export var event: String = "": +var event: String = "": set(value): event = value update_configuration_warnings() @@ -38,6 +58,9 @@ func _on_transition(_delta: float, _actor: Node, _blackboard: Blackboard) -> voi ## Evaluates true, if the transition conditions are met. func is_valid(_actor: Node, _blackboard: Blackboard) -> bool: + if always_transition: + return true + return false diff --git a/addons/behaviour_toolkit/finite_state_machine/transitions/fsm_split_transition.gd b/addons/behaviour_toolkit/finite_state_machine/transitions/fsm_split_transition.gd new file mode 100644 index 0000000..7ee917b --- /dev/null +++ b/addons/behaviour_toolkit/finite_state_machine/transitions/fsm_split_transition.gd @@ -0,0 +1,63 @@ +@tool +@icon("res://addons/behaviour_toolkit/icons/FSMSplitTransition.svg") +class_name FSMSplitTransition extends FSMTransition + +## The state to transition to. +@export var next_state_true: FSMState: + set(value): + next_state_true = value + update_configuration_warnings() + +@export var next_state_false: FSMState: + set(value): + next_state_false = value + update_configuration_warnings() + +# Internal flag that determines which state gets transitioned to +var _transition_flag: bool + + +# Executed when the transition is taken. +func _on_transition(_delta: float, _actor: Node, _blackboard: Blackboard) -> void: + pass + + +# Always returns true, because this transition always triggers one way or another. +# Because `get_next_state()` doesn't have access to the actor or blackboard, a flag is +# set internally here which sets up the next transition. +func is_valid(actor: Node, blackboard: Blackboard) -> bool: + set_transition_flag(actor, blackboard) + return true + + +func set_transition_flag(_actor: Node, _blackboard: Blackboard) -> void: + pass + + +## Returns which state to transition to, based on internal transition flag set in `set_transition_flag()`. +func get_next_state() -> FSMState: + return next_state_true if _transition_flag else next_state_false + + +# Add custom configuration warnings +# Note: Can be deleted if you don't want to define your own warnings. +func _get_configuration_warnings() -> PackedStringArray: + var warnings: Array = [] + + var parent: Node = get_parent() + if not parent is FSMState: + warnings.append("FSMSplitTransition should be a child of FSMState.") + + if next_state: + warnings.append("FSMSplitTransition has next state; unset and set true and false states.") + + if not next_state_true: + warnings.append("FSMSplitTransition has no next state for true.") + + if not next_state_false: + warnings.append("FSMSplitTransition has no next state for false.") + + if use_event and event == "": + warnings.append("FSMSplitTransition has no event set.") + + return warnings diff --git a/addons/behaviour_toolkit/icons/FSMSplitTransition.svg b/addons/behaviour_toolkit/icons/FSMSplitTransition.svg new file mode 100644 index 0000000..2f245a5 --- /dev/null +++ b/addons/behaviour_toolkit/icons/FSMSplitTransition.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + diff --git a/addons/behaviour_toolkit/icons/FSMSplitTransition.svg.import b/addons/behaviour_toolkit/icons/FSMSplitTransition.svg.import new file mode 100644 index 0000000..fbb7d8f --- /dev/null +++ b/addons/behaviour_toolkit/icons/FSMSplitTransition.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c4vs4uicrouno" +path="res://.godot/imported/FSMSplitTransition.svg-e6773317add235944b13c8c864f570b3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/behaviour_toolkit/icons/FSMSplitTransition.svg" +dest_files=["res://.godot/imported/FSMSplitTransition.svg-e6773317add235944b13c8c864f570b3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/behaviour_toolkit/ui/toolkit_ui.gd b/addons/behaviour_toolkit/ui/toolkit_ui.gd index f832d42..d3675fe 100644 --- a/addons/behaviour_toolkit/ui/toolkit_ui.gd +++ b/addons/behaviour_toolkit/ui/toolkit_ui.gd @@ -38,6 +38,7 @@ func _ready(): %ButtonAddFSM.connect("pressed", _on_button_pressed.bind(FiniteStateMachine, "FiniteStateMachine")) %ButtonState.connect("pressed", _on_button_pressed.bind(FSMState, "FSMState")) %ButtonTransition.connect("pressed", _on_button_pressed.bind(FSMTransition, "FSMTransition")) + %ButtonSplitTransition.connect("pressed", _on_button_pressed.bind(FSMSplitTransition, "FSMSplitTransition")) %ButtonStateIntegratedBT.connect("pressed", _on_button_pressed.bind(FSMStateIntegratedBT, "FSMStateIntegratedBT")) %ButtonStateIntegrationReturn.connect("pressed", _on_button_pressed.bind(FSMStateIntegrationReturn, "FSMStateIntegrationReturn")) diff --git a/addons/behaviour_toolkit/ui/toolkit_ui.tscn b/addons/behaviour_toolkit/ui/toolkit_ui.tscn index ddaca06..ad89aad 100644 --- a/addons/behaviour_toolkit/ui/toolkit_ui.tscn +++ b/addons/behaviour_toolkit/ui/toolkit_ui.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=39 format=3 uid="uid://o16emp4t4wb1"] +[gd_scene load_steps=40 format=3 uid="uid://o16emp4t4wb1"] [ext_resource type="Script" path="res://addons/behaviour_toolkit/ui/toolkit_ui.gd" id="1_51rvx"] [ext_resource type="Texture2D" uid="uid://boof0yioplbqr" path="res://addons/behaviour_toolkit/icons/FSMState.svg" id="1_hqqj5"] @@ -26,6 +26,7 @@ [ext_resource type="Texture2D" uid="uid://b45qiy5niriwu" path="res://addons/behaviour_toolkit/icons/BTLeafPrint.svg" id="10_ji4ht"] [ext_resource type="Texture2D" uid="uid://xi8li3jk2vs6" path="res://addons/behaviour_toolkit/icons/BTLeafWait.svg" id="10_n2nk3"] [ext_resource type="Texture2D" uid="uid://hn65lqk8tero" path="res://addons/behaviour_toolkit/icons/BTDecoratorRepeat.svg" id="10_nvbp1"] +[ext_resource type="Texture2D" uid="uid://c4vs4uicrouno" path="res://addons/behaviour_toolkit/icons/FSMSplitTransition.svg" id="10_ypexi"] [ext_resource type="Texture2D" uid="uid://cwvb6d7kvo53u" path="res://addons/behaviour_toolkit/icons/BTLeafCall.svg" id="12_axgn0"] [ext_resource type="Texture2D" uid="uid://by4ymq46fp1ra" path="res://addons/behaviour_toolkit/icons/BTLeafCondition.svg" id="13_6h0vl"] [ext_resource type="Texture2D" uid="uid://ddx7a726xhria" path="res://addons/behaviour_toolkit/icons/BTCompositeIntegration.svg" id="13_gmc6x"] @@ -187,6 +188,13 @@ layout_mode = 2 text = "New Transition" icon = ExtResource("2_24di4") +[node name="ButtonSplitTransition" type="Button" parent="ScrollContainer/MarginContainer/VBoxContainer/Panel/ScrollContainer/Toolbox/FiniteStateMachine"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +text = "Split Transition" +icon = ExtResource("10_ypexi") + [node name="BehaviourTree" type="VBoxContainer" parent="ScrollContainer/MarginContainer/VBoxContainer/Panel/ScrollContainer/Toolbox"] unique_name_in_owner = true layout_mode = 2 diff --git a/docs/documentation.md b/docs/documentation.md index 8472044..b90d6a7 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -29,9 +29,11 @@ The node icons where designed/choosen to give you a quick overview of their purp - [Signals](#signals) - [ FSMState](#-fsmstate) - [Methods](#methods-1) + - [Extras][#extras] - [ FSMTransition](#-fsmtransition) - [Properties](#properties-1) - [Methods](#methods-2) + - [Extras][#extras-1] - [Behaviour Tree](#behaviour-tree) - [Usage](#usage-1) - [Tree Nodes](#tree-nodes) @@ -109,6 +111,12 @@ This is the base class for all states. On ready, all `FSMTransition` child nodes - void `_on_exit(actor: Node, blackboard: Blackboard)` - Called when the state is exited. +#### Extras +There are extra states that you can use out of the box: + +- ![PrintOnEnter Icon](../addons/behaviour_toolkit/icons/BTLeafPrint.svg) Print on Enter + - Prints a message to the console when entering this state. Very handy for debugging. + ### ![FSM Transition Icon](../addons/behaviour_toolkit/icons/FSMTransition.svg) FSMTransition This is the base class for all transitions. To implement your logic you can override the `_on_transition` method when extending the node's script. To setup custom conditions you can override the `is_valid` method. If you want to use events to trigger the transition, set `use_event` to `true` and set the `event` property to the name of the event you want to listen for. @@ -128,6 +136,16 @@ This is the base class for all transitions. To implement your logic you can over - bool `is_valid` - Should return `true` if the conditions for the transition are met. +#### Extras +There are a few extra transitions that you can use out of the box: + +- ![SplitTransition Icon](../addons/behaviour_toolkit/icons/FSMSplitTransition.svg) Split + - Always transition to one of two states, based on a coded boolean check. +- ![AlwaysTransition Icon](../addons/behaviour_toolkit/icons/AlwaysTransition.svg) Always + - Always transition, useful for states that only act once. + +Script templates are also available to make it easier to extend the above classes. + # Behaviour Tree A behavior tree is a mathematical model of plan execution used in computer science, robotics, control systems and video games. They describe switchings between a finite set of tasks in a modular fashion. Their strength comes from their ability to create very complex tasks composed of simple tasks, without worrying how the simple tasks are implemented. ([Wikipedia](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control))) diff --git a/examples/fsm_split_transition/fsm_split_transition_example.tscn b/examples/fsm_split_transition/fsm_split_transition_example.tscn new file mode 100644 index 0000000..a7d8307 --- /dev/null +++ b/examples/fsm_split_transition/fsm_split_transition_example.tscn @@ -0,0 +1,53 @@ +[gd_scene load_steps=7 format=3 uid="uid://dd1t17mhnvpn4"] + +[ext_resource type="Script" path="res://addons/behaviour_toolkit/finite_state_machine/fsm.gd" id="1_ugani"] +[ext_resource type="Script" path="res://examples/fsm_split_transition/fsm_state_print_counter.gd" id="2_dxxwi"] +[ext_resource type="Script" path="res://examples/fsm_split_transition/fsm_state_print_on_enter.gd" id="2_s4dqp"] +[ext_resource type="Script" path="res://examples/fsm_split_transition/fsm_transition_to_finish.gd" id="3_mm16c"] +[ext_resource type="Script" path="res://examples/fsm_split_transition/fsm_transition_even_or_odd.gd" id="4_kimeb"] +[ext_resource type="Script" path="res://addons/behaviour_toolkit/finite_state_machine/fsm_transition.gd" id="6_am5vp"] + +[node name="SplitTransitionExample" type="Node2D"] + +[node name="FiniteStateMachine" type="Node" parent="." node_paths=PackedStringArray("initial_state")] +script = ExtResource("1_ugani") +autostart = true +initial_state = NodePath("PrintCounter") + +[node name="PrintCounter" type="Node" parent="FiniteStateMachine"] +script = ExtResource("2_dxxwi") + +[node name="TransitionToFinish" type="Node" parent="FiniteStateMachine/PrintCounter" node_paths=PackedStringArray("next_state")] +script = ExtResource("3_mm16c") +next_state = NodePath("../../PrintDone") +use_event = false +event = "" + +[node name="GoEvenOrOdd" type="Node" parent="FiniteStateMachine/PrintCounter" node_paths=PackedStringArray("next_state_true", "next_state_false")] +script = ExtResource("4_kimeb") +next_state_true = NodePath("../../PrintEven") +next_state_false = NodePath("../../PrintOdd") +use_event = false +event = "" + +[node name="PrintEven" type="Node" parent="FiniteStateMachine"] +script = ExtResource("2_s4dqp") +custom_text = "Even" + +[node name="ReturnToStart" type="Node" parent="FiniteStateMachine/PrintEven" node_paths=PackedStringArray("next_state")] +script = ExtResource("6_am5vp") +next_state = NodePath("../../PrintCounter") +always_transition = true + +[node name="PrintOdd" type="Node" parent="FiniteStateMachine"] +script = ExtResource("2_s4dqp") +custom_text = "Odd" + +[node name="ReturnToStart" type="Node" parent="FiniteStateMachine/PrintOdd" node_paths=PackedStringArray("next_state")] +script = ExtResource("6_am5vp") +next_state = NodePath("../../PrintCounter") +always_transition = true + +[node name="PrintDone" type="Node" parent="FiniteStateMachine"] +script = ExtResource("2_s4dqp") +custom_text = "Done" diff --git a/examples/fsm_split_transition/fsm_state_print_counter.gd b/examples/fsm_split_transition/fsm_state_print_counter.gd new file mode 100644 index 0000000..bb2541d --- /dev/null +++ b/examples/fsm_split_transition/fsm_state_print_counter.gd @@ -0,0 +1,19 @@ +@tool +extends FSMState + + +# Executes after the state is entered. +func _on_enter(_actor: Node, _blackboard: Blackboard) -> void: + if _blackboard.get_value("counter") == null: + _blackboard.set_value("counter", 0) + + var counter: int = _blackboard.get_value("counter") + if counter == null: + print(0) + else: + print(counter) + +## Executes before the state is exited. +func _on_exit(_actor: Node, _blackboard: Blackboard) -> void: + var counter: int = _blackboard.get_value("counter") + _blackboard.set_value("counter", counter + 1) diff --git a/examples/fsm_split_transition/fsm_state_print_on_enter.gd b/examples/fsm_split_transition/fsm_state_print_on_enter.gd new file mode 100644 index 0000000..89f0adf --- /dev/null +++ b/examples/fsm_split_transition/fsm_state_print_on_enter.gd @@ -0,0 +1,13 @@ +@tool +@icon("res://addons/behaviour_toolkit/icons/BTLeafPrint.svg") +extends FSMState + +@export var custom_text: String + + +# Executes after the state is entered. +func _on_enter(_actor: Node, _blackboard: Blackboard) -> void: + if custom_text != "": + print(custom_text) + else: + print("Hello World!") diff --git a/examples/fsm_split_transition/fsm_transition_even_or_odd.gd b/examples/fsm_split_transition/fsm_transition_even_or_odd.gd new file mode 100644 index 0000000..d9836f1 --- /dev/null +++ b/examples/fsm_split_transition/fsm_transition_even_or_odd.gd @@ -0,0 +1,30 @@ +@tool +@icon("res://addons/behaviour_toolkit/icons/FSMSplitTransition.svg") +extends FSMSplitTransition + + +# Executed when the transition is taken. +func _on_transition(_delta: float, _actor: Node, _blackboard: Blackboard) -> void: + pass + + +# Set `_transition_flag` to true or false, which will determine which transition is taken. +func set_transition_flag(_actor: Node, _blackboard: Blackboard) -> void: + var counter: int = _blackboard.get_value("counter") + if counter % 2 == 0: + _transition_flag = true + else: + _transition_flag = false + + +# Add custom configuration warnings +# Note: Can be deleted if you don't want to define your own warnings. +func _get_configuration_warnings() -> PackedStringArray: + var warnings: Array = [] + + warnings.append_array(super._get_configuration_warnings()) + + if use_event or event != "": + warnings.append("FSMAlwaysTransition does not use events.") + + return warnings diff --git a/examples/fsm_split_transition/fsm_transition_to_finish.gd b/examples/fsm_split_transition/fsm_transition_to_finish.gd new file mode 100644 index 0000000..0a1c342 --- /dev/null +++ b/examples/fsm_split_transition/fsm_transition_to_finish.gd @@ -0,0 +1,6 @@ +@tool +extends FSMTransition + +# Evaluates true, if the transition conditions are met. +func is_valid(_actor: Node, _blackboard: Blackboard) -> bool: + return _blackboard.get_value("counter") == 10 diff --git a/script_templates/FSMSplitTransition/new_split_transition.gd b/script_templates/FSMSplitTransition/new_split_transition.gd new file mode 100644 index 0000000..ca896d2 --- /dev/null +++ b/script_templates/FSMSplitTransition/new_split_transition.gd @@ -0,0 +1,25 @@ +@tool +extends FSMSplitTransition + + +# Executed when the transition is taken. +func _on_transition(_delta: float, _actor: Node, _blackboard: Blackboard) -> void: + pass + + +# Set `_transition_flag` to true or false, which will determine which transition is taken. +func set_transition_flag(_actor: Node, _blackboard: Blackboard) -> void: + pass + + +# Add custom configuration warnings +# Note: Can be deleted if you don't want to define your own warnings. +func _get_configuration_warnings() -> PackedStringArray: + var warnings: Array = [] + + warnings.append_array(super._get_configuration_warnings()) + + if use_event or event != "": + warnings.append("FSMAlwaysTransition does not use events.") + + return warnings