Merge branch 'probability-selector'

This commit is contained in:
Serhii Snitsaruk 2023-09-26 11:12:57 +02:00
commit cd299bef4b
12 changed files with 661 additions and 26 deletions

View File

@ -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");
}

View File

@ -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

View File

@ -84,6 +84,7 @@ def get_doc_classes():
"BTPlayAnimation",
"BTPlayer",
"BTProbability",
"BTProbabilitySelector",
"BTRandomSelector",
"BTRandomSequence",
"BTRandomWait",

View File

@ -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")

View File

@ -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")

View File

@ -15,11 +15,13 @@
#include "action_banner.h"
#include "modules/limboai/bt/tasks/bt_comment.h"
#include "modules/limboai/bt/tasks/composites/bt_probability_selector.h"
#include "modules/limboai/bt/tasks/composites/bt_selector.h"
#include "modules/limboai/editor/debugger/limbo_debugger_plugin.h"
#include "modules/limboai/util/limbo_utility.h"
#include "core/config/project_settings.h"
#include "core/error/error_macros.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/debugger/script_editor_debugger.h"
#include "editor/editor_file_system.h"
@ -31,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
@ -267,10 +270,13 @@ void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) {
Ref<BTTask> task = task_tree->get_selected();
ERR_FAIL_COND_MSG(task.is_null(), "LimboAIEditor: get_selected() returned null");
if (task_tree->selected_has_probability()) {
menu->add_item(TTR("Edit Probability"), ACTION_EDIT_PROBABILITY);
}
menu->add_icon_shortcut(theme_cache.rename_task_icon, ED_GET_SHORTCUT("limbo_ai/rename_task"), ACTION_RENAME);
menu->add_icon_item(theme_cache.edit_script_icon, TTR("Edit Script"), ACTION_EDIT_SCRIPT);
menu->add_icon_item(theme_cache.open_doc_icon, TTR("Open Documentation"), ACTION_OPEN_DOC);
menu->set_item_disabled(ACTION_EDIT_SCRIPT, task->get_script().is_null());
menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script().is_null());
menu->add_separator();
menu->add_icon_shortcut(theme_cache.move_task_up_icon, ED_GET_SHORTCUT("limbo_ai/move_task_up"), ACTION_MOVE_UP);
@ -308,6 +314,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);
_update_probability_edit();
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 +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) {
switch (p_id) {
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);
}
void LimboAIEditor::_on_tree_task_double_clicked() {
_action_selected(ACTION_RENAME);
}
void LimboAIEditor::_on_visibility_changed() {
if (task_tree->is_visible_in_tree()) {
Ref<BTTask> sel = task_tree->get_selected();
@ -774,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();
@ -925,7 +978,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 +1011,52 @@ LimboAIEditor::LimboAIEditor() {
add_child(menu);
menu->connect("id_pressed", callable_mp(this, &LimboAIEditor::_action_selected));
probability_popup = memnew(PopupPanel);
{
VBoxContainer *vbc = memnew(VBoxContainer);
probability_popup->add_child(vbc);
PanelContainer *mode_panel = memnew(PanelContainer);
vbc->add_child(mode_panel);
HBoxContainer *mode_hbox = memnew(HBoxContainer);
mode_panel->add_child(mode_hbox);
Ref<ButtonGroup> button_group;
button_group.instantiate();
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);
{
VBoxContainer *vbc = memnew(VBoxContainer);

View File

@ -23,6 +23,7 @@
#include "core/templates/hash_set.h"
#include "editor/editor_node.h"
#include "editor/editor_plugin.h"
#include "editor/gui/editor_spin_slider.h"
#include "scene/gui/box_container.h"
#include "scene/gui/control.h"
#include "scene/gui/file_dialog.h"
@ -41,6 +42,7 @@ class LimboAIEditor : public Control {
private:
enum Action {
ACTION_RENAME,
ACTION_EDIT_PROBABILITY,
ACTION_EDIT_SCRIPT,
ACTION_OPEN_DOC,
ACTION_MOVE_UP,
@ -79,12 +81,18 @@ private:
VBoxContainer *banners;
Panel *usage_hint;
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 *load_dialog;
Button *history_back;
Button *history_forward;
TaskPalette *task_palette;
HBoxContainer *fav_tasks_hbox;
Button *new_btn;
Button *load_btn;
@ -124,8 +132,10 @@ 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 _update_probability_edit();
void _probability_popup_closed();
void _on_tree_task_selected(const Ref<BTTask> &p_task);
void _on_tree_task_double_clicked();
void _on_visibility_changed();
void _on_header_pressed();
void _on_save_pressed();

View File

@ -12,6 +12,7 @@
#include "task_tree.h"
#include "modules/limboai/bt/tasks/bt_comment.h"
#include "modules/limboai/bt/tasks/composites/bt_probability_selector.h"
#include "modules/limboai/util/limbo_utility.h"
#include "editor/editor_scale.h"
@ -34,6 +35,15 @@ void TaskTree::_update_item(TreeItem *p_item) {
if (p_item == nullptr) {
return;
}
if (p_item->get_parent()) {
Ref<BTProbabilitySelector> sel = p_item->get_parent()->get_metadata(0);
if (sel.is_valid()) {
p_item->set_custom_draw(0, this, SNAME("_draw_probability"));
p_item->set_cell_mode(0, TreeItem::CELL_MODE_CUSTOM);
}
}
Ref<BTTask> task = p_item->get_metadata(0);
ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata.");
p_item->set_text(0, task->get_task_name());
@ -72,8 +82,6 @@ void TaskTree::_update_item(TreeItem *p_item) {
if (!warning_text.is_empty()) {
p_item->add_button(0, theme_cache.task_warning_icon, 0, false, warning_text);
}
// TODO: Update probabilities.
}
void TaskTree::_update_tree() {
@ -116,8 +124,13 @@ TreeItem *TaskTree::_find_item(const Ref<BTTask> &p_task) const {
return item;
}
void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, int p_button_index) {
if (p_button_index == 2) {
void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, MouseButton p_button_index) {
if (p_button_index == MouseButton::LEFT) {
Rect2 rect = get_selected_probability_rect();
if (rect != Rect2() && rect.has_point(p_pos)) {
emit_signal(SNAME("probability_clicked"));
}
} else if (p_button_index == MouseButton::RIGHT) {
emit_signal(SNAME("rmb_pressed"), get_screen_position() + p_pos);
}
}
@ -126,17 +139,17 @@ void TaskTree::_on_item_selected() {
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid()) {
update_task(last_selected);
if (last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
if (last_selected->is_connected(SNAME("changed"), on_task_changed)) {
last_selected->disconnect(SNAME("changed"), on_task_changed);
}
}
last_selected = get_selected();
last_selected->connect("changed", on_task_changed);
last_selected->connect(SNAME("changed"), on_task_changed);
emit_signal(SNAME("task_selected"), last_selected);
}
void TaskTree::_on_item_double_clicked() {
emit_signal(SNAME("task_double_clicked"));
void TaskTree::_on_item_activated() {
emit_signal(SNAME("task_activated"));
}
void TaskTree::_on_task_changed() {
@ -153,6 +166,7 @@ void TaskTree::load_bt(const Ref<BehaviorTree> &p_behavior_tree) {
bt = p_behavior_tree;
tree->clear();
probability_rect_cache.clear();
if (bt->get_root_task().is_valid()) {
_create_tree(bt->get_root_task(), nullptr);
}
@ -190,6 +204,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) {
if (editable && tree->get_item_at_position(p_point)) {
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() {
Control::_update_theme_item_cache();
theme_cache.comment_font = get_theme_font(SNAME("doc_italic"), SNAME("EditorFonts"));
theme_cache.name_font = get_theme_font(SNAME("font"));
theme_cache.custom_name_font = get_theme_font(SNAME("bold"), SNAME("EditorFonts"));
// theme_cache.normal_name_font = Ref<Font>(nullptr);
theme_cache.comment_font = get_theme_font(SNAME("doc_italic"), SNAME("EditorFonts"));
theme_cache.probability_font = get_theme_font(SNAME("font"));
theme_cache.name_font_size = get_theme_font_size("font_size");
theme_cache.probability_font_size = Math::floor(get_theme_font_size("font_size") * 0.9);
theme_cache.task_warning_icon = get_theme_icon(SNAME("NodeWarning"), SNAME("EditorIcons"));
theme_cache.comment_color = get_theme_color(SNAME("disabled_font_color"), SNAME("Editor"));
theme_cache.probability_font_color = get_theme_color(SNAME("font_color"), SNAME("Editor"));
theme_cache.probability_bg.instantiate();
theme_cache.probability_bg->set_bg_color(get_theme_color(SNAME("accent_color"), SNAME("Editor")) * Color(1, 1, 1, 0.25));
theme_cache.probability_bg->set_corner_radius_all(12.0 * EDSCALE);
}
void TaskTree::_notification(int p_what) {
@ -272,10 +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("_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 +392,7 @@ TaskTree::TaskTree() {
tree->set_allow_rmb_select(true);
tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected));
tree->connect("item_selected", callable_mp(this, &TaskTree::_on_item_selected));
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_double_clicked));
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated));
tree->set_drag_forwarding(callable_mp(this, &TaskTree::_get_drag_data_fw), callable_mp(this, &TaskTree::_can_drop_data_fw), callable_mp(this, &TaskTree::_drop_data_fw));
}

View File

@ -13,6 +13,7 @@
#include "scene/gui/control.h"
#include "scene/gui/tree.h"
#include "scene/resources/style_box.h"
class TaskTree : public Control {
GDCLASS(TaskTree, Control);
@ -22,15 +23,24 @@ private:
Ref<BehaviorTree> bt;
Ref<BTTask> last_selected;
bool editable;
HashMap<ObjectID, Rect2> probability_rect_cache;
struct ThemeCache {
Ref<Font> comment_font;
Ref<Font> name_font;
Ref<Font> custom_name_font;
Ref<Font> normal_name_font;
Ref<Font> probability_font;
double name_font_size = 18.0;
double probability_font_size = 16.0;
Ref<Texture2D> task_warning_icon;
Color comment_color;
Color probability_font_color;
Ref<StyleBoxFlat> probability_bg;
} theme_cache;
TreeItem *_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent, int p_idx = -1);
@ -39,14 +49,16 @@ private:
TreeItem *_find_item(const Ref<BTTask> &p_task) const;
void _on_item_selected();
void _on_item_double_clicked();
void _on_item_mouse_selected(const Vector2 &p_pos, int p_button_index);
void _on_item_activated();
void _on_item_mouse_selected(const Vector2 &p_pos, MouseButton p_button_index);
void _on_task_changed();
Variant _get_drag_data_fw(const Point2 &p_point);
bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data) const;
void _drop_data_fw(const Point2 &p_point, const Variant &p_data);
void _draw_probability(Object *item_obj, Rect2 rect);
protected:
virtual void _update_theme_item_cache() override;
@ -62,6 +74,11 @@ public:
Ref<BTTask> get_selected() const;
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; }
TaskTree();

View File

@ -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

View File

@ -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);

View File

@ -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