Merge pull request #4 from limbonaut/change-type

Implement "Change Type" action with a popup for tasks in the behavior tree editor
This commit is contained in:
Serhii Snitsaruk 2023-12-15 00:14:27 +01:00 committed by GitHub
commit 0f902041e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 132 additions and 21 deletions

View File

@ -68,31 +68,35 @@ void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
_mark_as_dirty(true); _mark_as_dirty(true);
} }
void LimboAIEditor::_add_task_by_class_or_path(String p_class_or_path) { Ref<BTTask> LimboAIEditor::_create_task_by_class_or_path(const String &p_class_or_path) const {
Ref<BTTask> task; Ref<BTTask> ret;
if (p_class_or_path.begins_with("res:")) { if (p_class_or_path.begins_with("res:")) {
Ref<Script> s = ResourceLoader::load(p_class_or_path, "Script"); Ref<Script> s = ResourceLoader::load(p_class_or_path, "Script");
ERR_FAIL_COND_MSG(s.is_null() || !s->is_valid(), vformat("LimboAI: Failed to instantiate task. Bad script: %s", p_class_or_path)); ERR_FAIL_COND_V_MSG(s.is_null() || !s->is_valid(), nullptr, vformat("LimboAI: Failed to instantiate task. Bad script: %s", p_class_or_path));
Variant inst = ClassDB::instantiate(s->get_instance_base_type()); Variant inst = ClassDB::instantiate(s->get_instance_base_type());
ERR_FAIL_COND_MSG(inst.is_zero(), vformat("LimboAI: Failed to instantiate base type \"%s\".", s->get_instance_base_type())); ERR_FAIL_COND_V_MSG(inst.is_zero(), nullptr, vformat("LimboAI: Failed to instantiate base type \"%s\".", s->get_instance_base_type()));
if (unlikely(!((Object *)inst)->is_class("BTTask"))) { if (unlikely(!((Object *)inst)->is_class("BTTask"))) {
if (!inst.is_ref_counted()) { if (!inst.is_ref_counted()) {
memdelete((Object *)inst); memdelete((Object *)inst);
} }
ERR_PRINT(vformat("LimboAI: Failed to instantiate task. Script is not a BTTask: %s", p_class_or_path)); ERR_PRINT(vformat("LimboAI: Failed to instantiate task. Script is not a BTTask: %s", p_class_or_path));
return; return nullptr;
} }
if (inst && s.is_valid()) { if (inst && s.is_valid()) {
((Object *)inst)->set_script(s); ((Object *)inst)->set_script(s);
task = inst; ret = inst;
} }
} else { } else {
task = ClassDB::instantiate(p_class_or_path); ret = ClassDB::instantiate(p_class_or_path);
} }
_add_task(task); return ret;
}
void LimboAIEditor::_add_task_by_class_or_path(const String &p_class_or_path) {
_add_task(_create_task_by_class_or_path(p_class_or_path));
} }
void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) { void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) {
@ -277,6 +281,7 @@ void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) {
menu->add_icon_item(theme_cache.percent_icon, TTR("Edit Probability"), ACTION_EDIT_PROBABILITY); menu->add_icon_item(theme_cache.percent_icon, 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_shortcut(theme_cache.rename_task_icon, ED_GET_SHORTCUT("limbo_ai/rename_task"), ACTION_RENAME);
menu->add_icon_item(theme_cache.change_type_icon, TTR("Change Type"), ACTION_CHANGE_TYPE);
menu->add_icon_item(theme_cache.edit_script_icon, TTR("Edit Script"), ACTION_EDIT_SCRIPT); 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->add_icon_item(theme_cache.open_doc_icon, TTR("Open Documentation"), ACTION_OPEN_DOC);
menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script().is_null()); menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script().is_null());
@ -317,6 +322,12 @@ void LimboAIEditor::_action_selected(int p_id) {
rename_edit->select_all(); rename_edit->select_all();
rename_edit->grab_focus(); rename_edit->grab_focus();
} break; } break;
case ACTION_CHANGE_TYPE: {
change_type_palette->clear_filter();
change_type_palette->refresh();
Rect2 rect = Rect2(get_global_mouse_position(), Size2(400.0, 600.0) * EDSCALE);
change_type_popup->popup(rect);
} break;
case ACTION_EDIT_PROBABILITY: { case ACTION_EDIT_PROBABILITY: {
Rect2 rect = task_tree->get_selected_probability_rect(); Rect2 rect = task_tree->get_selected_probability_rect();
ERR_FAIL_COND(rect == Rect2()); ERR_FAIL_COND(rect == Rect2());
@ -670,6 +681,62 @@ void LimboAIEditor::_on_resources_reload(const Vector<String> &p_resources) {
} }
} }
void LimboAIEditor::_task_type_selected(const String &p_class_or_path) {
change_type_popup->hide();
Ref<BTTask> selected_task = task_tree->get_selected();
ERR_FAIL_COND(selected_task.is_null());
Ref<BTTask> new_task = _create_task_by_class_or_path(p_class_or_path);
ERR_FAIL_COND_MSG(new_task.is_null(), "LimboAI: Unable to construct task.");
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Change BT task type"));
undo_redo->add_do_method(this, SNAME("_replace_task"), selected_task, new_task);
undo_redo->add_undo_method(this, SNAME("_replace_task"), new_task, selected_task);
undo_redo->add_do_method(task_tree, SNAME("update_tree"));
undo_redo->add_undo_method(task_tree, SNAME("update_tree"));
undo_redo->commit_action();
_mark_as_dirty(true);
}
void LimboAIEditor::_replace_task(const Ref<BTTask> &p_task, const Ref<BTTask> &p_by_task) {
ERR_FAIL_COND(p_task.is_null());
ERR_FAIL_COND(p_by_task.is_null());
ERR_FAIL_COND(p_by_task->get_child_count() > 0);
ERR_FAIL_COND(p_by_task->get_parent().is_valid());
while (p_task->get_child_count() > 0) {
Ref<BTTask> child = p_task->get_child(0);
p_task->remove_child_at_index(0);
p_by_task->add_child(child);
}
p_by_task->set_custom_name(p_task->get_custom_name());
Ref<BTTask> parent = p_task->get_parent();
if (parent.is_null()) {
// Assuming root task is replaced.
ERR_FAIL_COND(task_tree->get_bt().is_null());
ERR_FAIL_COND(task_tree->get_bt()->get_root_task() != p_task);
task_tree->get_bt()->set_root_task(p_by_task);
} else {
// Non-root task is replaced.
int idx = p_task->get_index();
double weight = 0.0;
Ref<BTProbabilitySelector> probability_selector = parent;
if (probability_selector.is_valid()) {
weight = probability_selector->get_weight(idx);
}
parent->remove_child(p_task);
parent->add_child_at_index(p_by_task, idx);
if (probability_selector.is_valid()) {
probability_selector->set_weight(idx, weight);
}
}
}
void LimboAIEditor::_reload_modified() { void LimboAIEditor::_reload_modified() {
for (const String &fn : disk_changed_files) { for (const String &fn : disk_changed_files) {
Ref<Resource> res = ResourceCache::get_ref(fn); Ref<Resource> res = ResourceCache::get_ref(fn);
@ -807,16 +874,17 @@ void LimboAIEditor::_update_banners() {
void LimboAIEditor::_update_theme_item_cache() { void LimboAIEditor::_update_theme_item_cache() {
Control::_update_theme_item_cache(); Control::_update_theme_item_cache();
theme_cache.duplicate_task_icon = get_theme_icon(SNAME("Duplicate"), SNAME("EditorIcons")); theme_cache.duplicate_task_icon = get_editor_theme_icon(SNAME("Duplicate"));
theme_cache.edit_script_icon = get_theme_icon(SNAME("Script"), SNAME("EditorIcons")); theme_cache.edit_script_icon = get_editor_theme_icon(SNAME("Script"));
theme_cache.make_root_icon = get_theme_icon(SNAME("NewRoot"), SNAME("EditorIcons")); theme_cache.make_root_icon = get_editor_theme_icon(SNAME("NewRoot"));
theme_cache.move_task_down_icon = get_theme_icon(SNAME("MoveDown"), SNAME("EditorIcons")); theme_cache.move_task_down_icon = get_editor_theme_icon(SNAME("MoveDown"));
theme_cache.move_task_up_icon = get_theme_icon(SNAME("MoveUp"), SNAME("EditorIcons")); theme_cache.move_task_up_icon = get_editor_theme_icon(SNAME("MoveUp"));
theme_cache.open_debugger_icon = get_theme_icon(SNAME("Debug"), SNAME("EditorIcons")); theme_cache.open_debugger_icon = get_editor_theme_icon(SNAME("Debug"));
theme_cache.open_doc_icon = get_theme_icon(SNAME("Help"), SNAME("EditorIcons")); theme_cache.open_doc_icon = get_editor_theme_icon(SNAME("Help"));
theme_cache.percent_icon = get_theme_icon(SNAME("LimboPercent"), SNAME("EditorIcons")); theme_cache.percent_icon = get_editor_theme_icon(SNAME("LimboPercent"));
theme_cache.remove_task_icon = get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")); theme_cache.remove_task_icon = get_editor_theme_icon(SNAME("Remove"));
theme_cache.rename_task_icon = get_theme_icon(SNAME("Rename"), SNAME("EditorIcons")); theme_cache.rename_task_icon = get_editor_theme_icon(SNAME("Rename"));
theme_cache.change_type_icon = get_editor_theme_icon(SNAME("Reload"));
} }
void LimboAIEditor::_notification(int p_what) { void LimboAIEditor::_notification(int p_what) {
@ -862,6 +930,7 @@ void LimboAIEditor::_bind_methods() {
ClassDB::bind_method(D_METHOD("edit_bt", "p_behavior_tree", "p_force_refresh"), &LimboAIEditor::edit_bt, Variant(false)); ClassDB::bind_method(D_METHOD("edit_bt", "p_behavior_tree", "p_force_refresh"), &LimboAIEditor::edit_bt, Variant(false));
ClassDB::bind_method(D_METHOD("_reload_modified"), &LimboAIEditor::_reload_modified); ClassDB::bind_method(D_METHOD("_reload_modified"), &LimboAIEditor::_reload_modified);
ClassDB::bind_method(D_METHOD("_resave_modified"), &LimboAIEditor::_resave_modified); ClassDB::bind_method(D_METHOD("_resave_modified"), &LimboAIEditor::_resave_modified);
ClassDB::bind_method(D_METHOD("_replace_task", "p_task", "p_by_task"), &LimboAIEditor::_replace_task);
} }
LimboAIEditor::LimboAIEditor() { LimboAIEditor::LimboAIEditor() {
@ -1020,6 +1089,25 @@ LimboAIEditor::LimboAIEditor() {
task_palette->hide(); task_palette->hide();
hsc->add_child(task_palette); hsc->add_child(task_palette);
change_type_popup = memnew(PopupPanel);
add_child(change_type_popup);
{
VBoxContainer *change_type_vbox = memnew(VBoxContainer);
change_type_popup->add_child(change_type_vbox);
Label *change_type_title = memnew(Label);
change_type_vbox->add_child(change_type_title);
change_type_title->set_theme_type_variation("HeaderSmall");
change_type_title->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
change_type_title->set_text(TTR("Choose New Type"));
change_type_palette = memnew(TaskPalette);
change_type_vbox->add_child(change_type_palette);
change_type_palette->use_dialog_mode();
change_type_palette->connect("task_selected", callable_mp(this, &LimboAIEditor::_task_type_selected));
change_type_palette->set_v_size_flags(SIZE_EXPAND_FILL);
}
banners = memnew(VBoxContainer); banners = memnew(VBoxContainer);
vbox->add_child(banners); vbox->add_child(banners);

View File

@ -31,6 +31,7 @@
#include "scene/gui/line_edit.h" #include "scene/gui/line_edit.h"
#include "scene/gui/margin_container.h" #include "scene/gui/margin_container.h"
#include "scene/gui/panel_container.h" #include "scene/gui/panel_container.h"
#include "scene/gui/popup.h"
#include "scene/gui/popup_menu.h" #include "scene/gui/popup_menu.h"
#include "scene/gui/split_container.h" #include "scene/gui/split_container.h"
#include "scene/gui/tree.h" #include "scene/gui/tree.h"
@ -42,6 +43,7 @@ class LimboAIEditor : public Control {
private: private:
enum Action { enum Action {
ACTION_RENAME, ACTION_RENAME,
ACTION_CHANGE_TYPE,
ACTION_EDIT_PROBABILITY, ACTION_EDIT_PROBABILITY,
ACTION_EDIT_SCRIPT, ACTION_EDIT_SCRIPT,
ACTION_OPEN_DOC, ACTION_OPEN_DOC,
@ -70,6 +72,7 @@ private:
Ref<Texture2D> percent_icon; Ref<Texture2D> percent_icon;
Ref<Texture2D> remove_task_icon; Ref<Texture2D> remove_task_icon;
Ref<Texture2D> rename_task_icon; Ref<Texture2D> rename_task_icon;
Ref<Texture2D> change_type_icon;
} theme_cache; } theme_cache;
Vector<Ref<BehaviorTree>> history; Vector<Ref<BehaviorTree>> history;
@ -91,6 +94,9 @@ private:
Button *weight_mode; Button *weight_mode;
Button *percent_mode; Button *percent_mode;
PopupPanel *change_type_popup;
TaskPalette *change_type_palette;
FileDialog *save_dialog; FileDialog *save_dialog;
FileDialog *load_dialog; FileDialog *load_dialog;
Button *history_back; Button *history_back;
@ -110,7 +116,8 @@ private:
HashSet<String> disk_changed_files; HashSet<String> disk_changed_files;
void _add_task(const Ref<BTTask> &p_task); void _add_task(const Ref<BTTask> &p_task);
void _add_task_by_class_or_path(String p_class_or_path); Ref<BTTask> _create_task_by_class_or_path(const String &p_class_or_path) const;
void _add_task_by_class_or_path(const String &p_class_or_path);
void _remove_task(const Ref<BTTask> &p_task); void _remove_task(const Ref<BTTask> &p_task);
_FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); } _FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); }
void _update_header() const; void _update_header() const;
@ -146,6 +153,9 @@ private:
void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type); void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type);
void _on_resources_reload(const Vector<String> &p_resources); void _on_resources_reload(const Vector<String> &p_resources);
void _task_type_selected(const String &p_class_or_path);
void _replace_task(const Ref<BTTask> &p_task, const Ref<BTTask> &p_by_task);
virtual void shortcut_input(const Ref<InputEvent> &p_event) override; virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
protected: protected:

View File

@ -179,6 +179,10 @@ void TaskPalette::_on_task_button_pressed(const String &p_task) {
} }
void TaskPalette::_on_task_button_rmb(const String &p_task) { void TaskPalette::_on_task_button_rmb(const String &p_task) {
if (dialog_mode) {
return;
}
ERR_FAIL_COND(p_task.is_empty()); ERR_FAIL_COND(p_task.is_empty());
context_task = p_task; context_task = p_task;
@ -415,14 +419,20 @@ void TaskPalette::refresh() {
sec->connect(SNAME("task_button_pressed"), callable_mp(this, &TaskPalette::_on_task_button_pressed)); sec->connect(SNAME("task_button_pressed"), callable_mp(this, &TaskPalette::_on_task_button_pressed));
sec->connect(SNAME("task_button_rmb"), callable_mp(this, &TaskPalette::_on_task_button_rmb)); sec->connect(SNAME("task_button_rmb"), callable_mp(this, &TaskPalette::_on_task_button_rmb));
sections->add_child(sec); sections->add_child(sec);
sec->set_collapsed(collapsed_sections.has(cat)); sec->set_collapsed(!dialog_mode && collapsed_sections.has(cat));
} }
if (!filter_edit->get_text().is_empty()) { if (!dialog_mode && !filter_edit->get_text().is_empty()) {
_apply_filter(filter_edit->get_text()); _apply_filter(filter_edit->get_text());
} }
} }
void TaskPalette::use_dialog_mode() {
tool_filters->hide();
tool_refresh->hide();
dialog_mode = true;
}
void TaskPalette::_update_theme_item_cache() { void TaskPalette::_update_theme_item_cache() {
PanelContainer::_update_theme_item_cache(); PanelContainer::_update_theme_item_cache();

View File

@ -118,6 +118,7 @@ private:
VBoxContainer *category_list; VBoxContainer *category_list;
String context_task; String context_task;
bool dialog_mode = false;
void _menu_action_selected(int p_id); void _menu_action_selected(int p_id);
void _on_task_button_pressed(const String &p_task); void _on_task_button_pressed(const String &p_task);
@ -149,6 +150,8 @@ protected:
public: public:
void refresh(); void refresh();
void use_dialog_mode();
void clear_filter() { filter_edit->set_text(""); }
TaskPalette(); TaskPalette();
~TaskPalette(); ~TaskPalette();