Add editor support for BTProbabilitySelector

This commit is contained in:
Serhii Snitsaruk 2023-09-25 18:07:26 +02:00
parent 32cbce6b80
commit 52a70fdee5
5 changed files with 208 additions and 25 deletions

View File

@ -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<BTTask> 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++) {

View File

@ -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<BTTask> 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<BTTask> selected = task_tree->get_selected();
ERR_FAIL_COND(selected == nullptr);
Ref<BTProbabilitySelector> 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<BTTask> &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<BTTask> 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<ButtonGroup> 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);

View File

@ -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<BTTask> &p_task);
void _on_tree_task_double_clicked();
void _on_visibility_changed();
void _on_header_pressed();
void _on_save_pressed();

View File

@ -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<BTProbabilitySelector> 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<BTTask> 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<BTTask> &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<BehaviorTree> &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<BTTask> selected = get_selected();
ERR_FAIL_COND_V(selected.is_null(), 0.0);
Ref<BTProbabilitySelector> 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<BTTask> selected = get_selected();
if (selected.is_valid()) {
Ref<BTProbabilitySelector> 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<TreeItem>(item_obj);
if (!item) {
return;
}
Ref<BTProbabilitySelector> 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<Font>(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));
}

View File

@ -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<BehaviorTree> bt;
Ref<BTTask> last_selected;
bool editable;
HashMap<ObjectID, Rect2> probability_rect_cache;
struct ThemeCache {
Ref<Font> comment_font;
Ref<Font> name_font;
Ref<Font> custom_name_font;
Ref<Font> normal_name_font;
Ref<Font> probability_font;
double name_font_size = 18.0;
double probability_font_size = 16.0;
Ref<Texture2D> task_warning_icon;
Color comment_color;
Color probability_font_color;
Ref<StyleBoxFlat> probability_bg;
} theme_cache;
TreeItem *_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent, int p_idx = -1);
@ -39,14 +49,16 @@ private:
TreeItem *_find_item(const Ref<BTTask> &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<BTTask> 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();