From 4c580c6b1d5ecb0061f4a78d41b48c1660d1f304 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Fri, 2 Sep 2022 00:20:37 +0200 Subject: [PATCH] Add Editor functionality (rudimentary) * Adding fav tasks * Saving/loading * Popup menu with a number of functions * Header Also: * BTTask: Fix broken get_icon() * BTTask: Initialize parent during set_children() --- SCsub | 1 + bt/bt_task.cpp | 5 +- editor/limbo_ai_editor_plugin.cpp | 473 ++++++++++++++++++++++++++++++ editor/limbo_ai_editor_plugin.h | 108 +++++++ register_types.cpp | 8 + 5 files changed, 593 insertions(+), 2 deletions(-) create mode 100644 editor/limbo_ai_editor_plugin.cpp create mode 100644 editor/limbo_ai_editor_plugin.h diff --git a/SCsub b/SCsub index 38df91a..8651eaf 100644 --- a/SCsub +++ b/SCsub @@ -8,3 +8,4 @@ env.add_source_files(env.modules_sources, "bt/composites/*.cpp") env.add_source_files(env.modules_sources, "bt/actions/*.cpp") env.add_source_files(env.modules_sources, "bt/decorators/*.cpp") env.add_source_files(env.modules_sources, "bt/conditions/*.cpp") +env.add_source_files(env.modules_sources, "editor/*.cpp") diff --git a/bt/bt_task.cpp b/bt/bt_task.cpp index 655de67..777854e 100644 --- a/bt/bt_task.cpp +++ b/bt/bt_task.cpp @@ -42,7 +42,8 @@ void BTTask::_set_children(Array p_children) { children.resize(num_children); for (int i = 0; i < num_children; i++) { Variant task_var = p_children[i]; - const Ref task_ref = task_var; + Ref task_ref = task_var; + task_ref->parent = this; children.set(i, task_var); } } @@ -202,7 +203,7 @@ String BTTask::get_configuration_warning() const { } Ref BTTask::get_icon() const { - return EditorNode::get_singleton()->get_class_icon(_class_name, "Object"); + return EditorNode::get_singleton()->get_class_icon(get_class(), "Object"); } void BTTask::print_tree(int p_initial_tabs) const { diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp new file mode 100644 index 0000000..bdfe73b --- /dev/null +++ b/editor/limbo_ai_editor_plugin.cpp @@ -0,0 +1,473 @@ +/* limbo_ai_editor_plugin.cpp */ + +#ifdef TOOLS_ENABLED + +#include "limbo_ai_editor_plugin.h" + +#include "../bt/composites/bt_parallel.h" +#include "../bt/composites/bt_selector.h" +#include "../bt/composites/bt_sequence.h" +#include "core/class_db.h" +#include "core/io/resource_loader.h" +#include "core/io/resource_saver.h" +#include "core/math/math_defs.h" +#include "core/object.h" +#include "core/os/memory.h" +#include "core/print_string.h" +#include "core/variant.h" +#include "core/vector.h" +#include "editor/editor_node.h" +#include "editor/editor_plugin.h" +#include "scene/gui/box_container.h" +#include "scene/gui/file_dialog.h" +#include "scene/gui/label.h" +#include "scene/gui/popup_menu.h" +#include "scene/gui/separator.h" +#include "scene/gui/tree.h" +#include + +TreeItem *TaskTree::_create_tree(const Ref &p_task, TreeItem *p_parent, int p_idx) { + ERR_FAIL_COND_V(p_task.is_null(), nullptr); + TreeItem *item = tree->create_item(p_parent, p_idx); + item->set_metadata(0, p_task); + // p_task->connect("changed"...) + for (int i = 0; i < p_task->get_child_count(); i++) { + _create_tree(p_task->get_child(i), item); + } + _update_item(item); + return item; +} + +void TaskTree::_update_item(TreeItem *p_item) { + ERR_FAIL_COND_MSG(p_item == nullptr, "Argument \"p_item\" is null."); + 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()); + p_item->set_icon(0, task->get_icon()); + p_item->set_editable(0, false); + + // TODO: Update configuration warning. + + // TODO: Update probabilities. +} + +void TaskTree::_update_tree() { + Ref sel; + if (tree->get_selected()) { + sel = tree->get_selected()->get_metadata(0); + } + + tree->clear(); + if (bt->get_root_task().is_valid()) { + _create_tree(bt->get_root_task(), nullptr); + } + + TreeItem *item = _find_item(sel); + if (item) { + item->select(0); + } +} + +TreeItem *TaskTree::_find_item(const Ref &p_task) const { + if (p_task.is_null()) { + return nullptr; + } + TreeItem *item = tree->get_root(); + List stack; + while (item && item->get_metadata(0) != p_task) { + if (item->get_children()) { + stack.push_back(item->get_children()); + } + item = item->get_next(); + if (item == nullptr && !stack.empty()) { + item = stack.front()->get(); + stack.pop_front(); + } + } + return item; +} + +void TaskTree::_on_item_rmb_selected(const Vector2 &p_pos) { + emit_signal("rmb_pressed", tree->get_global_transform().xform(p_pos)); +} + +void TaskTree::_on_item_selected() { + if (last_selected.is_valid()) { + update_task(last_selected); + } + last_selected = get_selected(); + emit_signal("task_selected", last_selected); +} + +void TaskTree::load_bt(const Ref &p_behavior_tree) { + ERR_FAIL_COND_MSG(p_behavior_tree.is_null(), "Tried to load a null tree."); + bt = p_behavior_tree; + tree->clear(); + if (bt->get_root_task().is_valid()) { + _create_tree(bt->get_root_task(), nullptr); + } +} + +void TaskTree::update_task(const Ref &p_task) { + ERR_FAIL_COND(p_task.is_null()); + TreeItem *item = _find_item(p_task); + if (item) { + _update_item(item); + } +} + +Ref TaskTree::get_selected() const { + if (tree->get_selected()) { + return tree->get_selected()->get_metadata(0); + } + return nullptr; +} + +void TaskTree::deselect() { + TreeItem *sel = tree->get_selected(); + if (sel) { + sel->deselect(0); + } +} + +void TaskTree::_bind_methods() { + ClassDB::bind_method(D_METHOD("_on_item_rmb_selected"), &TaskTree::_on_item_rmb_selected); + ClassDB::bind_method(D_METHOD("_on_item_selected"), &TaskTree::_on_item_selected); + ClassDB::bind_method(D_METHOD("load_bt", "p_behavior_tree"), &TaskTree::load_bt); + ClassDB::bind_method(D_METHOD("get_bt"), &TaskTree::get_bt); + ClassDB::bind_method(D_METHOD("update_tree"), &TaskTree::update_tree); + ClassDB::bind_method(D_METHOD("update_task", "p_task"), &TaskTree::update_task); + ClassDB::bind_method(D_METHOD("get_selected"), &TaskTree::get_selected); + ClassDB::bind_method(D_METHOD("deselect"), &TaskTree::deselect); + + ADD_SIGNAL(MethodInfo("rmb_pressed")); + ADD_SIGNAL(MethodInfo("task_selected")); +} + +TaskTree::TaskTree() { + tree = memnew(Tree); + add_child(tree); + tree->set_columns(2); + tree->set_column_expand(0, true); + tree->set_column_expand(1, false); + tree->set_column_min_width(1, 64); + tree->set_anchor(MARGIN_RIGHT, ANCHOR_END); + tree->set_anchor(MARGIN_BOTTOM, ANCHOR_END); + tree->set_allow_rmb_select(true); + tree->connect("item_rmb_selected", this, "_on_item_rmb_selected"); + tree->connect("item_selected", this, "_on_item_selected"); +} + +TaskTree::~TaskTree() { +} + +//////////////////////////////////////////////////////////////////////////////// + +void LimboAIEditor::_add_task(const Ref &p_prototype) { + ERR_FAIL_COND(p_prototype.is_null()); + Ref parent = task_tree->get_selected(); + if (parent.is_null()) { + parent = task_tree->get_bt()->get_root_task(); + } + if (parent.is_null()) { + task_tree->get_bt()->set_root_task(p_prototype->clone()); + } else { + parent->add_child(p_prototype->clone()); + } + task_tree->update_tree(); +} + +void LimboAIEditor::_update_header() { + String text = task_tree->get_bt()->get_path(); + if (text.empty()) { + text = TTR("New Behavior Tree"); + } + header->set_text(text); + header->set_icon(editor->get_object_icon(task_tree->get_bt().ptr(), "BehaviorTree")); +} + +void LimboAIEditor::_new_bt() { + BehaviorTree *bt = memnew(BehaviorTree); + bt->set_root_task(memnew(BTSelector)); + task_tree->load_bt(bt); + _update_header(); +} + +void LimboAIEditor::_save_bt(String p_path) { + ERR_FAIL_COND_MSG(p_path.empty(), "Empty p_path"); + ERR_FAIL_COND_MSG(task_tree->get_bt().is_null(), "Behavior Tree is null."); + task_tree->get_bt()->set_path(p_path, true); + ResourceSaver::save(p_path, task_tree->get_bt(), ResourceSaver::FLAG_CHANGE_PATH); + _update_header(); +} + +void LimboAIEditor::_load_bt(String p_path) { + ERR_FAIL_COND_MSG(p_path.empty(), "Empty p_path"); + task_tree->load_bt(ResourceLoader::load(p_path, "BehaviorTree")); + _update_header(); +} + +void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) { + menu->set_size(Size2(1, 1)); + menu->set_position(p_menu_pos); + + menu->clear(); + menu->add_icon_item(get_icon("Remove", "EditorIcons"), TTR("Remove"), ACTION_REMOVE); + menu->add_separator(); + menu->add_icon_item(get_icon("MoveUp", "EditorIcons"), TTR("Move Up"), ACTION_MOVE_UP); + menu->add_icon_item(get_icon("MoveDown", "EditorIcons"), TTR("Move Down"), ACTION_MOVE_DOWN); + menu->add_icon_item(get_icon("Duplicate", "EditorIcons"), TTR("Duplicate"), ACTION_DUPLICATE); + menu->add_icon_item(get_icon("NewRoot", "EditorIcons"), TTR("Make Root"), ACTION_MAKE_ROOT); + + menu->popup(); +} + +void LimboAIEditor::_on_action_selected(int p_id) { + switch (p_id) { + case ACTION_REMOVE: { + Ref sel = task_tree->get_selected(); + if (sel.is_valid()) { + if (sel->get_parent().is_null()) { + task_tree->get_bt()->set_root_task(nullptr); + } else { + sel->get_parent()->remove_child(sel); + } + task_tree->update_tree(); + editor->edit_node(nullptr); + } + } break; + case ACTION_MOVE_UP: { + Ref sel = task_tree->get_selected(); + if (sel.is_valid() && sel->get_parent().is_valid()) { + Ref parent = sel->get_parent(); + int idx = parent->get_child_index(sel); + if (idx > 0 && idx < parent->get_child_count()) { + parent->remove_child(sel); + parent->add_child_at_index(sel, idx - 1); + task_tree->update_tree(); + } + } + } break; + case ACTION_MOVE_DOWN: { + Ref sel = task_tree->get_selected(); + if (sel.is_valid() && sel->get_parent().is_valid()) { + Ref parent = sel->get_parent(); + int idx = parent->get_child_index(sel); + if (idx >= 0 && idx < (parent->get_child_count() - 1)) { + parent->remove_child(sel); + parent->add_child_at_index(sel, idx + 1); + task_tree->update_tree(); + } + } + } break; + case ACTION_DUPLICATE: { + Ref sel = task_tree->get_selected(); + if (sel.is_valid()) { + Ref parent = sel->get_parent(); + if (parent.is_null()) { + parent = sel; + } + parent->add_child(sel->clone()); + task_tree->update_tree(); + } + } break; + case ACTION_MAKE_ROOT: { + Ref sel = task_tree->get_selected(); + if (sel.is_valid() && task_tree->get_bt()->get_root_task() != sel) { + Ref parent = sel->get_parent(); + ERR_FAIL_COND(parent.is_null()); + parent->remove_child(sel); + Ref old_root = task_tree->get_bt()->get_root_task(); + task_tree->get_bt()->set_root_task(sel); + sel->add_child(old_root); + task_tree->update_tree(); + } + } break; + } +} + +void LimboAIEditor::_on_task_selected(const Ref &p_task) const { + editor->edit_resource(p_task); +} + +void LimboAIEditor::_on_visibility_changed() const { + if (is_visible()) { + Ref sel = task_tree->get_selected(); + if (sel.is_valid()) { + editor->edit_resource(sel); + } else { + editor->edit_resource(task_tree->get_bt()); + } + } +} + +void LimboAIEditor::_on_header_pressed() const { + task_tree->deselect(); + editor->edit_resource(task_tree->get_bt()); +} + +void LimboAIEditor::_on_save_pressed() { + String path = task_tree->get_bt()->get_path(); + if (path.empty()) { + save_dialog->popup_centered_ratio(); + } else { + _save_bt(path); + } +} + +void LimboAIEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_add_task", "p_task"), &LimboAIEditor::_add_task); + ClassDB::bind_method(D_METHOD("_on_tree_rmb"), &LimboAIEditor::_on_tree_rmb); + ClassDB::bind_method(D_METHOD("_on_action_selected", "p_id"), &LimboAIEditor::_on_action_selected); + ClassDB::bind_method(D_METHOD("_on_task_selected", "p_task"), &LimboAIEditor::_on_task_selected); + ClassDB::bind_method(D_METHOD("_on_visibility_changed"), &LimboAIEditor::_on_visibility_changed); + ClassDB::bind_method(D_METHOD("_on_header_pressed"), &LimboAIEditor::_on_header_pressed); + ClassDB::bind_method(D_METHOD("_on_save_pressed"), &LimboAIEditor::_on_save_pressed); + ClassDB::bind_method(D_METHOD("_new_bt"), &LimboAIEditor::_new_bt); + ClassDB::bind_method(D_METHOD("_save_bt", "p_path"), &LimboAIEditor::_save_bt); + ClassDB::bind_method(D_METHOD("_load_bt", "p_path"), &LimboAIEditor::_load_bt); +} + +LimboAIEditor::LimboAIEditor(EditorNode *p_editor) { + editor = p_editor; + + save_dialog = memnew(FileDialog); + add_child(save_dialog); + save_dialog->set_mode(FileDialog::MODE_SAVE_FILE); + save_dialog->set_title("Save Behavior Tree"); + save_dialog->add_filter("*.tres"); + save_dialog->connect("file_selected", this, "_save_bt"); + save_dialog->hide(); + + load_dialog = memnew(FileDialog); + add_child(load_dialog); + load_dialog->set_mode(FileDialog::MODE_OPEN_FILE); + load_dialog->set_title("Load Behavior Tree"); + load_dialog->add_filter("*.tres"); + load_dialog->connect("file_selected", this, "_load_bt"); + load_dialog->hide(); + + VBoxContainer *vbox = memnew(VBoxContainer); + vbox->set_anchor(MARGIN_RIGHT, ANCHOR_END); + vbox->set_anchor(MARGIN_BOTTOM, ANCHOR_END); + add_child(vbox); + + HBoxContainer *panel = memnew(HBoxContainer); + vbox->add_child(panel); + + Button *selector_btn = memnew(Button); + selector_btn->set_text(TTR("Selector")); + selector_btn->set_tooltip(TTR("Add Selector task.")); + selector_btn->set_icon(editor->get_class_icon("BTSelector")); + selector_btn->set_flat(true); + selector_btn->set_focus_mode(Control::FOCUS_NONE); + selector_btn->connect("pressed", this, "_add_task", varray(Ref(memnew(BTSelector)))); + panel->add_child(selector_btn); + + Button *sequence_btn = memnew(Button); + sequence_btn->set_text(TTR("Sequence")); + sequence_btn->set_tooltip(TTR("Add Sequence task.")); + sequence_btn->set_icon(editor->get_class_icon("BTSequence")); + sequence_btn->set_flat(true); + sequence_btn->set_focus_mode(Control::FOCUS_NONE); + sequence_btn->connect("pressed", this, "_add_task", varray(Ref(memnew(BTSequence)))); + panel->add_child(sequence_btn); + + Button *parallel_btn = memnew(Button); + parallel_btn->set_text(TTR("Parallel")); + parallel_btn->set_tooltip(TTR("Add Parallel task.")); + parallel_btn->set_icon(editor->get_class_icon("BTParallel")); + parallel_btn->set_flat(true); + parallel_btn->set_focus_mode(Control::FOCUS_NONE); + parallel_btn->connect("pressed", this, "_add_task", varray(Ref(memnew(BTParallel)))); + panel->add_child(parallel_btn); + + panel->add_child(memnew(VSeparator)); + + Button *new_btn = memnew(Button); + panel->add_child(new_btn); + new_btn->set_text(TTR("New")); + new_btn->set_tooltip(TTR("Create new behavior tree.")); + new_btn->set_icon(editor->get_gui_base()->get_icon("New", "EditorIcons")); + new_btn->set_flat(true); + new_btn->set_focus_mode(Control::FOCUS_NONE); + new_btn->connect("pressed", this, "_new_bt"); + + Button *load_btn = memnew(Button); + panel->add_child(load_btn); + load_btn->set_text(TTR("Load")); + load_btn->set_tooltip(TTR("Load behavior tree.")); + load_btn->set_icon(editor->get_gui_base()->get_icon("Load", "EditorIcons")); + load_btn->set_flat(true); + load_btn->set_focus_mode(Control::FOCUS_NONE); + load_btn->connect("pressed", load_dialog, "popup_centered_ratio"); + + Button *save_btn = memnew(Button); + panel->add_child(save_btn); + save_btn->set_text(TTR("Save")); + save_btn->set_tooltip(TTR("Save current behavior tree.")); + save_btn->set_icon(editor->get_gui_base()->get_icon("Save", "EditorIcons")); + save_btn->set_flat(true); + save_btn->set_focus_mode(Control::FOCUS_NONE); + save_btn->connect("pressed", this, "_on_save_pressed"); + + panel->add_child(memnew(VSeparator)); + + header = memnew(Button); + vbox->add_child(header); + header->set_text_align(Button::ALIGN_LEFT); + header->add_constant_override("hseparation", 8); + header->connect("pressed", this, "_on_header_pressed"); + + task_tree = memnew(TaskTree); + task_tree->set_v_size_flags(SIZE_EXPAND_FILL); + task_tree->set_h_size_flags(SIZE_EXPAND_FILL); + vbox->add_child(task_tree); + task_tree->connect("rmb_pressed", this, "_on_tree_rmb"); + task_tree->connect("task_selected", this, "_on_task_selected"); + + menu = memnew(PopupMenu); + add_child(menu); + menu->connect("id_pressed", this, "_on_action_selected"); + menu->set_hide_on_window_lose_focus(true); + + BehaviorTree *bt = memnew(BehaviorTree); + BTSelector *seq = memnew(BTSelector); + bt->set_root_task(seq); + + task_tree->load_bt(bt); + _update_header(); + + task_tree->connect("visibility_changed", this, "_on_visibility_changed"); +} + +LimboAIEditor::~LimboAIEditor() { +} + +//////////////////////////////////////////////////////////////////////////////// + +const Ref LimboAIEditorPlugin::get_icon() const { + // TODO: + return nullptr; +} + +void LimboAIEditorPlugin::_notification(int p_notification) { + // print_line(vformat("NOTIFICATION: %d", p_notification)); +} + +void LimboAIEditorPlugin::make_visible(bool p_visible) { + limbo_ai_editor->set_visible(p_visible); +} + +LimboAIEditorPlugin::LimboAIEditorPlugin(EditorNode *p_editor) { + editor = p_editor; + limbo_ai_editor = memnew(LimboAIEditor(p_editor)); + limbo_ai_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL); + editor->get_viewport()->add_child(limbo_ai_editor); + limbo_ai_editor->hide(); +} + +LimboAIEditorPlugin::~LimboAIEditorPlugin() { +} + +#endif // TOOLS_ENABLED \ No newline at end of file diff --git a/editor/limbo_ai_editor_plugin.h b/editor/limbo_ai_editor_plugin.h new file mode 100644 index 0000000..38aa80d --- /dev/null +++ b/editor/limbo_ai_editor_plugin.h @@ -0,0 +1,108 @@ +/* limbo_ai_editor_plugin.h */ + +#ifndef LIMBO_AI_EDITOR_PLUGIN_H +#define LIMBO_AI_EDITOR_PLUGIN_H +#ifdef TOOLS_ENABLED + +#include "../bt/behavior_tree.h" +#include "core/object.h" +#include "editor/editor_node.h" +#include "editor/editor_plugin.h" +#include "scene/gui/file_dialog.h" +#include "scene/gui/popup_menu.h" +#include "scene/gui/tree.h" + +class TaskTree : public Control { + GDCLASS(TaskTree, Control); + +private: + Tree *tree; + Ref bt; + Ref last_selected; + + TreeItem *_create_tree(const Ref &p_task, TreeItem *p_parent, int p_idx = -1); + void _update_item(TreeItem *p_item); + void _update_tree(); + TreeItem *_find_item(const Ref &p_task) const; + + void _on_item_selected(); + void _on_item_rmb_selected(const Vector2 &p_pos); + +protected: + static void _bind_methods(); + +public: + void load_bt(const Ref &p_behavior_tree); + Ref get_bt() const { return bt; } + void update_tree() { _update_tree(); } + void update_task(const Ref &p_task); + Ref get_selected() const; + void deselect(); + + TaskTree(); + ~TaskTree(); +}; + +class LimboAIEditor : public Control { + GDCLASS(LimboAIEditor, Control); + +private: + enum Action { + ACTION_REMOVE, + ACTION_MOVE_UP, + ACTION_MOVE_DOWN, + ACTION_DUPLICATE, + ACTION_MAKE_ROOT, + }; + + EditorNode *editor; + Button *header; + TaskTree *task_tree; + PopupMenu *menu; + FileDialog *save_dialog; + FileDialog *load_dialog; + + void _add_task(const Ref &p_prototype); + void _update_header(); + void _new_bt(); + void _save_bt(String p_path); + void _load_bt(String p_path); + + void _on_tree_rmb(const Vector2 &p_menu_pos); + void _on_action_selected(int p_id); + void _on_task_selected(const Ref &p_task) const; + void _on_visibility_changed() const; + void _on_header_pressed() const; + void _on_save_pressed(); + +protected: + static void _bind_methods(); + +public: + LimboAIEditor(EditorNode *p_editor); + ~LimboAIEditor(); +}; + +class LimboAIEditorPlugin : public EditorPlugin { + GDCLASS(LimboAIEditorPlugin, EditorPlugin); + +private: + EditorNode *editor; + LimboAIEditor *limbo_ai_editor; + +protected: + void _notification(int p_notification); + +public: + virtual String get_name() const { return "LimboAI"; } + virtual const Ref get_icon() const; + bool has_main_screen() const { return true; } + virtual void make_visible(bool p_visible); + + LimboAIEditorPlugin(EditorNode *p_editor); + ~LimboAIEditorPlugin(); +}; + +#endif // TOOLS_ENABLED + +#endif // LIMBO_AI_EDITOR_PLUGIN_H \ No newline at end of file diff --git a/register_types.cpp b/register_types.cpp index 85ce121..19cc263 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -36,6 +36,10 @@ #include "limbo_string_names.h" #include "limbo_utility.h" +#ifdef TOOLS_ENABLED +#include "editor/limbo_ai_editor_plugin.h" +#endif + void register_limboai_types() { ClassDB::register_class(); ClassDB::register_class(); @@ -72,6 +76,10 @@ void register_limboai_types() { ClassDB::register_class(); +#ifdef TOOLS_ENABLED + EditorPlugins::add_by_type(); +#endif + LimboStringNames::create(); }