From 55e0b7404cd3172dc613c5755e22b09d3954b4a5 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Sat, 23 Sep 2023 20:29:17 +0200 Subject: [PATCH 1/8] Add BTProbabilitySelector class --- .../composites/bt_probability_selector.cpp | 101 ++++++++++++++++++ bt/tasks/composites/bt_probability_selector.h | 55 ++++++++++ config.py | 1 + register_types.cpp | 2 + 4 files changed, 159 insertions(+) create mode 100644 bt/tasks/composites/bt_probability_selector.cpp create mode 100644 bt/tasks/composites/bt_probability_selector.h diff --git a/bt/tasks/composites/bt_probability_selector.cpp b/bt/tasks/composites/bt_probability_selector.cpp new file mode 100644 index 0000000..74e12fd --- /dev/null +++ b/bt/tasks/composites/bt_probability_selector.cpp @@ -0,0 +1,101 @@ +/** + * bt_probability_selector.cpp + * ============================================================================= + * Copyright 2021-2023 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. + * ============================================================================= + */ + +#include "bt_probability_selector.h" + +#include "modules/limboai/bt/tasks/bt_task.h" + +#include "core/error/error_macros.h" + +double BTProbabilitySelector::get_weight(int p_index) const { + return _get_weight(p_index); +} + +void BTProbabilitySelector::set_weight(int p_index, double p_weight) { + _set_weight(p_index, p_weight); +} + +double BTProbabilitySelector::get_probability(int p_index) const { + ERR_FAIL_INDEX_V(p_index, get_child_count(), 0.0); + return _get_weight(p_index) / _get_total_weight(); +} + +void BTProbabilitySelector::set_probability(int p_index, double p_probability) { + ERR_FAIL_INDEX(p_index, get_child_count()); + ERR_FAIL_COND(p_probability < 0.0); + ERR_FAIL_COND(p_probability > 1.0); + ERR_FAIL_COND(p_probability > 0.99 && get_child_count() > 1); + + double others_total = _get_total_weight() - _get_weight(p_index); + double others_probability = 1.0 - p_probability; + double new_total = others_total / others_probability; + _set_weight(p_index, new_total - others_total); +} + +void BTProbabilitySelector::_enter() { + _select_task(); +} + +void BTProbabilitySelector::_exit() { + failed_tasks.clear(); + selected_task.unref(); +} + +BT::Status BTProbabilitySelector::_tick(double p_delta) { + while (selected_task.is_valid()) { + Status status = selected_task->execute(p_delta); + if (status == FAILURE) { + failed_tasks.insert(selected_task); + _select_task(); + } else { // RUNNING or SUCCESS + return status; + } + } + + return FAILURE; +} + +void BTProbabilitySelector::_select_task() { + selected_task.unref(); + + double remaining_tasks_weight = _get_total_weight(); + for (const Ref &task : failed_tasks) { + remaining_tasks_weight -= _get_weight(task); + } + + double roll = Math::random(0.0, remaining_tasks_weight); + for (int i = 0; i < get_child_count(); i++) { + Ref task = get_child(i); + if (failed_tasks.has(task)) { + continue; + } + double weight = _get_weight(i); + if (weight == 0) { + continue; + } + if (roll > weight) { + roll -= weight; + continue; + } + + selected_task = task; + break; + } +} + +//***** Godot + +void BTProbabilitySelector::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_weight", "p_index"), &BTProbabilitySelector::get_weight); + ClassDB::bind_method(D_METHOD("set_weight", "p_index", "p_weight"), &BTProbabilitySelector::set_weight); + ClassDB::bind_method(D_METHOD("get_probability", "p_index"), &BTProbabilitySelector::get_probability); + ClassDB::bind_method(D_METHOD("set_probability", "p_index", "p_probability"), &BTProbabilitySelector::set_probability); +} diff --git a/bt/tasks/composites/bt_probability_selector.h b/bt/tasks/composites/bt_probability_selector.h new file mode 100644 index 0000000..c45157d --- /dev/null +++ b/bt/tasks/composites/bt_probability_selector.h @@ -0,0 +1,55 @@ +/** + * bt_probability_selector.h + * ============================================================================= + * Copyright 2021-2023 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. + * ============================================================================= + */ + +#ifndef BT_PROBABILITY_SELECTOR_H +#define BT_PROBABILITY_SELECTOR_H + +#include "modules/limboai/bt/tasks/bt_composite.h" + +#include "core/typedefs.h" + +class BTProbabilitySelector : public BTComposite { + GDCLASS(BTProbabilitySelector, BTComposite); + TASK_CATEGORY(Composites); + +private: + HashSet> failed_tasks; + Ref selected_task; + + void _select_task(); + + _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_ double _get_total_weight() const { + double total = 0.0; + for (int i = 0; i < get_child_count(); i++) { + total += _get_weight(i); + } + return total; + } + +protected: + static void _bind_methods(); + + virtual void _enter() override; + virtual void _exit() override; + virtual Status _tick(double p_delta) override; + +public: + double get_weight(int p_index) const; + void set_weight(int p_index, double p_weight); + + double get_probability(int p_index) const; + void set_probability(int p_index, double p_probability); +}; + +#endif // BT_PROBABILITY_SELECTOR_H diff --git a/config.py b/config.py index 8f47e4c..d456416 100644 --- a/config.py +++ b/config.py @@ -84,6 +84,7 @@ def get_doc_classes(): "BTPlayAnimation", "BTPlayer", "BTProbability", + "BTProbabilitySelector", "BTRandomSelector", "BTRandomSequence", "BTRandomWait", diff --git a/register_types.cpp b/register_types.cpp index 9886a78..9f93b7d 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -59,6 +59,7 @@ #include "bt/tasks/composites/bt_dynamic_selector.h" #include "bt/tasks/composites/bt_dynamic_sequence.h" #include "bt/tasks/composites/bt_parallel.h" +#include "bt/tasks/composites/bt_probability_selector.h" #include "bt/tasks/composites/bt_random_selector.h" #include "bt/tasks/composites/bt_random_sequence.h" #include "bt/tasks/composites/bt_selector.h" @@ -131,6 +132,7 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) { LIMBO_REGISTER_TASK(BTParallel); LIMBO_REGISTER_TASK(BTDynamicSequence); LIMBO_REGISTER_TASK(BTDynamicSelector); + LIMBO_REGISTER_TASK(BTProbabilitySelector); LIMBO_REGISTER_TASK(BTRandomSequence); LIMBO_REGISTER_TASK(BTRandomSelector); From a7f6388bd76be4daf53228e8670afb20ca171f4c Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Sun, 24 Sep 2023 14:11:52 +0200 Subject: [PATCH 2/8] Add icon for BTProbabilitySelector --- icons/BTProbabilitySelector.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icons/BTProbabilitySelector.svg b/icons/BTProbabilitySelector.svg index 80b80ea..f9516a1 100644 --- a/icons/BTProbabilitySelector.svg +++ b/icons/BTProbabilitySelector.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From b33c1cae319359f12f0c7fc690722faaad0d1b5c Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Sun, 24 Sep 2023 14:12:50 +0200 Subject: [PATCH 3/8] Add tests for BTProbabilitySelector --- tests/test_probability_selector.h | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/test_probability_selector.h diff --git a/tests/test_probability_selector.h b/tests/test_probability_selector.h new file mode 100644 index 0000000..fefe3eb --- /dev/null +++ b/tests/test_probability_selector.h @@ -0,0 +1,164 @@ +/** + * test_probability_selector.h + * ============================================================================= + * Copyright 2021-2023 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. + * ============================================================================= + */ + +#ifndef TEST_PROBABILITY_SELECTOR_H +#define TEST_PROBABILITY_SELECTOR_H + +#include "limbo_test.h" + +#include "modules/limboai/bt/tasks/bt_task.h" +#include "modules/limboai/bt/tasks/composites/bt_probability_selector.h" + +namespace TestProbabilitySelector { + +TEST_CASE("[Modules][LimboAI] BTProbabilitySelector") { + Ref sel = memnew(BTProbabilitySelector); + + SUBCASE("When empty") { + ERR_PRINT_OFF; + CHECK(sel->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + } + + Ref task1 = memnew(BTTestAction); + Ref task2 = memnew(BTTestAction); + Ref task3 = memnew(BTTestAction); + sel->add_child(task1); + sel->add_child(task2); + sel->add_child(task3); + + Math::randomize(); + + SUBCASE("With zero weight") { + sel->set_weight(0, 0.0); + sel->set_weight(1, 0.0); + sel->set_weight(2, 0.0); + + CHECK(sel->execute(0.01666) == BTTask::FAILURE); + + for (int i = 0; i < 100; i++) { + sel->execute(0.01666); + } + + CHECK_STATUS_ENTRIES_TICKS_EXITS(task1, BTTask::FRESH, 0, 0, 0); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task2, BTTask::FRESH, 0, 0, 0); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task3, BTTask::FRESH, 0, 0, 0); + } + SUBCASE("When a child task returns SUCCESS") { + sel->set_weight(0, 1.0); + sel->set_weight(1, 0.0); + sel->set_weight(2, 0.0); + task1->ret_status = BTTask::SUCCESS; + + CHECK(sel->execute(0.01666) == BTTask::SUCCESS); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task1, BTTask::SUCCESS, 1, 1, 1); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task2, BTTask::FRESH, 0, 0, 0); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task3, BTTask::FRESH, 0, 0, 0); + CHECK(sel->execute(0.01666) == BTTask::SUCCESS); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task1, BTTask::SUCCESS, 2, 2, 2); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task2, BTTask::FRESH, 0, 0, 0); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task3, BTTask::FRESH, 0, 0, 0); + } + SUBCASE("With a RUNNING status and a low-weight remaining child") { + sel->set_weight(0, 0.0); + sel->set_weight(1, 1.0); + sel->set_weight(2, 0.0); + task1->ret_status = BTTask::FAILURE; + task2->ret_status = BTTask::RUNNING; + task3->ret_status = BTTask::FAILURE; + + CHECK(sel->execute(0.01666) == BTTask::RUNNING); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task1, BTTask::FRESH, 0, 0, 0); // * ignored + CHECK_STATUS_ENTRIES_TICKS_EXITS(task2, BTTask::RUNNING, 1, 1, 0); // * running + CHECK_STATUS_ENTRIES_TICKS_EXITS(task3, BTTask::FRESH, 0, 0, 0); // * ignored + + CHECK(sel->execute(0.01666) == BTTask::RUNNING); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task1, BTTask::FRESH, 0, 0, 0); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task2, BTTask::RUNNING, 1, 2, 0); // * continued + CHECK_STATUS_ENTRIES_TICKS_EXITS(task3, BTTask::FRESH, 0, 0, 0); + + task2->ret_status = BTTask::FAILURE; + task1->ret_status = BTTask::SUCCESS; + sel->set_weight(0, 0.000000000001); // * extremely low weight, however, when it is the only child to evaluate, it should have 100% probability of being chosen. + CHECK(sel->execute(0.01666) == BTTask::SUCCESS); + CHECK_STATUS_ENTRIES_TICKS_EXITS(task1, BTTask::SUCCESS, 1, 1, 1); // * started & succeeded (2) + CHECK_STATUS_ENTRIES_TICKS_EXITS(task2, BTTask::FAILURE, 1, 3, 1); // * continued & failed (1) + CHECK_STATUS_ENTRIES_TICKS_EXITS(task3, BTTask::FRESH, 0, 0, 0); // * ignored + } + SUBCASE("When all return SUCCESS status") { + task1->ret_status = BTTask::SUCCESS; + task2->ret_status = BTTask::SUCCESS; + task3->ret_status = BTTask::SUCCESS; + + CHECK(sel->execute(0.01666) == BTTask::SUCCESS); + CHECK(sel->execute(0.01666) == BTTask::SUCCESS); + CHECK(sel->execute(0.01666) == BTTask::SUCCESS); + + int num_ticks = task1->num_ticks + task2->num_ticks + task3->num_ticks; + CHECK(num_ticks == 3); + + int num_entries = task1->num_entries + task2->num_entries + task3->num_entries; + CHECK(num_entries == 3); + + int num_exits = task1->num_exits + task2->num_exits + task3->num_exits; + CHECK(num_exits == 3); + + CHECK(task1->is_status_either(BTTask::SUCCESS, BTTask::FRESH)); + CHECK(task2->is_status_either(BTTask::SUCCESS, BTTask::FRESH)); + CHECK(task3->is_status_either(BTTask::SUCCESS, BTTask::FRESH)); + } + SUBCASE("With balanced weights") { + task1->ret_status = BTTask::SUCCESS; + task2->ret_status = BTTask::SUCCESS; + task3->ret_status = BTTask::SUCCESS; + + int sample_size = 1000; + sel->set_weight(0, 1.0); + sel->set_weight(1, 1.0); + sel->set_weight(2, 1.0); + + for (int i = 0; i < sample_size; i++) { + sel->execute(0.01666); + } + + CHECK(task1->num_ticks > 300); + CHECK(task1->num_ticks < 366); + CHECK(task2->num_ticks > 300); + CHECK(task2->num_ticks < 366); + CHECK(task3->num_ticks > 300); + CHECK(task3->num_ticks < 366); + } + SUBCASE("With imbalanced weights") { + task1->ret_status = BTTask::SUCCESS; + task2->ret_status = BTTask::SUCCESS; + task3->ret_status = BTTask::SUCCESS; + + int sample_size = 10000; + sel->set_weight(0, 1.0); // * ~1250 + sel->set_weight(1, 2.0); // * ~2500 + sel->set_weight(2, 5.0); // * ~6250 + + for (int i = 0; i < sample_size; i++) { + sel->execute(0.01666); + } + + CHECK(task1->num_ticks > 1150); + CHECK(task1->num_ticks < 1350); + CHECK(task2->num_ticks > 2250); + CHECK(task2->num_ticks < 2750); + CHECK(task3->num_ticks > 5750); + CHECK(task3->num_ticks < 6750); + } +} + +} //namespace TestProbabilitySelector + +#endif // TEST_PROBABILITY_SELECTOR_H From 32cbce6b800e26f76efe4bdf84954ef894b91f28 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Sun, 24 Sep 2023 16:45:10 +0200 Subject: [PATCH 4/8] Add abort_on_failure policy to BTProbabilitySelector --- .../composites/bt_probability_selector.cpp | 16 +++++++++++++ bt/tasks/composites/bt_probability_selector.h | 4 ++++ tests/test_probability_selector.h | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/bt/tasks/composites/bt_probability_selector.cpp b/bt/tasks/composites/bt_probability_selector.cpp index 74e12fd..36a9416 100644 --- a/bt/tasks/composites/bt_probability_selector.cpp +++ b/bt/tasks/composites/bt_probability_selector.cpp @@ -40,6 +40,15 @@ void BTProbabilitySelector::set_probability(int p_index, double p_probability) { _set_weight(p_index, new_total - others_total); } +void BTProbabilitySelector::set_abort_on_failure(bool p_abort_on_failure) { + abort_on_failure = p_abort_on_failure; + emit_changed(); +} + +bool BTProbabilitySelector::get_abort_on_failure() const { + return abort_on_failure; +} + void BTProbabilitySelector::_enter() { _select_task(); } @@ -53,6 +62,9 @@ BT::Status BTProbabilitySelector::_tick(double p_delta) { while (selected_task.is_valid()) { Status status = selected_task->execute(p_delta); if (status == FAILURE) { + if (abort_on_failure) { + return FAILURE; + } failed_tasks.insert(selected_task); _select_task(); } else { // RUNNING or SUCCESS @@ -98,4 +110,8 @@ void BTProbabilitySelector::_bind_methods() { ClassDB::bind_method(D_METHOD("set_weight", "p_index", "p_weight"), &BTProbabilitySelector::set_weight); ClassDB::bind_method(D_METHOD("get_probability", "p_index"), &BTProbabilitySelector::get_probability); ClassDB::bind_method(D_METHOD("set_probability", "p_index", "p_probability"), &BTProbabilitySelector::set_probability); + ClassDB::bind_method(D_METHOD("get_abort_on_failure"), &BTProbabilitySelector::get_abort_on_failure); + ClassDB::bind_method(D_METHOD("set_abort_on_failure", "p_value"), &BTProbabilitySelector::set_abort_on_failure); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "abort_on_failure"), "set_abort_on_failure", "get_abort_on_failure"); } diff --git a/bt/tasks/composites/bt_probability_selector.h b/bt/tasks/composites/bt_probability_selector.h index c45157d..744a926 100644 --- a/bt/tasks/composites/bt_probability_selector.h +++ b/bt/tasks/composites/bt_probability_selector.h @@ -23,6 +23,7 @@ class BTProbabilitySelector : public BTComposite { private: HashSet> failed_tasks; Ref selected_task; + bool abort_on_failure = false; void _select_task(); @@ -50,6 +51,9 @@ public: double get_probability(int p_index) const; void set_probability(int p_index, double p_probability); + + void set_abort_on_failure(bool p_abort_on_failure); + bool get_abort_on_failure() const; }; #endif // BT_PROBABILITY_SELECTOR_H diff --git a/tests/test_probability_selector.h b/tests/test_probability_selector.h index fefe3eb..13337b5 100644 --- a/tests/test_probability_selector.h +++ b/tests/test_probability_selector.h @@ -157,6 +157,30 @@ TEST_CASE("[Modules][LimboAI] BTProbabilitySelector") { CHECK(task3->num_ticks > 5750); CHECK(task3->num_ticks < 6750); } + SUBCASE("Test abort_on_failure") { + task1->ret_status = BTTask::FAILURE; + task2->ret_status = BTTask::FAILURE; + task3->ret_status = BTTask::FAILURE; + + int expected_child_executions = 0; + + SUBCASE("When abort_on_failure == false") { + sel->set_abort_on_failure(false); + expected_child_executions = 3; + } + SUBCASE("When abort_on_failure == true") { + sel->set_abort_on_failure(true); + expected_child_executions = 1; + } + + sel->execute(0.01666); + int num_ticks = task1->num_ticks + task2->num_ticks + task3->num_ticks; + CHECK(num_ticks == expected_child_executions); + int num_entries = task1->num_entries + task2->num_entries + task3->num_entries; + CHECK(num_entries == expected_child_executions); + int num_exits = task1->num_exits + task2->num_exits + task3->num_exits; + CHECK(num_exits == expected_child_executions); + } } } //namespace TestProbabilitySelector From 52a70fdee521d6aad1f52be5a8e26a9717026c7e Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 25 Sep 2023 18:07:26 +0200 Subject: [PATCH 5/8] 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(); From 1df821fdfa5d5395d89647a1027db7640c605c08 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 25 Sep 2023 22:36:37 +0200 Subject: [PATCH 6/8] Allow editing weight as percent in Probability popup --- .../composites/bt_probability_selector.cpp | 15 ++- bt/tasks/composites/bt_probability_selector.h | 1 + editor/limbo_ai_editor_plugin.cpp | 97 ++++++++++++------- editor/limbo_ai_editor_plugin.h | 4 + editor/task_tree.cpp | 8 ++ editor/task_tree.h | 1 + 6 files changed, 88 insertions(+), 38 deletions(-) diff --git a/bt/tasks/composites/bt_probability_selector.cpp b/bt/tasks/composites/bt_probability_selector.cpp index 36a9416..575633b 100644 --- a/bt/tasks/composites/bt_probability_selector.cpp +++ b/bt/tasks/composites/bt_probability_selector.cpp @@ -25,19 +25,23 @@ void BTProbabilitySelector::set_weight(int p_index, double p_weight) { double BTProbabilitySelector::get_probability(int p_index) const { ERR_FAIL_INDEX_V(p_index, get_child_count(), 0.0); - return _get_weight(p_index) / _get_total_weight(); + double total = _get_total_weight(); + return total == 0.0 ? 0.0 : _get_weight(p_index) / total; } void BTProbabilitySelector::set_probability(int p_index, double p_probability) { ERR_FAIL_INDEX(p_index, get_child_count()); ERR_FAIL_COND(p_probability < 0.0); - ERR_FAIL_COND(p_probability > 1.0); - ERR_FAIL_COND(p_probability > 0.99 && get_child_count() > 1); + ERR_FAIL_COND(p_probability >= 1.0); double others_total = _get_total_weight() - _get_weight(p_index); double others_probability = 1.0 - p_probability; - double new_total = others_total / others_probability; - _set_weight(p_index, new_total - others_total); + if (others_total == 0.0) { + _set_weight(p_index, p_probability > 0.0 ? 1.0 : 0.0); + } else { + double new_total = others_total / others_probability; + _set_weight(p_index, new_total - others_total); + } } void BTProbabilitySelector::set_abort_on_failure(bool p_abort_on_failure) { @@ -108,6 +112,7 @@ void BTProbabilitySelector::_select_task() { void BTProbabilitySelector::_bind_methods() { ClassDB::bind_method(D_METHOD("get_weight", "p_index"), &BTProbabilitySelector::get_weight); ClassDB::bind_method(D_METHOD("set_weight", "p_index", "p_weight"), &BTProbabilitySelector::set_weight); + ClassDB::bind_method(D_METHOD("get_total_weight"), &BTProbabilitySelector::get_total_weight); ClassDB::bind_method(D_METHOD("get_probability", "p_index"), &BTProbabilitySelector::get_probability); ClassDB::bind_method(D_METHOD("set_probability", "p_index", "p_probability"), &BTProbabilitySelector::set_probability); ClassDB::bind_method(D_METHOD("get_abort_on_failure"), &BTProbabilitySelector::get_abort_on_failure); diff --git a/bt/tasks/composites/bt_probability_selector.h b/bt/tasks/composites/bt_probability_selector.h index 8afd1d9..4e893ff 100644 --- a/bt/tasks/composites/bt_probability_selector.h +++ b/bt/tasks/composites/bt_probability_selector.h @@ -52,6 +52,7 @@ protected: public: double get_weight(int p_index) const; void set_weight(int p_index, double p_weight); + double get_total_weight() const { return _get_total_weight(); }; double get_probability(int p_index) const; void set_probability(int p_index, double p_probability); diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index 1c570af..270f4bb 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -33,6 +33,7 @@ #include "editor/inspector_dock.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/project_settings_editor.h" +#include "scene/gui/panel_container.h" #include "scene/gui/separator.h" //**** LimboAIEditor @@ -319,7 +320,7 @@ void LimboAIEditor::_action_selected(int p_id) { 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()); + _update_probability_edit(); probability_popup->popup(rect); } break; case ACTION_EDIT_SCRIPT: { @@ -438,7 +439,42 @@ void LimboAIEditor::_on_probability_edited(double p_value) { 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); + if (percent_mode->is_pressed()) { + probability_selector->set_probability(probability_selector->get_child_index(selected), p_value * 0.01); + } else { + probability_selector->set_weight(probability_selector->get_child_index(selected), p_value); + } +} + +void LimboAIEditor::_update_probability_edit() { + Ref selected = task_tree->get_selected(); + ERR_FAIL_COND(selected.is_null()); + Ref prob = selected->get_parent(); + ERR_FAIL_COND(prob.is_null()); + double others_weight = prob->get_total_weight() - prob->get_weight(prob->get_child_index(selected)); + bool cannot_edit_percent = others_weight == 0.0; + percent_mode->set_disabled(cannot_edit_percent); + if (cannot_edit_percent && percent_mode->is_pressed()) { + weight_mode->set_pressed(true); + } + + if (percent_mode->is_pressed()) { + probability_edit->set_suffix("%"); + probability_edit->set_max(99.0); + probability_edit->set_allow_greater(false); + probability_edit->set_step(0.01); + probability_edit->set_value_no_signal(task_tree->get_selected_probability_percent()); + } else { + probability_edit->set_suffix(""); + probability_edit->set_allow_greater(true); + probability_edit->set_max(10.0); + probability_edit->set_step(0.01); + probability_edit->set_value_no_signal(task_tree->get_selected_probability_weight()); + } +} + +void LimboAIEditor::_probability_popup_closed() { + probability_edit->get_line_edit()->release_focus(); } void LimboAIEditor::_misc_option_selected(int p_id) { @@ -792,7 +828,6 @@ void LimboAIEditor::_notification(int p_what) { new_script_btn->set_icon(get_theme_icon(SNAME("ScriptCreate"), SNAME("EditorIcons"))); history_back->set_icon(get_theme_icon(SNAME("Back"), SNAME("EditorIcons"))); history_forward->set_icon(get_theme_icon(SNAME("Forward"), SNAME("EditorIcons"))); - misc_btn->set_icon(get_theme_icon(SNAME("Tools"), SNAME("EditorIcons"))); _update_favorite_tasks(); @@ -981,39 +1016,33 @@ LimboAIEditor::LimboAIEditor() { VBoxContainer *vbc = memnew(VBoxContainer); probability_popup->add_child(vbc); - // PanelContainer *mode_panel = memnew(PanelContainer); - // vbc->add_child(mode_panel); + PanelContainer *mode_panel = memnew(PanelContainer); + vbc->add_child(mode_panel); - // HBoxContainer *mode_hbox = memnew(HBoxContainer); - // mode_panel->add_child(mode_hbox); + HBoxContainer *mode_hbox = memnew(HBoxContainer); + mode_panel->add_child(mode_hbox); - // Ref button_group; - // button_group.instantiate(); + 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()) + weight_mode = memnew(Button); + mode_hbox->add_child(weight_mode); + weight_mode->set_toggle_mode(true); + weight_mode->set_button_group(button_group); + weight_mode->set_focus_mode(Control::FOCUS_NONE); + weight_mode->set_text(TTR("Weight")); + weight_mode->set_tooltip_text(TTR("Edit weight")); + weight_mode->connect("pressed", callable_mp(this, &LimboAIEditor::_update_probability_edit)); + weight_mode->set_pressed_no_signal(true); - // 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"); + percent_mode = memnew(Button); + mode_hbox->add_child(percent_mode); + percent_mode->set_toggle_mode(true); + percent_mode->set_button_group(button_group); + percent_mode->set_focus_mode(Control::FOCUS_NONE); + percent_mode->set_text(TTR("Percent")); + percent_mode->set_tooltip_text(TTR("Edit percent")); + percent_mode->connect("pressed", callable_mp(this, &LimboAIEditor::_update_probability_edit)); probability_edit = memnew(EditorSpinSlider); vbc->add_child(probability_edit); @@ -1022,7 +1051,9 @@ LimboAIEditor::LimboAIEditor() { 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)); + probability_edit->connect("value_changed", callable_mp(this, &LimboAIEditor::_on_probability_edited)); + + probability_popup->connect("popup_hide", callable_mp(this, &LimboAIEditor::_probability_popup_closed)); } add_child(probability_popup); diff --git a/editor/limbo_ai_editor_plugin.h b/editor/limbo_ai_editor_plugin.h index d3c1dfc..1734584 100644 --- a/editor/limbo_ai_editor_plugin.h +++ b/editor/limbo_ai_editor_plugin.h @@ -86,6 +86,8 @@ private: PopupPanel *probability_popup; EditorSpinSlider *probability_edit; + Button *weight_mode; + Button *percent_mode; FileDialog *save_dialog; FileDialog *load_dialog; @@ -131,6 +133,8 @@ private: void _action_selected(int p_id); void _misc_option_selected(int p_id); void _on_probability_edited(double p_value); + void _update_probability_edit(); + void _probability_popup_closed(); void _on_tree_task_selected(const Ref &p_task); void _on_visibility_changed(); void _on_header_pressed(); diff --git a/editor/task_tree.cpp b/editor/task_tree.cpp index 8b3af83..9480705 100644 --- a/editor/task_tree.cpp +++ b/editor/task_tree.cpp @@ -225,6 +225,14 @@ double TaskTree::get_selected_probability_weight() const { return probability_selector->get_weight(probability_selector->get_child_index(selected)); } +double TaskTree::get_selected_probability_percent() 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_probability(probability_selector->get_child_index(selected)) * 100.0; +} + bool TaskTree::selected_has_probability() const { bool result = false; Ref selected = get_selected(); diff --git a/editor/task_tree.h b/editor/task_tree.h index 4d65d94..a8f611d 100644 --- a/editor/task_tree.h +++ b/editor/task_tree.h @@ -76,6 +76,7 @@ public: Rect2 get_selected_probability_rect() const; double get_selected_probability_weight() const; + double get_selected_probability_percent() const; bool selected_has_probability() const; virtual bool editor_can_reload_from_file() { return false; } From d0e9bb0ed3c3380ebeb0b875081990cddf9755db Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Mon, 25 Sep 2023 22:52:08 +0200 Subject: [PATCH 7/8] Add ProbabilitySelector usage test to demo project --- .../bt_test_probability_selector.tres | 27 +++++++++++++++++++ .../test_probability_selector.tscn | 8 ++++++ 2 files changed, 35 insertions(+) create mode 100644 demo/tests/probability_selector/bt_test_probability_selector.tres create mode 100644 demo/tests/probability_selector/test_probability_selector.tscn diff --git a/demo/tests/probability_selector/bt_test_probability_selector.tres b/demo/tests/probability_selector/bt_test_probability_selector.tres new file mode 100644 index 0000000..1997bb2 --- /dev/null +++ b/demo/tests/probability_selector/bt_test_probability_selector.tres @@ -0,0 +1,27 @@ +[gd_resource type="BehaviorTree" load_steps=8 format=3 uid="uid://cen725hsk8lyl"] + +[sub_resource type="BTComment" id="BTComment_84hry"] +custom_name = "This is a test of ProbabilitySelector choosing action to execute" + +[sub_resource type="BTConsolePrint" id="BTConsolePrint_3d5qm"] +text = "Rare action" + +[sub_resource type="BTConsolePrint" id="BTConsolePrint_s6p66"] +text = "Uncommon action" +metadata/_weight_ = 4.0 + +[sub_resource type="BTConsolePrint" id="BTConsolePrint_2f8re"] +text = "Common action" +metadata/_weight_ = 12.0 + +[sub_resource type="BTProbabilitySelector" id="BTProbabilitySelector_hy6es"] +children = [SubResource("BTConsolePrint_3d5qm"), SubResource("BTConsolePrint_s6p66"), SubResource("BTConsolePrint_2f8re")] + +[sub_resource type="BTDelay" id="BTDelay_mxnxy"] +children = [SubResource("BTProbabilitySelector_hy6es")] + +[sub_resource type="BTSequence" id="BTSequence_auek2"] +children = [SubResource("BTComment_84hry"), SubResource("BTDelay_mxnxy")] + +[resource] +root_task = SubResource("BTSequence_auek2") diff --git a/demo/tests/probability_selector/test_probability_selector.tscn b/demo/tests/probability_selector/test_probability_selector.tscn new file mode 100644 index 0000000..dbdca9a --- /dev/null +++ b/demo/tests/probability_selector/test_probability_selector.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=3 uid="uid://dgeb7tg8xb3j4"] + +[ext_resource type="BehaviorTree" uid="uid://cen725hsk8lyl" path="res://tests/probability_selector/bt_test_probability_selector.tres" id="1_lr7l2"] + +[node name="test_probability_selector" type="Node2D"] + +[node name="BTPlayer" type="BTPlayer" parent="."] +behavior_tree = ExtResource("1_lr7l2") From 27254581a4509c9382968953a98bb9bde7f54cab Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Tue, 26 Sep 2023 11:10:23 +0200 Subject: [PATCH 8/8] Fix Probability popup stuck with ghost value --- editor/limbo_ai_editor_plugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index 270f4bb..dc92550 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -474,7 +474,7 @@ void LimboAIEditor::_update_probability_edit() { } void LimboAIEditor::_probability_popup_closed() { - probability_edit->get_line_edit()->release_focus(); + probability_edit->grab_focus(); // Hack: Workaround for an EditorSpinSlider bug keeping LineEdit visible and "stuck" with ghost value. } void LimboAIEditor::_misc_option_selected(int p_id) {