diff --git a/demo/demo/assets/ui.theme b/demo/demo/assets/ui.theme
index 1382dac..e010fad 100644
Binary files a/demo/demo/assets/ui.theme and b/demo/demo/assets/ui.theme differ
diff --git a/demo/demo/scenes/base/code_edit.gd b/demo/demo/scenes/base/code_edit.gd
new file mode 100644
index 0000000..5416996
--- /dev/null
+++ b/demo/demo/scenes/base/code_edit.gd
@@ -0,0 +1,140 @@
+#*
+#* code_edit.gd
+#* =============================================================================
+#* Copyright 2024 Serhii Snitsaruk
+#*
+#* Use of this source code is governed by an MIT-style
+#* license that can be found in the LICENSE file or at
+#* https://opensource.org/licenses/MIT.
+#* =============================================================================
+#*
+
+extends CodeEdit
+
+const RESERVED_WORDS := [
+ # Control flow.
+ "break",
+ "continue",
+ "elif",
+ "else",
+ "for",
+ "if",
+ "match",
+ "pass",
+ "return",
+ "when",
+ "while",
+ # Declarations.
+ "class",
+ "class_name",
+ "const",
+ "enum",
+ "extends",
+ "func",
+ "namespace",
+ "signal",
+ "static",
+ "trait",
+ "var",
+ # Other keywords.
+ "await",
+ "breakpoint",
+ "self",
+ "super",
+ "yield",
+ # Operators.
+ "and",
+ "as",
+ "in",
+ "is",
+ "not",
+ "or",
+ # Special values.
+ "false",
+ "null",
+ "true",
+ # Constants.
+ "INF",
+ "NAN",
+ "PI",
+ "TAU",
+ # Functions.
+ "assert",
+ "preload",
+]
+
+const TYPE_WORDS := [
+ "bool",
+ "int",
+ "float",
+ "void",
+ "String",
+ "Vector2",
+ "Vector2i",
+ "Rect2",
+ "Rect2i",
+ "Vector3",
+ "Vector3i",
+ "Transform2D",
+ "Vector4",
+ "Vector4i",
+ "Plane",
+ "Quaternion",
+ "AABB",
+ "Basis",
+ "Transform3D",
+ "Projection",
+ "Color",
+ "StringName",
+ "NodePath",
+ "RID",
+ "Callable",
+ "Signal",
+ "Dictionary",
+ "Array",
+ "PackedByteArray",
+ "PackedInt32Array",
+ "PackedInt64Array",
+ "PackedFloat32Array",
+ "PackedFloat64Array",
+ "PackedStringArray",
+ "PackedVector2Array",
+ "PackedVector3Array",
+ "PackedColorArray",
+ # Other types
+ "Status",
+]
+
+
+func _ready() -> void:
+ var highlighter := CodeHighlighter.new()
+ syntax_highlighter = highlighter
+ highlighter.number_color = Color.AQUAMARINE
+ highlighter.symbol_color = Color.CORNFLOWER_BLUE
+ highlighter.function_color = Color.DEEP_SKY_BLUE
+ highlighter.member_variable_color = Color.LIGHT_BLUE
+
+ # Engine types
+ for c in ClassDB.get_class_list():
+ syntax_highlighter.add_keyword_color(c, Color.AQUAMARINE)
+
+ syntax_highlighter.add_color_region("#", "", Color.DIM_GRAY, true)
+ syntax_highlighter.add_color_region("@", " ", Color.GOLDENROD)
+ syntax_highlighter.add_color_region("\"", "\"", Color.GOLD)
+
+ for keyword in RESERVED_WORDS:
+ syntax_highlighter.add_keyword_color(keyword, Color.INDIAN_RED)
+
+ for typeword in TYPE_WORDS:
+ syntax_highlighter.add_keyword_color(typeword, Color.AQUAMARINE)
+
+
+func set_source_code(source_code: String) -> void:
+ # Hide license header
+ var idx: int = source_code.find("#*")
+ while idx != - 1:
+ source_code = source_code.substr(0, idx) + source_code.substr(source_code.findn("\n", idx) + 1)
+ idx = source_code.findn("#*", idx)
+
+ text = "" # Workaround
+ text = source_code
diff --git a/demo/demo/scenes/showcase.gd b/demo/demo/scenes/showcase.gd
index 692451c..50c5946 100644
--- a/demo/demo/scenes/showcase.gd
+++ b/demo/demo/scenes/showcase.gd
@@ -1,3 +1,14 @@
+#*
+#* showcase.gd
+#* =============================================================================
+#* Copyright 2024 Serhii Snitsaruk
+#*
+#* Use of this source code is governed by an MIT-style
+#* license that can be found in the LICENSE file or at
+#* https://opensource.org/licenses/MIT.
+#* =============================================================================
+#*
+
extends Node2D
@onready var behavior_tree_view: BehaviorTreeView = %BehaviorTreeView
@@ -10,6 +21,8 @@ extends Node2D
@onready var begin_tutorial: Button = %BeginTutorial
@onready var navigation_hint: Label = %NavigationHint
@onready var scene_title: Label = %SceneTitle
+@onready var code_popup = %CodePopup
+@onready var code_edit = %CodeEdit
var bt_player: BTPlayer
var selected_tree_index: int = -1
@@ -17,6 +30,7 @@ var agent_files: Array[String]
var agents_dir: String
var is_tutorial: bool = false
+
func _ready() -> void:
agent_selection.get_popup().id_pressed.connect(_on_agent_selection_id_pressed)
previous.pressed.connect(func(): _on_agent_selection_id_pressed(selected_tree_index - 1))
@@ -116,14 +130,13 @@ func _on_agent_selection_id_pressed(id: int) -> void:
# Treat filename as a title
agent_selection.text = agent_selection.text.trim_suffix(".tres")
previous.disabled = id == 0
- next.disabled = id == (agent_files.size()-1)
+ next.disabled = id == (agent_files.size() - 1)
func _on_switch_to_game_pressed() -> void:
get_tree().change_scene_to_file("res://demo/scenes/game.tscn")
-
func _on_minimize_description_button_down() -> void:
description.visible = not description.visible
minimize_description.text = "-" if description.visible else "+"
@@ -132,3 +145,10 @@ func _on_minimize_description_button_down() -> void:
func _on_tutorial_pressed() -> void:
is_tutorial = not is_tutorial
_initialize()
+
+
+func _on_behavior_tree_view_task_selected(_type_name: String, p_script_path: String) -> void:
+ if not p_script_path.is_empty():
+ var sc: Script = load(p_script_path)
+ code_edit.set_source_code(sc.source_code)
+ code_popup.popup.call_deferred()
diff --git a/demo/demo/scenes/showcase.tscn b/demo/demo/scenes/showcase.tscn
index 0ec339b..5e7946a 100644
--- a/demo/demo/scenes/showcase.tscn
+++ b/demo/demo/scenes/showcase.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=15 format=3 uid="uid://b3ae14mc2ty3y"]
+[gd_scene load_steps=16 format=3 uid="uid://b3ae14mc2ty3y"]
[ext_resource type="Script" path="res://demo/scenes/showcase.gd" id="1_l12ql"]
[ext_resource type="Theme" uid="uid://boqtjf88xcpu4" path="res://demo/assets/ui.theme" id="2_3d7dj"]
@@ -10,6 +10,7 @@
[ext_resource type="Texture2D" uid="uid://bjakugmqbbtw7" path="res://demo/assets/arrow_right.png" id="7_5do2y"]
[ext_resource type="PackedScene" uid="uid://bsig1usigbbuy" path="res://demo/scenes/base/arena.tscn" id="7_42nq6"]
[ext_resource type="PackedScene" uid="uid://c5fhe3tulhlco" path="res://demo/props/dummy.tscn" id="8_apshw"]
+[ext_resource type="Script" path="res://demo/scenes/base/code_edit.gd" id="9_txke7"]
[sub_resource type="LabelSettings" id="LabelSettings_rdr7a"]
font = ExtResource("3_7vli5")
@@ -242,6 +243,27 @@ bbcode_enabled = true
text = "[b]Behavior Trees[/b] are composed of tasks that represent specific actions or decision-making rules. Tasks can be broadly categorized into two main types: control tasks and leaf tasks. Control tasks determine the execution flow within the tree. They include Sequence, Selector, and Invert. Leaf tasks represent specific actions to perform, like moving or attacking, or conditions that need to be checked. The BTTask class provides the foundation for various building blocks of the Behavior Trees. BT tasks can share data with the help of the Blackboard."
fit_content = true
+[node name="CodePopup" type="PopupPanel" parent="UI Layer/Control"]
+unique_name_in_owner = true
+position = Vector2i(135, 60)
+size = Vector2i(1024, 708)
+visible = true
+
+[node name="CodeEdit" type="CodeEdit" parent="UI Layer/Control/CodePopup"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(800, 700)
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 4.0
+offset_top = 4.0
+offset_right = -4.0
+offset_bottom = -4.0
+grow_horizontal = 2
+grow_vertical = 2
+gutters_draw_line_numbers = true
+script = ExtResource("9_txke7")
+
[node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(400, 0)
zoom = Vector2(0.88, 0.88)
@@ -262,4 +284,5 @@ position = Vector2(1106, 423)
[connection signal="pressed" from="UI Layer/Control/Toolbar/HBoxContainer/SwitchToGame" to="." method="_on_switch_to_game_pressed"]
[connection signal="pressed" from="UI Layer/Control/Toolbar/HBoxContainer/BeginTutorial" to="." method="_on_tutorial_pressed"]
+[connection signal="task_selected" from="UI Layer/Control/BehaviorInspector/VBoxContainer/BehaviorTreeView" to="." method="_on_behavior_tree_view_task_selected"]
[connection signal="button_down" from="UI Layer/Control/PanelContainer/Control/Header/MinimizeDescription" to="." method="_on_minimize_description_button_down"]
diff --git a/doc_classes/BehaviorTreeView.xml b/doc_classes/BehaviorTreeView.xml
index 6c191b2..1df9fac 100644
--- a/doc_classes/BehaviorTreeView.xml
+++ b/doc_classes/BehaviorTreeView.xml
@@ -22,4 +22,13 @@
Minimum delay between two updates (in milliseconds). Set to higher values for a lower CPU load.
+
+
+
+
+
+ Emitted when a task item is selected in [BehaviorTreeView].
+
+
+
diff --git a/editor/debugger/behavior_tree_view.cpp b/editor/debugger/behavior_tree_view.cpp
index c8e05d2..40c6a4a 100644
--- a/editor/debugger/behavior_tree_view.cpp
+++ b/editor/debugger/behavior_tree_view.cpp
@@ -34,6 +34,22 @@
#include
#endif // LIMBOAI_GDEXTENSION
+inline static uint64_t item_get_task_id(TreeItem *p_item) {
+ return p_item->get_metadata(0);
+}
+
+inline static BTTask::Status item_get_task_status(TreeItem *p_item) {
+ return VariantCaster::cast(p_item->get_metadata(1));
+}
+
+inline static String item_get_task_type(TreeItem *p_item) {
+ return ((String)p_item->get_metadata(2)).get_slicec('|', 0);
+}
+
+inline static String item_get_task_script_path(TreeItem *p_item) {
+ return ((String)p_item->get_metadata(2)).get_slicec('|', 1);
+}
+
void BehaviorTreeView::_draw_running_status(Object *p_obj, Rect2 p_rect) {
p_rect = p_rect.grow_side(SIDE_LEFT, p_rect.get_position().x);
theme_cache.sbf_running->draw(tree->get_canvas_item(), p_rect);
@@ -54,15 +70,21 @@ void BehaviorTreeView::_item_collapsed(Object *p_obj) {
if (!item) {
return;
}
- uint64_t id = item->get_metadata(0);
+ uint64_t id = item_get_task_id(item);
bool collapsed = item->is_collapsed();
if (!collapsed_ids.has(id) && collapsed) {
- collapsed_ids.push_back(item->get_metadata(0));
+ collapsed_ids.push_back(item_get_task_id(item));
} else if (collapsed_ids.has(id) && !collapsed) {
collapsed_ids.erase(id);
}
}
+void BehaviorTreeView::_item_selected() {
+ TreeItem *item = tree->get_selected();
+ ERR_FAIL_NULL(item);
+ emit_signal(LW_NAME(task_selected), item_get_task_type(item), item_get_task_script_path(item));
+}
+
double BehaviorTreeView::_get_editor_scale() const {
if (Engine::get_singleton()->is_editor_hint()) {
return EDSCALE;
@@ -85,7 +107,7 @@ void BehaviorTreeView::_update_tree(const Ref &p_data) {
// Remember selected.
uint64_t selected_id = 0;
if (tree->get_selected()) {
- selected_id = tree->get_selected()->get_metadata(0);
+ selected_id = item_get_task_id(tree->get_selected());
}
if (last_root_id != 0 && p_data->tasks.size() > 0 && last_root_id == (uint64_t)p_data->tasks[0].id) {
@@ -98,7 +120,7 @@ void BehaviorTreeView::_update_tree(const Ref &p_data) {
ERR_FAIL_COND(idx >= p_data->tasks.size());
const BTTask::Status current_status = (BTTask::Status)p_data->tasks[idx].status;
- const BTTask::Status last_status = VariantCaster::cast(item->get_metadata(1));
+ const BTTask::Status last_status = item_get_task_status(item);
const bool status_changed = last_status != p_data->tasks[idx].status;
if (status_changed) {
@@ -166,6 +188,7 @@ void BehaviorTreeView::_update_tree(const Ref &p_data) {
item->set_metadata(0, task_data.id);
item->set_metadata(1, task_data.status);
+ item->set_metadata(2, task_data.type_name + String("|") + task_data.script_path);
item->set_text(0, task_data.name);
if (task_data.is_custom_name) {
@@ -262,6 +285,7 @@ void BehaviorTreeView::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
tree->connect(LW_NAME(item_collapsed), callable_mp(this, &BehaviorTreeView::_item_collapsed));
+ tree->connect(LW_NAME(item_selected), callable_mp(this, &BehaviorTreeView::_item_selected));
} break;
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED:
@@ -292,6 +316,8 @@ void BehaviorTreeView::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_update_interval_msec", "p_milliseconds"), &BehaviorTreeView::set_update_interval_msec);
ClassDB::bind_method(D_METHOD("get_update_interval_msec"), &BehaviorTreeView::get_update_interval_msec);
ADD_PROPERTY(PropertyInfo(Variant::INT, "update_interval_msec"), "set_update_interval_msec", "get_update_interval_msec");
+
+ ADD_SIGNAL(MethodInfo("task_selected", PropertyInfo(Variant::STRING, "p_type_name"), PropertyInfo(Variant::STRING, "p_script_path")));
}
BehaviorTreeView::BehaviorTreeView() {
diff --git a/editor/debugger/behavior_tree_view.h b/editor/debugger/behavior_tree_view.h
index 3194348..bfbc599 100644
--- a/editor/debugger/behavior_tree_view.h
+++ b/editor/debugger/behavior_tree_view.h
@@ -61,6 +61,7 @@ private:
void _draw_failure_status(Object *p_obj, Rect2 p_rect);
void _draw_fresh(Object *p_obj, Rect2 p_rect) {}
void _item_collapsed(Object *p_obj);
+ void _item_selected();
double _get_editor_scale() const;
void _update_tree(const Ref &p_data);