Merge branch 'probability-selector'
This commit is contained in:
commit
cd299bef4b
|
@ -0,0 +1,122 @@
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
double others_total = _get_total_weight() - _get_weight(p_index);
|
||||||
|
double others_probability = 1.0 - p_probability;
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (abort_on_failure) {
|
||||||
|
return 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<BTTask> &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<BTTask> 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_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);
|
||||||
|
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");
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* 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/core_string_names.h"
|
||||||
|
#include "core/typedefs.h"
|
||||||
|
|
||||||
|
class BTProbabilitySelector : public BTComposite {
|
||||||
|
GDCLASS(BTProbabilitySelector, BTComposite);
|
||||||
|
TASK_CATEGORY(Composites);
|
||||||
|
|
||||||
|
private:
|
||||||
|
HashSet<Ref<BTTask>> failed_tasks;
|
||||||
|
Ref<BTTask> selected_task;
|
||||||
|
bool abort_on_failure = false;
|
||||||
|
|
||||||
|
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<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));
|
||||||
|
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++) {
|
||||||
|
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_total_weight() const { return _get_total_weight(); };
|
||||||
|
|
||||||
|
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
|
|
@ -84,6 +84,7 @@ def get_doc_classes():
|
||||||
"BTPlayAnimation",
|
"BTPlayAnimation",
|
||||||
"BTPlayer",
|
"BTPlayer",
|
||||||
"BTProbability",
|
"BTProbability",
|
||||||
|
"BTProbabilitySelector",
|
||||||
"BTRandomSelector",
|
"BTRandomSelector",
|
||||||
"BTRandomSequence",
|
"BTRandomSequence",
|
||||||
"BTRandomWait",
|
"BTRandomWait",
|
||||||
|
|
|
@ -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")
|
|
@ -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")
|
|
@ -15,11 +15,13 @@
|
||||||
|
|
||||||
#include "action_banner.h"
|
#include "action_banner.h"
|
||||||
#include "modules/limboai/bt/tasks/bt_comment.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/bt/tasks/composites/bt_selector.h"
|
||||||
#include "modules/limboai/editor/debugger/limbo_debugger_plugin.h"
|
#include "modules/limboai/editor/debugger/limbo_debugger_plugin.h"
|
||||||
#include "modules/limboai/util/limbo_utility.h"
|
#include "modules/limboai/util/limbo_utility.h"
|
||||||
|
|
||||||
#include "core/config/project_settings.h"
|
#include "core/config/project_settings.h"
|
||||||
|
#include "core/error/error_macros.h"
|
||||||
#include "editor/debugger/editor_debugger_node.h"
|
#include "editor/debugger/editor_debugger_node.h"
|
||||||
#include "editor/debugger/script_editor_debugger.h"
|
#include "editor/debugger/script_editor_debugger.h"
|
||||||
#include "editor/editor_file_system.h"
|
#include "editor/editor_file_system.h"
|
||||||
|
@ -31,6 +33,7 @@
|
||||||
#include "editor/inspector_dock.h"
|
#include "editor/inspector_dock.h"
|
||||||
#include "editor/plugins/script_editor_plugin.h"
|
#include "editor/plugins/script_editor_plugin.h"
|
||||||
#include "editor/project_settings_editor.h"
|
#include "editor/project_settings_editor.h"
|
||||||
|
#include "scene/gui/panel_container.h"
|
||||||
#include "scene/gui/separator.h"
|
#include "scene/gui/separator.h"
|
||||||
|
|
||||||
//**** LimboAIEditor
|
//**** LimboAIEditor
|
||||||
|
@ -267,10 +270,13 @@ void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) {
|
||||||
Ref<BTTask> task = task_tree->get_selected();
|
Ref<BTTask> task = task_tree->get_selected();
|
||||||
ERR_FAIL_COND_MSG(task.is_null(), "LimboAIEditor: get_selected() returned null");
|
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_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.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(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_separator();
|
||||||
menu->add_icon_shortcut(theme_cache.move_task_up_icon, ED_GET_SHORTCUT("limbo_ai/move_task_up"), ACTION_MOVE_UP);
|
menu->add_icon_shortcut(theme_cache.move_task_up_icon, ED_GET_SHORTCUT("limbo_ai/move_task_up"), ACTION_MOVE_UP);
|
||||||
|
@ -308,6 +314,15 @@ 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_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);
|
||||||
|
_update_probability_edit();
|
||||||
|
probability_popup->popup(rect);
|
||||||
|
} break;
|
||||||
case ACTION_EDIT_SCRIPT: {
|
case ACTION_EDIT_SCRIPT: {
|
||||||
ERR_FAIL_COND(task_tree->get_selected().is_null());
|
ERR_FAIL_COND(task_tree->get_selected().is_null());
|
||||||
EditorNode::get_singleton()->edit_resource(task_tree->get_selected()->get_script());
|
EditorNode::get_singleton()->edit_resource(task_tree->get_selected()->get_script());
|
||||||
|
@ -419,6 +434,49 @@ 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());
|
||||||
|
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<BTTask> selected = task_tree->get_selected();
|
||||||
|
ERR_FAIL_COND(selected.is_null());
|
||||||
|
Ref<BTProbabilitySelector> 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->grab_focus(); // Hack: Workaround for an EditorSpinSlider bug keeping LineEdit visible and "stuck" with ghost value.
|
||||||
|
}
|
||||||
|
|
||||||
void LimboAIEditor::_misc_option_selected(int p_id) {
|
void LimboAIEditor::_misc_option_selected(int p_id) {
|
||||||
switch (p_id) {
|
switch (p_id) {
|
||||||
case MISC_OPEN_DEBUGGER: {
|
case MISC_OPEN_DEBUGGER: {
|
||||||
|
@ -491,10 +549,6 @@ void LimboAIEditor::_on_tree_task_selected(const Ref<BTTask> &p_task) {
|
||||||
EditorNode::get_singleton()->edit_resource(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() {
|
void LimboAIEditor::_on_visibility_changed() {
|
||||||
if (task_tree->is_visible_in_tree()) {
|
if (task_tree->is_visible_in_tree()) {
|
||||||
Ref<BTTask> sel = task_tree->get_selected();
|
Ref<BTTask> sel = task_tree->get_selected();
|
||||||
|
@ -774,7 +828,6 @@ void LimboAIEditor::_notification(int p_what) {
|
||||||
new_script_btn->set_icon(get_theme_icon(SNAME("ScriptCreate"), SNAME("EditorIcons")));
|
new_script_btn->set_icon(get_theme_icon(SNAME("ScriptCreate"), SNAME("EditorIcons")));
|
||||||
history_back->set_icon(get_theme_icon(SNAME("Back"), SNAME("EditorIcons")));
|
history_back->set_icon(get_theme_icon(SNAME("Back"), SNAME("EditorIcons")));
|
||||||
history_forward->set_icon(get_theme_icon(SNAME("Forward"), SNAME("EditorIcons")));
|
history_forward->set_icon(get_theme_icon(SNAME("Forward"), SNAME("EditorIcons")));
|
||||||
|
|
||||||
misc_btn->set_icon(get_theme_icon(SNAME("Tools"), SNAME("EditorIcons")));
|
misc_btn->set_icon(get_theme_icon(SNAME("Tools"), SNAME("EditorIcons")));
|
||||||
|
|
||||||
_update_favorite_tasks();
|
_update_favorite_tasks();
|
||||||
|
@ -925,7 +978,8 @@ LimboAIEditor::LimboAIEditor() {
|
||||||
task_tree->connect("rmb_pressed", callable_mp(this, &LimboAIEditor::_on_tree_rmb));
|
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_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_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::_on_visibility_changed));
|
||||||
task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_update_banners));
|
task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_update_banners));
|
||||||
hsc->add_child(task_tree);
|
hsc->add_child(task_tree);
|
||||||
|
@ -957,6 +1011,52 @@ LimboAIEditor::LimboAIEditor() {
|
||||||
add_child(menu);
|
add_child(menu);
|
||||||
menu->connect("id_pressed", callable_mp(this, &LimboAIEditor::_action_selected));
|
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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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("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);
|
||||||
|
|
||||||
rename_dialog = memnew(ConfirmationDialog);
|
rename_dialog = memnew(ConfirmationDialog);
|
||||||
{
|
{
|
||||||
VBoxContainer *vbc = memnew(VBoxContainer);
|
VBoxContainer *vbc = memnew(VBoxContainer);
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
#include "core/templates/hash_set.h"
|
#include "core/templates/hash_set.h"
|
||||||
#include "editor/editor_node.h"
|
#include "editor/editor_node.h"
|
||||||
#include "editor/editor_plugin.h"
|
#include "editor/editor_plugin.h"
|
||||||
|
#include "editor/gui/editor_spin_slider.h"
|
||||||
#include "scene/gui/box_container.h"
|
#include "scene/gui/box_container.h"
|
||||||
#include "scene/gui/control.h"
|
#include "scene/gui/control.h"
|
||||||
#include "scene/gui/file_dialog.h"
|
#include "scene/gui/file_dialog.h"
|
||||||
|
@ -41,6 +42,7 @@ class LimboAIEditor : public Control {
|
||||||
private:
|
private:
|
||||||
enum Action {
|
enum Action {
|
||||||
ACTION_RENAME,
|
ACTION_RENAME,
|
||||||
|
ACTION_EDIT_PROBABILITY,
|
||||||
ACTION_EDIT_SCRIPT,
|
ACTION_EDIT_SCRIPT,
|
||||||
ACTION_OPEN_DOC,
|
ACTION_OPEN_DOC,
|
||||||
ACTION_MOVE_UP,
|
ACTION_MOVE_UP,
|
||||||
|
@ -79,12 +81,18 @@ private:
|
||||||
VBoxContainer *banners;
|
VBoxContainer *banners;
|
||||||
Panel *usage_hint;
|
Panel *usage_hint;
|
||||||
PopupMenu *menu;
|
PopupMenu *menu;
|
||||||
|
HBoxContainer *fav_tasks_hbox;
|
||||||
|
TaskPalette *task_palette;
|
||||||
|
|
||||||
|
PopupPanel *probability_popup;
|
||||||
|
EditorSpinSlider *probability_edit;
|
||||||
|
Button *weight_mode;
|
||||||
|
Button *percent_mode;
|
||||||
|
|
||||||
FileDialog *save_dialog;
|
FileDialog *save_dialog;
|
||||||
FileDialog *load_dialog;
|
FileDialog *load_dialog;
|
||||||
Button *history_back;
|
Button *history_back;
|
||||||
Button *history_forward;
|
Button *history_forward;
|
||||||
TaskPalette *task_palette;
|
|
||||||
HBoxContainer *fav_tasks_hbox;
|
|
||||||
|
|
||||||
Button *new_btn;
|
Button *new_btn;
|
||||||
Button *load_btn;
|
Button *load_btn;
|
||||||
|
@ -124,8 +132,10 @@ private:
|
||||||
void _on_tree_rmb(const Vector2 &p_menu_pos);
|
void _on_tree_rmb(const Vector2 &p_menu_pos);
|
||||||
void _action_selected(int p_id);
|
void _action_selected(int p_id);
|
||||||
void _misc_option_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<BTTask> &p_task);
|
void _on_tree_task_selected(const Ref<BTTask> &p_task);
|
||||||
void _on_tree_task_double_clicked();
|
|
||||||
void _on_visibility_changed();
|
void _on_visibility_changed();
|
||||||
void _on_header_pressed();
|
void _on_header_pressed();
|
||||||
void _on_save_pressed();
|
void _on_save_pressed();
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#include "task_tree.h"
|
#include "task_tree.h"
|
||||||
|
|
||||||
#include "modules/limboai/bt/tasks/bt_comment.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 "modules/limboai/util/limbo_utility.h"
|
||||||
|
|
||||||
#include "editor/editor_scale.h"
|
#include "editor/editor_scale.h"
|
||||||
|
@ -34,6 +35,15 @@ void TaskTree::_update_item(TreeItem *p_item) {
|
||||||
if (p_item == nullptr) {
|
if (p_item == nullptr) {
|
||||||
return;
|
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);
|
Ref<BTTask> task = p_item->get_metadata(0);
|
||||||
ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata.");
|
ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata.");
|
||||||
p_item->set_text(0, task->get_task_name());
|
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()) {
|
if (!warning_text.is_empty()) {
|
||||||
p_item->add_button(0, theme_cache.task_warning_icon, 0, false, warning_text);
|
p_item->add_button(0, theme_cache.task_warning_icon, 0, false, warning_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Update probabilities.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskTree::_update_tree() {
|
void TaskTree::_update_tree() {
|
||||||
|
@ -116,8 +124,13 @@ TreeItem *TaskTree::_find_item(const Ref<BTTask> &p_task) const {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, int p_button_index) {
|
void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, MouseButton p_button_index) {
|
||||||
if (p_button_index == 2) {
|
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);
|
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);
|
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
|
||||||
if (last_selected.is_valid()) {
|
if (last_selected.is_valid()) {
|
||||||
update_task(last_selected);
|
update_task(last_selected);
|
||||||
if (last_selected->is_connected("changed", on_task_changed)) {
|
if (last_selected->is_connected(SNAME("changed"), on_task_changed)) {
|
||||||
last_selected->disconnect("changed", on_task_changed);
|
last_selected->disconnect(SNAME("changed"), on_task_changed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
last_selected = get_selected();
|
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);
|
emit_signal(SNAME("task_selected"), last_selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskTree::_on_item_double_clicked() {
|
void TaskTree::_on_item_activated() {
|
||||||
emit_signal(SNAME("task_double_clicked"));
|
emit_signal(SNAME("task_activated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskTree::_on_task_changed() {
|
void TaskTree::_on_task_changed() {
|
||||||
|
@ -153,6 +166,7 @@ void TaskTree::load_bt(const Ref<BehaviorTree> &p_behavior_tree) {
|
||||||
|
|
||||||
bt = p_behavior_tree;
|
bt = p_behavior_tree;
|
||||||
tree->clear();
|
tree->clear();
|
||||||
|
probability_rect_cache.clear();
|
||||||
if (bt->get_root_task().is_valid()) {
|
if (bt->get_root_task().is_valid()) {
|
||||||
_create_tree(bt->get_root_task(), nullptr);
|
_create_tree(bt->get_root_task(), nullptr);
|
||||||
}
|
}
|
||||||
|
@ -190,6 +204,45 @@ 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
double TaskTree::get_selected_probability_percent() 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_probability(probability_selector->get_child_index(selected)) * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) {
|
||||||
if (editable && tree->get_item_at_position(p_point)) {
|
if (editable && tree->get_item_at_position(p_point)) {
|
||||||
Dictionary drag_data;
|
Dictionary drag_data;
|
||||||
|
@ -241,16 +294,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() {
|
void TaskTree::_update_theme_item_cache() {
|
||||||
Control::_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.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.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.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) {
|
void TaskTree::_notification(int p_what) {
|
||||||
|
@ -272,10 +366,12 @@ void TaskTree::_bind_methods() {
|
||||||
ClassDB::bind_method(D_METHOD("_get_drag_data_fw"), &TaskTree::_get_drag_data_fw);
|
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("_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("_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("rmb_pressed"));
|
||||||
ADD_SIGNAL(MethodInfo("task_selected"));
|
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",
|
ADD_SIGNAL(MethodInfo("task_dragged",
|
||||||
PropertyInfo(Variant::OBJECT, "p_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
|
PropertyInfo(Variant::OBJECT, "p_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
|
||||||
PropertyInfo(Variant::OBJECT, "p_to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
|
PropertyInfo(Variant::OBJECT, "p_to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
|
||||||
|
@ -296,7 +392,7 @@ TaskTree::TaskTree() {
|
||||||
tree->set_allow_rmb_select(true);
|
tree->set_allow_rmb_select(true);
|
||||||
tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected));
|
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_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));
|
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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
#include "scene/gui/control.h"
|
#include "scene/gui/control.h"
|
||||||
#include "scene/gui/tree.h"
|
#include "scene/gui/tree.h"
|
||||||
|
#include "scene/resources/style_box.h"
|
||||||
|
|
||||||
class TaskTree : public Control {
|
class TaskTree : public Control {
|
||||||
GDCLASS(TaskTree, Control);
|
GDCLASS(TaskTree, Control);
|
||||||
|
@ -22,15 +23,24 @@ private:
|
||||||
Ref<BehaviorTree> bt;
|
Ref<BehaviorTree> bt;
|
||||||
Ref<BTTask> last_selected;
|
Ref<BTTask> last_selected;
|
||||||
bool editable;
|
bool editable;
|
||||||
|
HashMap<ObjectID, Rect2> probability_rect_cache;
|
||||||
|
|
||||||
struct ThemeCache {
|
struct ThemeCache {
|
||||||
Ref<Font> comment_font;
|
Ref<Font> comment_font;
|
||||||
|
Ref<Font> name_font;
|
||||||
Ref<Font> custom_name_font;
|
Ref<Font> custom_name_font;
|
||||||
Ref<Font> normal_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;
|
Ref<Texture2D> task_warning_icon;
|
||||||
|
|
||||||
Color comment_color;
|
Color comment_color;
|
||||||
|
Color probability_font_color;
|
||||||
|
|
||||||
|
Ref<StyleBoxFlat> probability_bg;
|
||||||
} theme_cache;
|
} theme_cache;
|
||||||
|
|
||||||
TreeItem *_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent, int p_idx = -1);
|
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;
|
TreeItem *_find_item(const Ref<BTTask> &p_task) const;
|
||||||
|
|
||||||
void _on_item_selected();
|
void _on_item_selected();
|
||||||
void _on_item_double_clicked();
|
void _on_item_activated();
|
||||||
void _on_item_mouse_selected(const Vector2 &p_pos, int p_button_index);
|
void _on_item_mouse_selected(const Vector2 &p_pos, MouseButton p_button_index);
|
||||||
void _on_task_changed();
|
void _on_task_changed();
|
||||||
|
|
||||||
Variant _get_drag_data_fw(const Point2 &p_point);
|
Variant _get_drag_data_fw(const Point2 &p_point);
|
||||||
bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data) const;
|
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 _drop_data_fw(const Point2 &p_point, const Variant &p_data);
|
||||||
|
|
||||||
|
void _draw_probability(Object *item_obj, Rect2 rect);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void _update_theme_item_cache() override;
|
virtual void _update_theme_item_cache() override;
|
||||||
|
|
||||||
|
@ -62,6 +74,11 @@ public:
|
||||||
Ref<BTTask> get_selected() const;
|
Ref<BTTask> get_selected() const;
|
||||||
void deselect();
|
void deselect();
|
||||||
|
|
||||||
|
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; }
|
virtual bool editor_can_reload_from_file() { return false; }
|
||||||
|
|
||||||
TaskTree();
|
TaskTree();
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<svg enable-background="new 0 0 16 16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#8da5f3"><path d="m16 12c-2.27-.89-5.09-2.4-6.84-4l1.03 3h-10.19v2h10.19l-1.03 3c1.75-1.6 4.57-3.11 6.84-4z"/><path d="m4 1.99c0-1.1-.9-1.99-1.99-1.99-1.11 0-2.01.89-2.01 1.98 0 1.13.89 2.02 2.01 2.02 1.1 0 1.99-.9 1.99-2.01zm-1.99 1.51c-.85 0-1.51-.67-1.51-1.51 0-.82.67-1.49 1.51-1.49.82 0 1.49.67 1.49 1.49 0 .83-.67 1.51-1.49 1.51z"/><path d="m8.24 6.23c0-1.1-.89-1.99-1.99-1.99-1.11 0-2.01.89-2.01 1.98 0 1.13.89 2.01 2.01 2.01 1.1.01 1.99-.89 1.99-2zm-1.99 1.51c-.85 0-1.51-.66-1.51-1.51 0-.82.67-1.49 1.51-1.49.82 0 1.49.67 1.49 1.49 0 .84-.67 1.51-1.49 1.51z"/><path d="m-.88 3.62h10v1h-10z" transform="matrix(.7071 -.7071 .7071 .7071 -1.7066 4.1199)"/></g></svg>
|
<svg enable-background="new 0 0 16 16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#8da5f3"><path d="m16 12c-2.27-.89-5.09-2.4-6.84-4l1.03 3h-10.19v2h10.19l-1.03 3c1.75-1.6 4.57-3.11 6.84-4z"/><path d="m2.01 0c-1.11 0-2.01.89-2.01 1.98 0 1.13.89 2.02 2.01 2.02 1.1 0 1.99-.9 1.99-2.01 0-1.1-.9-1.99-1.99-1.99zm-1.02 1.99c0-.55.45-1 1.01-1 .55 0 1 .45 1 1 0 .56-.45 1.01-1 1.01-.56.01-1.01-.44-1.01-1.01z"/><path d="m6.25 4.24c-1.11 0-2.01.89-2.01 1.98 0 1.13.89 2.01 2.01 2.01 1.1 0 1.99-.9 1.98-2.01.01-1.08-.88-1.98-1.98-1.98zm-1.02 1.99c0-.55.45-1 1.01-1 .55 0 1 .45 1 1 0 .56-.45 1.01-1 1.01-.56.01-1.01-.44-1.01-1.01z"/><path d="m-.88 3.62h10v1h-10z" transform="matrix(.7071 -.7071 .7071 .7071 -1.7066 4.1199)"/></g></svg>
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 747 B |
|
@ -59,6 +59,7 @@
|
||||||
#include "bt/tasks/composites/bt_dynamic_selector.h"
|
#include "bt/tasks/composites/bt_dynamic_selector.h"
|
||||||
#include "bt/tasks/composites/bt_dynamic_sequence.h"
|
#include "bt/tasks/composites/bt_dynamic_sequence.h"
|
||||||
#include "bt/tasks/composites/bt_parallel.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_selector.h"
|
||||||
#include "bt/tasks/composites/bt_random_sequence.h"
|
#include "bt/tasks/composites/bt_random_sequence.h"
|
||||||
#include "bt/tasks/composites/bt_selector.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(BTParallel);
|
||||||
LIMBO_REGISTER_TASK(BTDynamicSequence);
|
LIMBO_REGISTER_TASK(BTDynamicSequence);
|
||||||
LIMBO_REGISTER_TASK(BTDynamicSelector);
|
LIMBO_REGISTER_TASK(BTDynamicSelector);
|
||||||
|
LIMBO_REGISTER_TASK(BTProbabilitySelector);
|
||||||
LIMBO_REGISTER_TASK(BTRandomSequence);
|
LIMBO_REGISTER_TASK(BTRandomSequence);
|
||||||
LIMBO_REGISTER_TASK(BTRandomSelector);
|
LIMBO_REGISTER_TASK(BTRandomSelector);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
/**
|
||||||
|
* 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<BTProbabilitySelector> sel = memnew(BTProbabilitySelector);
|
||||||
|
|
||||||
|
SUBCASE("When empty") {
|
||||||
|
ERR_PRINT_OFF;
|
||||||
|
CHECK(sel->execute(0.01666) == BTTask::FAILURE);
|
||||||
|
ERR_PRINT_ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref<BTTestAction> task1 = memnew(BTTestAction);
|
||||||
|
Ref<BTTestAction> task2 = memnew(BTTestAction);
|
||||||
|
Ref<BTTestAction> 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);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
#endif // TEST_PROBABILITY_SELECTOR_H
|
Loading…
Reference in New Issue