From 52a70fdee521d6aad1f52be5a8e26a9717026c7e Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 25 Sep 2023 18:07:26 +0200 Subject: [PATCH] Add editor support for BTProbabilitySelector --- bt/tasks/composites/bt_probability_selector.h | 6 +- editor/limbo_ai_editor_plugin.cpp | 81 ++++++++++++- editor/limbo_ai_editor_plugin.h | 12 +- editor/task_tree.cpp | 114 ++++++++++++++++-- editor/task_tree.h | 20 ++- 5 files changed, 208 insertions(+), 25 deletions(-) diff --git a/bt/tasks/composites/bt_probability_selector.h b/bt/tasks/composites/bt_probability_selector.h index 744a926..8afd1d9 100644 --- a/bt/tasks/composites/bt_probability_selector.h +++ b/bt/tasks/composites/bt_probability_selector.h @@ -14,6 +14,7 @@ #include "modules/limboai/bt/tasks/bt_composite.h" +#include "core/core_string_names.h" #include "core/typedefs.h" class BTProbabilitySelector : public BTComposite { @@ -29,7 +30,10 @@ private: _FORCE_INLINE_ double _get_weight(int p_index) const { return get_child(p_index)->get_meta(SNAME("_weight_"), 1.0); } _FORCE_INLINE_ double _get_weight(Ref p_task) const { return p_task->get_meta(SNAME("_weight_"), 1.0); } - _FORCE_INLINE_ void _set_weight(int p_index, double p_weight) { get_child(p_index)->set_meta(SNAME("_weight_"), Variant(p_weight)); } + _FORCE_INLINE_ void _set_weight(int p_index, double p_weight) { + get_child(p_index)->set_meta(SNAME("_weight_"), Variant(p_weight)); + get_child(p_index)->emit_signal(CoreStringNames::get_singleton()->changed); + } _FORCE_INLINE_ double _get_total_weight() const { double total = 0.0; for (int i = 0; i < get_child_count(); i++) { diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index 82f5933..1c570af 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -15,11 +15,13 @@ #include "action_banner.h" #include "modules/limboai/bt/tasks/bt_comment.h" +#include "modules/limboai/bt/tasks/composites/bt_probability_selector.h" #include "modules/limboai/bt/tasks/composites/bt_selector.h" #include "modules/limboai/editor/debugger/limbo_debugger_plugin.h" #include "modules/limboai/util/limbo_utility.h" #include "core/config/project_settings.h" +#include "core/error/error_macros.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/debugger/script_editor_debugger.h" #include "editor/editor_file_system.h" @@ -267,10 +269,13 @@ void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) { Ref task = task_tree->get_selected(); ERR_FAIL_COND_MSG(task.is_null(), "LimboAIEditor: get_selected() returned null"); + if (task_tree->selected_has_probability()) { + menu->add_item(TTR("Edit Probability"), ACTION_EDIT_PROBABILITY); + } menu->add_icon_shortcut(theme_cache.rename_task_icon, ED_GET_SHORTCUT("limbo_ai/rename_task"), ACTION_RENAME); menu->add_icon_item(theme_cache.edit_script_icon, TTR("Edit Script"), ACTION_EDIT_SCRIPT); menu->add_icon_item(theme_cache.open_doc_icon, TTR("Open Documentation"), ACTION_OPEN_DOC); - menu->set_item_disabled(ACTION_EDIT_SCRIPT, task->get_script().is_null()); + menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script().is_null()); menu->add_separator(); menu->add_icon_shortcut(theme_cache.move_task_up_icon, ED_GET_SHORTCUT("limbo_ai/move_task_up"), ACTION_MOVE_UP); @@ -308,6 +313,15 @@ void LimboAIEditor::_action_selected(int p_id) { rename_edit->select_all(); rename_edit->grab_focus(); } break; + case ACTION_EDIT_PROBABILITY: { + Rect2 rect = task_tree->get_selected_probability_rect(); + ERR_FAIL_COND(rect == Rect2()); + rect.position.y += rect.size.y; + rect.position += task_tree->get_rect().position; + rect = task_tree->get_screen_transform().xform(rect); + probability_edit->set_value_no_signal(task_tree->get_selected_probability_weight()); + probability_popup->popup(rect); + } break; case ACTION_EDIT_SCRIPT: { ERR_FAIL_COND(task_tree->get_selected().is_null()); EditorNode::get_singleton()->edit_resource(task_tree->get_selected()->get_script()); @@ -419,6 +433,14 @@ void LimboAIEditor::_action_selected(int p_id) { } } +void LimboAIEditor::_on_probability_edited(double p_value) { + Ref selected = task_tree->get_selected(); + ERR_FAIL_COND(selected == nullptr); + Ref probability_selector = selected->get_parent(); + ERR_FAIL_COND(probability_selector.is_null()); + probability_selector->set_weight(probability_selector->get_child_index(selected), p_value); +} + void LimboAIEditor::_misc_option_selected(int p_id) { switch (p_id) { case MISC_OPEN_DEBUGGER: { @@ -491,10 +513,6 @@ void LimboAIEditor::_on_tree_task_selected(const Ref &p_task) { EditorNode::get_singleton()->edit_resource(p_task); } -void LimboAIEditor::_on_tree_task_double_clicked() { - _action_selected(ACTION_RENAME); -} - void LimboAIEditor::_on_visibility_changed() { if (task_tree->is_visible_in_tree()) { Ref sel = task_tree->get_selected(); @@ -925,7 +943,8 @@ LimboAIEditor::LimboAIEditor() { task_tree->connect("rmb_pressed", callable_mp(this, &LimboAIEditor::_on_tree_rmb)); task_tree->connect("task_selected", callable_mp(this, &LimboAIEditor::_on_tree_task_selected)); task_tree->connect("task_dragged", callable_mp(this, &LimboAIEditor::_on_task_dragged)); - task_tree->connect("task_double_clicked", callable_mp(this, &LimboAIEditor::_on_tree_task_double_clicked)); + task_tree->connect("task_activated", callable_mp(this, &LimboAIEditor::_action_selected).bind(ACTION_RENAME)); + task_tree->connect("probability_clicked", callable_mp(this, &LimboAIEditor::_action_selected).bind(ACTION_EDIT_PROBABILITY)); task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_on_visibility_changed)); task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_update_banners)); hsc->add_child(task_tree); @@ -957,6 +976,56 @@ LimboAIEditor::LimboAIEditor() { add_child(menu); menu->connect("id_pressed", callable_mp(this, &LimboAIEditor::_action_selected)); + probability_popup = memnew(PopupPanel); + { + VBoxContainer *vbc = memnew(VBoxContainer); + probability_popup->add_child(vbc); + + // PanelContainer *mode_panel = memnew(PanelContainer); + // vbc->add_child(mode_panel); + + // HBoxContainer *mode_hbox = memnew(HBoxContainer); + // mode_panel->add_child(mode_hbox); + + // Ref button_group; + // button_group.instantiate(); + + // Button *percent_button = memnew(Button); + // mode_hbox->add_child(percent_button); + // percent_button->set_flat(true); + // percent_button->set_toggle_mode(true); + // percent_button->set_button_group(button_group); + // percent_button->set_focus_mode(Control::FOCUS_NONE); + // percent_button->set_text(TTR("Percent")); + // percent_button->set_tooltip_text(TTR("Edit percent")); + // percent_button->set_pressed(true); + // // percent_button->connect(SNAME("pressed"), callable_mp()) + + // Button *weight_button = memnew(Button); + // mode_hbox->add_child(weight_button); + // weight_button->set_flat(true); + // weight_button->set_toggle_mode(true); + // weight_button->set_button_group(button_group); + // weight_button->set_focus_mode(Control::FOCUS_NONE); + // weight_button->set_text(TTR("Weight")); + // weight_button->set_tooltip_text(TTR("Edit weight")); + + Label *probability_header = memnew(Label); + vbc->add_child(probability_header); + probability_header->set_text(TTR("Weight")); + probability_header->set_theme_type_variation("HeaderSmall"); + + probability_edit = memnew(EditorSpinSlider); + vbc->add_child(probability_edit); + probability_edit->set_min(0.0); + probability_edit->set_max(10.0); + probability_edit->set_step(0.01); + probability_edit->set_allow_greater(true); + probability_edit->set_custom_minimum_size(Size2(200.0 * EDSCALE, 0.0)); + probability_edit->connect(SNAME("value_changed"), callable_mp(this, &LimboAIEditor::_on_probability_edited)); + } + add_child(probability_popup); + rename_dialog = memnew(ConfirmationDialog); { VBoxContainer *vbc = memnew(VBoxContainer); diff --git a/editor/limbo_ai_editor_plugin.h b/editor/limbo_ai_editor_plugin.h index 2a3e44c..d3c1dfc 100644 --- a/editor/limbo_ai_editor_plugin.h +++ b/editor/limbo_ai_editor_plugin.h @@ -23,6 +23,7 @@ #include "core/templates/hash_set.h" #include "editor/editor_node.h" #include "editor/editor_plugin.h" +#include "editor/gui/editor_spin_slider.h" #include "scene/gui/box_container.h" #include "scene/gui/control.h" #include "scene/gui/file_dialog.h" @@ -41,6 +42,7 @@ class LimboAIEditor : public Control { private: enum Action { ACTION_RENAME, + ACTION_EDIT_PROBABILITY, ACTION_EDIT_SCRIPT, ACTION_OPEN_DOC, ACTION_MOVE_UP, @@ -79,12 +81,16 @@ private: VBoxContainer *banners; Panel *usage_hint; PopupMenu *menu; + HBoxContainer *fav_tasks_hbox; + TaskPalette *task_palette; + + PopupPanel *probability_popup; + EditorSpinSlider *probability_edit; + FileDialog *save_dialog; FileDialog *load_dialog; Button *history_back; Button *history_forward; - TaskPalette *task_palette; - HBoxContainer *fav_tasks_hbox; Button *new_btn; Button *load_btn; @@ -124,8 +130,8 @@ private: void _on_tree_rmb(const Vector2 &p_menu_pos); void _action_selected(int p_id); void _misc_option_selected(int p_id); + void _on_probability_edited(double p_value); void _on_tree_task_selected(const Ref &p_task); - void _on_tree_task_double_clicked(); void _on_visibility_changed(); void _on_header_pressed(); void _on_save_pressed(); diff --git a/editor/task_tree.cpp b/editor/task_tree.cpp index 76fcb67..8b3af83 100644 --- a/editor/task_tree.cpp +++ b/editor/task_tree.cpp @@ -12,6 +12,7 @@ #include "task_tree.h" #include "modules/limboai/bt/tasks/bt_comment.h" +#include "modules/limboai/bt/tasks/composites/bt_probability_selector.h" #include "modules/limboai/util/limbo_utility.h" #include "editor/editor_scale.h" @@ -34,6 +35,15 @@ void TaskTree::_update_item(TreeItem *p_item) { if (p_item == nullptr) { return; } + + if (p_item->get_parent()) { + Ref sel = p_item->get_parent()->get_metadata(0); + if (sel.is_valid()) { + p_item->set_custom_draw(0, this, SNAME("_draw_probability")); + p_item->set_cell_mode(0, TreeItem::CELL_MODE_CUSTOM); + } + } + Ref task = p_item->get_metadata(0); ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata."); p_item->set_text(0, task->get_task_name()); @@ -72,8 +82,6 @@ void TaskTree::_update_item(TreeItem *p_item) { if (!warning_text.is_empty()) { p_item->add_button(0, theme_cache.task_warning_icon, 0, false, warning_text); } - - // TODO: Update probabilities. } void TaskTree::_update_tree() { @@ -116,8 +124,13 @@ TreeItem *TaskTree::_find_item(const Ref &p_task) const { return item; } -void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, int p_button_index) { - if (p_button_index == 2) { +void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, MouseButton p_button_index) { + if (p_button_index == MouseButton::LEFT) { + Rect2 rect = get_selected_probability_rect(); + if (rect != Rect2() && rect.has_point(p_pos)) { + emit_signal(SNAME("probability_clicked")); + } + } else if (p_button_index == MouseButton::RIGHT) { emit_signal(SNAME("rmb_pressed"), get_screen_position() + p_pos); } } @@ -126,17 +139,17 @@ void TaskTree::_on_item_selected() { Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed); if (last_selected.is_valid()) { update_task(last_selected); - if (last_selected->is_connected("changed", on_task_changed)) { - last_selected->disconnect("changed", on_task_changed); + if (last_selected->is_connected(SNAME("changed"), on_task_changed)) { + last_selected->disconnect(SNAME("changed"), on_task_changed); } } last_selected = get_selected(); - last_selected->connect("changed", on_task_changed); + last_selected->connect(SNAME("changed"), on_task_changed); emit_signal(SNAME("task_selected"), last_selected); } -void TaskTree::_on_item_double_clicked() { - emit_signal(SNAME("task_double_clicked")); +void TaskTree::_on_item_activated() { + emit_signal(SNAME("task_activated")); } void TaskTree::_on_task_changed() { @@ -153,6 +166,7 @@ void TaskTree::load_bt(const Ref &p_behavior_tree) { bt = p_behavior_tree; tree->clear(); + probability_rect_cache.clear(); if (bt->get_root_task().is_valid()) { _create_tree(bt->get_root_task(), nullptr); } @@ -190,6 +204,37 @@ void TaskTree::deselect() { } } +Rect2 TaskTree::get_selected_probability_rect() const { + if (tree->get_selected() == nullptr) { + return Rect2(); + } + + ObjectID key = tree->get_selected()->get_instance_id(); + if (unlikely(!probability_rect_cache.has(key))) { + return Rect2(); + } else { + return probability_rect_cache[key]; + } +} + +double TaskTree::get_selected_probability_weight() const { + Ref selected = get_selected(); + ERR_FAIL_COND_V(selected.is_null(), 0.0); + Ref probability_selector = selected->get_parent(); + ERR_FAIL_COND_V(probability_selector.is_null(), 0.0); + return probability_selector->get_weight(probability_selector->get_child_index(selected)); +} + +bool TaskTree::selected_has_probability() const { + bool result = false; + Ref selected = get_selected(); + if (selected.is_valid()) { + Ref probability_selector = selected->get_parent(); + result = probability_selector.is_valid(); + } + return result; +} + Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) { if (editable && tree->get_item_at_position(p_point)) { Dictionary drag_data; @@ -241,16 +286,57 @@ void TaskTree::_drop_data_fw(const Point2 &p_point, const Variant &p_data) { } } +void TaskTree::_draw_probability(Object *item_obj, Rect2 rect) { + TreeItem *item = Object::cast_to(item_obj); + if (!item) { + return; + } + Ref sel = item->get_parent()->get_metadata(0); + if (sel.is_null()) { + return; + } + + String text = rtos(Math::snapped(sel->get_probability(item->get_index()) * 100, 0.01)) + "%"; + Size2 text_size = theme_cache.probability_font->get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.probability_font_size); + + Rect2 prob_rect = rect; + prob_rect.position.x += theme_cache.name_font->get_string_size(item->get_text(0), HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.name_font_size).x; + prob_rect.position.x += EDSCALE * 40.0; + prob_rect.size.x = text_size.x + EDSCALE * 12; + prob_rect.position.y += 4 * EDSCALE; + prob_rect.size.y -= 8 * EDSCALE; + probability_rect_cache[item->get_instance_id()] = prob_rect; // Cache rect for later click detection. + + theme_cache.probability_bg->draw(tree->get_canvas_item(), prob_rect); + + Point2 text_pos = prob_rect.position; + text_pos.y += text_size.y + (prob_rect.size.y - text_size.y) * 0.5; + text_pos.y -= theme_cache.probability_font->get_descent(theme_cache.probability_font_size); + text_pos.y = Math::floor(text_pos.y); + + tree->draw_string(theme_cache.probability_font, text_pos, text, HORIZONTAL_ALIGNMENT_CENTER, + prob_rect.size.x, theme_cache.probability_font_size, theme_cache.probability_font_color); +} + void TaskTree::_update_theme_item_cache() { Control::_update_theme_item_cache(); - theme_cache.comment_font = get_theme_font(SNAME("doc_italic"), SNAME("EditorFonts")); + theme_cache.name_font = get_theme_font(SNAME("font")); theme_cache.custom_name_font = get_theme_font(SNAME("bold"), SNAME("EditorFonts")); - // theme_cache.normal_name_font = Ref(nullptr); + theme_cache.comment_font = get_theme_font(SNAME("doc_italic"), SNAME("EditorFonts")); + theme_cache.probability_font = get_theme_font(SNAME("font")); + + theme_cache.name_font_size = get_theme_font_size("font_size"); + theme_cache.probability_font_size = Math::floor(get_theme_font_size("font_size") * 0.9); theme_cache.task_warning_icon = get_theme_icon(SNAME("NodeWarning"), SNAME("EditorIcons")); theme_cache.comment_color = get_theme_color(SNAME("disabled_font_color"), SNAME("Editor")); + theme_cache.probability_font_color = get_theme_color(SNAME("font_color"), SNAME("Editor")); + + theme_cache.probability_bg.instantiate(); + theme_cache.probability_bg->set_bg_color(get_theme_color(SNAME("accent_color"), SNAME("Editor")) * Color(1, 1, 1, 0.25)); + theme_cache.probability_bg->set_corner_radius_all(12.0 * EDSCALE); } void TaskTree::_notification(int p_what) { @@ -272,10 +358,12 @@ void TaskTree::_bind_methods() { ClassDB::bind_method(D_METHOD("_get_drag_data_fw"), &TaskTree::_get_drag_data_fw); ClassDB::bind_method(D_METHOD("_can_drop_data_fw"), &TaskTree::_can_drop_data_fw); ClassDB::bind_method(D_METHOD("_drop_data_fw"), &TaskTree::_drop_data_fw); + ClassDB::bind_method(D_METHOD("_draw_probability"), &TaskTree::_draw_probability); ADD_SIGNAL(MethodInfo("rmb_pressed")); ADD_SIGNAL(MethodInfo("task_selected")); - ADD_SIGNAL(MethodInfo("task_double_clicked")); + ADD_SIGNAL(MethodInfo("task_activated")); + ADD_SIGNAL(MethodInfo("probability_clicked")); ADD_SIGNAL(MethodInfo("task_dragged", PropertyInfo(Variant::OBJECT, "p_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"), PropertyInfo(Variant::OBJECT, "p_to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"), @@ -296,7 +384,7 @@ TaskTree::TaskTree() { tree->set_allow_rmb_select(true); tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected)); tree->connect("item_selected", callable_mp(this, &TaskTree::_on_item_selected)); - tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_double_clicked)); + tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated)); tree->set_drag_forwarding(callable_mp(this, &TaskTree::_get_drag_data_fw), callable_mp(this, &TaskTree::_can_drop_data_fw), callable_mp(this, &TaskTree::_drop_data_fw)); } diff --git a/editor/task_tree.h b/editor/task_tree.h index 6ee93e4..4d65d94 100644 --- a/editor/task_tree.h +++ b/editor/task_tree.h @@ -13,6 +13,7 @@ #include "scene/gui/control.h" #include "scene/gui/tree.h" +#include "scene/resources/style_box.h" class TaskTree : public Control { GDCLASS(TaskTree, Control); @@ -22,15 +23,24 @@ private: Ref bt; Ref last_selected; bool editable; + HashMap probability_rect_cache; struct ThemeCache { Ref comment_font; + Ref name_font; Ref custom_name_font; Ref normal_name_font; + Ref probability_font; + + double name_font_size = 18.0; + double probability_font_size = 16.0; Ref task_warning_icon; Color comment_color; + Color probability_font_color; + + Ref probability_bg; } theme_cache; TreeItem *_create_tree(const Ref &p_task, TreeItem *p_parent, int p_idx = -1); @@ -39,14 +49,16 @@ private: TreeItem *_find_item(const Ref &p_task) const; void _on_item_selected(); - void _on_item_double_clicked(); - void _on_item_mouse_selected(const Vector2 &p_pos, int p_button_index); + void _on_item_activated(); + void _on_item_mouse_selected(const Vector2 &p_pos, MouseButton p_button_index); void _on_task_changed(); Variant _get_drag_data_fw(const Point2 &p_point); bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data) const; void _drop_data_fw(const Point2 &p_point, const Variant &p_data); + void _draw_probability(Object *item_obj, Rect2 rect); + protected: virtual void _update_theme_item_cache() override; @@ -62,6 +74,10 @@ public: Ref get_selected() const; void deselect(); + Rect2 get_selected_probability_rect() const; + double get_selected_probability_weight() const; + bool selected_has_probability() const; + virtual bool editor_can_reload_from_file() { return false; } TaskTree();