Implement basics of behavior tree debugger

This commit is contained in:
Serhii Snitsaruk 2023-04-13 09:29:45 +02:00
parent 9344bce9b1
commit 9abfeadc21
19 changed files with 682 additions and 34 deletions

1
SCsub
View File

@ -12,5 +12,6 @@ module_env.add_source_files(env.modules_sources, "bt/actions/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/decorators/*.cpp") module_env.add_source_files(env.modules_sources, "bt/decorators/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/conditions/*.cpp") module_env.add_source_files(env.modules_sources, "bt/conditions/*.cpp")
module_env.add_source_files(env.modules_sources, "bb_param/*.cpp") module_env.add_source_files(env.modules_sources, "bb_param/*.cpp")
module_env.add_source_files(env.modules_sources, "debugger/*.cpp")
if env.editor_build: if env.editor_build:
module_env.add_source_files(env.modules_sources, "editor/*.cpp") module_env.add_source_files(env.modules_sources, "editor/*.cpp")

View File

@ -12,10 +12,16 @@
#include "core/os/memory.h" #include "core/os/memory.h"
#include "core/variant/variant.h" #include "core/variant/variant.h"
#include "modules/limboai/blackboard.h" #include "modules/limboai/blackboard.h"
#include "modules/limboai/debugger/limbo_debugger.h"
VARIANT_ENUM_CAST(BTPlayer::UpdateMode); VARIANT_ENUM_CAST(BTPlayer::UpdateMode);
void BTPlayer::_load_tree() { void BTPlayer::_load_tree() {
#ifdef DEBUG_ENABLED
if (tree_instance.is_valid()) {
LimboDebugger::get_singleton()->unregister_bt_instance(tree_instance, get_path());
}
#endif
tree_instance.unref(); tree_instance.unref();
ERR_FAIL_COND_MSG(!behavior_tree.is_valid(), "BTPlayer: Needs a valid behavior tree."); ERR_FAIL_COND_MSG(!behavior_tree.is_valid(), "BTPlayer: Needs a valid behavior tree.");
ERR_FAIL_COND_MSG(!behavior_tree->get_root_task().is_valid(), "BTPlayer: Behavior tree has no valid root task."); ERR_FAIL_COND_MSG(!behavior_tree->get_root_task().is_valid(), "BTPlayer: Behavior tree has no valid root task.");
@ -23,6 +29,9 @@ void BTPlayer::_load_tree() {
blackboard->prefetch_nodepath_vars(this); blackboard->prefetch_nodepath_vars(this);
} }
tree_instance = behavior_tree->instantiate(get_owner(), blackboard); tree_instance = behavior_tree->instantiate(get_owner(), blackboard);
#ifdef DEBUG_ENABLED
LimboDebugger::get_singleton()->register_bt_instance(tree_instance, get_path());
#endif
} }
void BTPlayer::set_behavior_tree(const Ref<BehaviorTree> &p_tree) { void BTPlayer::set_behavior_tree(const Ref<BehaviorTree> &p_tree) {
@ -52,6 +61,7 @@ void BTPlayer::update(double p_delta) {
} }
if (active) { if (active) {
last_status = tree_instance->execute(p_delta); last_status = tree_instance->execute(p_delta);
emit_signal(LimboStringNames::get_singleton()->updated, last_status);
if (last_status == BTTask::SUCCESS || last_status == BTTask::FAILURE) { if (last_status == BTTask::SUCCESS || last_status == BTTask::FAILURE) {
emit_signal(LimboStringNames::get_singleton()->behavior_tree_finished, last_status); emit_signal(LimboStringNames::get_singleton()->behavior_tree_finished, last_status);
} }
@ -81,6 +91,13 @@ void BTPlayer::_notification(int p_notification) {
set_active(active); set_active(active);
} }
} break; } break;
#ifdef DEBUG_ENABLED
case NOTIFICATION_EXIT_TREE: {
if (tree_instance.is_valid()) {
LimboDebugger::get_singleton()->unregister_bt_instance(tree_instance, get_path());
}
} break;
#endif
} }
} }
@ -115,6 +132,7 @@ void BTPlayer::_bind_methods() {
BIND_ENUM_CONSTANT(MANUAL); BIND_ENUM_CONSTANT(MANUAL);
ADD_SIGNAL(MethodInfo("behavior_tree_finished", PropertyInfo(Variant::INT, "p_status"))); ADD_SIGNAL(MethodInfo("behavior_tree_finished", PropertyInfo(Variant::INT, "p_status")));
ADD_SIGNAL(MethodInfo("updated", PropertyInfo(Variant::INT, "p_status")));
} }
BTPlayer::BTPlayer() { BTPlayer::BTPlayer() {

View File

@ -5,11 +5,17 @@
#include "core/object/class_db.h" #include "core/object/class_db.h"
#include "core/variant/variant.h" #include "core/variant/variant.h"
#include "modules/limboai/bt/bt_task.h" #include "modules/limboai/bt/bt_task.h"
#include "modules/limboai/debugger/limbo_debugger.h"
#include "modules/limboai/limbo_state.h" #include "modules/limboai/limbo_state.h"
#include "modules/limboai/limbo_string_names.h"
void BTState::_setup() { void BTState::_setup() {
ERR_FAIL_COND_MSG(behavior_tree.is_null(), "BTState: BehaviorTree is not assigned."); ERR_FAIL_COND_MSG(behavior_tree.is_null(), "BTState: BehaviorTree is not assigned.");
tree_instance = behavior_tree->instantiate(get_agent(), get_blackboard()); tree_instance = behavior_tree->instantiate(get_agent(), get_blackboard());
#ifdef DEBUG_ENABLED
LimboDebugger::get_singleton()->register_bt_instance(tree_instance, get_path());
#endif
} }
void BTState::_exit() { void BTState::_exit() {
@ -20,6 +26,7 @@ void BTState::_exit() {
void BTState::_update(double p_delta) { void BTState::_update(double p_delta) {
ERR_FAIL_COND(tree_instance == nullptr); ERR_FAIL_COND(tree_instance == nullptr);
int status = tree_instance->execute(p_delta); int status = tree_instance->execute(p_delta);
emit_signal(LimboStringNames::get_singleton()->updated, p_delta);
if (status == BTTask::SUCCESS) { if (status == BTTask::SUCCESS) {
get_root()->dispatch(success_event, Variant()); get_root()->dispatch(success_event, Variant());
} else if (status == BTTask::FAILURE) { } else if (status == BTTask::FAILURE) {
@ -27,6 +34,16 @@ void BTState::_update(double p_delta) {
} }
} }
#ifdef DEBUG_ENABLED
void BTState::_notification(int p_notification) {
switch (p_notification) {
case NOTIFICATION_EXIT_TREE: {
LimboDebugger::get_singleton()->unregister_bt_instance(tree_instance, get_path());
} break;
}
}
#endif // DEBUG_ENABLED
void BTState::_bind_methods() { void BTState::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_behavior_tree", "p_value"), &BTState::set_behavior_tree); ClassDB::bind_method(D_METHOD("set_behavior_tree", "p_value"), &BTState::set_behavior_tree);
ClassDB::bind_method(D_METHOD("get_behavior_tree"), &BTState::get_behavior_tree); ClassDB::bind_method(D_METHOD("get_behavior_tree"), &BTState::get_behavior_tree);

View File

@ -36,6 +36,12 @@ public:
String get_failure_event() const { return failure_event; } String get_failure_event() const { return failure_event; }
BTState(); BTState();
#ifdef DEBUG_ENABLED
protected:
void _notification(int p_notification);
#endif
}; };
#endif // BT_STATE_H #endif // BT_STATE_H

View File

@ -140,6 +140,7 @@ int BTTask::execute(double p_delta) {
if (!GDVIRTUAL_CALL(_tick, p_delta, status)) { if (!GDVIRTUAL_CALL(_tick, p_delta, status)) {
status = _tick(p_delta); status = _tick(p_delta);
} }
last_tick_usec = Engine::get_singleton()->get_frame_ticks();
if (status != RUNNING) { if (status != RUNNING) {
if (!GDVIRTUAL_CALL(_exit)) { if (!GDVIRTUAL_CALL(_exit)) {
@ -311,6 +312,7 @@ BTTask::BTTask() {
parent = nullptr; parent = nullptr;
children = Vector<Ref<BTTask>>(); children = Vector<Ref<BTTask>>();
status = FRESH; status = FRESH;
last_tick_usec = 0;
} }
BTTask::~BTTask() { BTTask::~BTTask() {

View File

@ -33,6 +33,7 @@ private:
BTTask *parent; BTTask *parent;
Vector<Ref<BTTask>> children; Vector<Ref<BTTask>> children;
int status; int status;
int last_tick_usec;
Array _get_children() const; Array _get_children() const;
void _set_children(Array children); void _set_children(Array children);
@ -75,6 +76,7 @@ public:
int execute(double p_delta); int execute(double p_delta);
void cancel(); void cancel();
int get_status() const { return status; } int get_status() const { return status; }
int get_last_tick_usec() const { return last_tick_usec; }
Ref<BTTask> get_child(int p_idx) const; Ref<BTTask> get_child(int p_idx) const;
int get_child_count() const; int get_child_count() const;

View File

@ -0,0 +1,54 @@
/* behavior_tree_data.cpp */
#include "behavior_tree_data.h"
#include "core/templates/list.h"
//// BehaviorTreeData
BehaviorTreeData::BehaviorTreeData(const Ref<BTTask> &p_instance) {
// Flatten tree into list depth first
List<Ref<BTTask>> stack;
stack.push_back(p_instance);
while (stack.size()) {
Ref<BTTask> task = stack[0];
stack.pop_front();
int num_children = task->get_child_count();
for (int i = 0; i < num_children; i++) {
stack.push_front(task->get_child(num_children - 1 - i));
}
tasks.push_back(TaskData(
task->get_task_name(),
num_children,
task->get_status(),
task->get_last_tick_usec(),
task->get_class()));
}
}
void BehaviorTreeData::serialize(Array &p_arr) {
for (const TaskData &td : tasks) {
p_arr.push_back(td.name);
p_arr.push_back(td.num_children);
p_arr.push_back(td.status);
p_arr.push_back(td.last_tick_usec);
p_arr.push_back(td.type_name);
}
}
void BehaviorTreeData::deserialize(const Array &p_arr) {
ERR_FAIL_COND(tasks.size() != 0);
int idx = 0;
while (p_arr.size() > idx) {
ERR_FAIL_COND(p_arr.size() < 5);
ERR_FAIL_COND(p_arr[idx].get_type() != Variant::STRING);
ERR_FAIL_COND(p_arr[idx + 1].get_type() != Variant::INT);
ERR_FAIL_COND(p_arr[idx + 2].get_type() != Variant::INT);
ERR_FAIL_COND(p_arr[idx + 3].get_type() != Variant::INT);
ERR_FAIL_COND(p_arr[idx + 4].get_type() != Variant::STRING);
tasks.push_back(TaskData(p_arr[idx], p_arr[idx + 1], p_arr[idx + 2], p_arr[idx + 3], p_arr[idx + 4]));
idx += 5;
}
}

View File

@ -0,0 +1,41 @@
/* behavior_tree_data.h */
#ifndef BEHAVIOR_TREE_DATA_H
#define BEHAVIOR_TREE_DATA_H
#include "core/object/object.h"
#include "modules/limboai/bt/bt_task.h"
class BehaviorTreeData {
public:
struct TaskData {
String name;
// ObjectID object_id;
int num_children = 0;
int status = 0;
int last_tick_usec = 0;
String type_name;
// String script_path;
// String resource_path;
TaskData(const String &p_name, int p_num_children, int p_status, int p_last_tick_usec, const String &p_type_name) {
name = p_name;
num_children = p_num_children;
status = p_status;
last_tick_usec = p_last_tick_usec;
type_name = p_type_name;
}
TaskData() {}
};
List<TaskData> tasks;
void serialize(Array &p_arr);
void deserialize(const Array &p_arr);
BehaviorTreeData(const Ref<BTTask> &p_instance);
BehaviorTreeData() {}
};
#endif // BEHAVIOR_TREE_DATA

View File

@ -0,0 +1,65 @@
/* behavior_tree_view.cpp */
#include "behavior_tree_view.h"
#include "behavior_tree_data.h"
#include "core/math/color.h"
#include "modules/limboai/bt/bt_task.h"
#include "modules/limboai/limbo_utility.h"
void BehaviorTreeView::update_tree(const BehaviorTreeData &p_data) {
tree->clear();
int recent_tick_usec = -1;
TreeItem *parent = nullptr;
List<Pair<TreeItem *, int>> parents;
for (const BehaviorTreeData::TaskData &task_data : p_data.tasks) {
// Figure out parent.
parent = nullptr;
if (parents.size()) {
Pair<TreeItem *, int> &p = parents[0];
parent = p.first;
if (!(--p.second)) {
// No children left, remove it.
parents.pop_front();
}
}
TreeItem *item = tree->create_item(parent);
item->set_text(0, task_data.name);
item->set_icon(0, LimboUtility::get_singleton()->get_task_icon(task_data.type_name));
// First task in list is the root task and has most recent tick time.
if (recent_tick_usec == -1) {
recent_tick_usec = task_data.last_tick_usec;
}
// Item BG color depends on age of the task status.
int timediff = recent_tick_usec - task_data.last_tick_usec;
if (timediff <= 300000) {
float alpha = (300000 - timediff) / 300000.0;
if (task_data.status == BTTask::SUCCESS) {
item->set_custom_bg_color(0, Color(0, 0.5, 0, alpha));
} else if (task_data.status == BTTask::FAILURE) {
item->set_custom_bg_color(0, Color(0.5, 0, 0, alpha));
} else if (task_data.status == BTTask::RUNNING) {
item->set_custom_bg_color(0, Color(0.5, 0.5, 0, alpha));
}
}
// Add in front of parents stack if it expects children.
if (task_data.num_children) {
parents.push_front(Pair<TreeItem *, int>(item, task_data.num_children));
}
}
}
void BehaviorTreeView::clear() {
tree->clear();
}
BehaviorTreeView::BehaviorTreeView() {
tree = memnew(Tree);
add_child(tree);
tree->set_columns(1);
tree->set_column_expand(0, true);
tree->set_anchor(SIDE_RIGHT, ANCHOR_END);
tree->set_anchor(SIDE_BOTTOM, ANCHOR_END);
}

View File

@ -0,0 +1,25 @@
/* behavior_tree_view.h */
#ifndef BEHAVIOR_TREE_VIEW_H
#define BEHAVIOR_TREE_VIEW_H
#include "behavior_tree_data.h"
#include "core/object/class_db.h"
#include "core/object/object.h"
#include "scene/gui/control.h"
#include "scene/gui/tree.h"
class BehaviorTreeView : public Control {
GDCLASS(BehaviorTreeView, Control);
private:
Tree *tree;
public:
BehaviorTreeView();
void update_tree(const BehaviorTreeData &p_data);
void clear();
};
#endif // BEHAVIOR_TREE_VIEW

144
debugger/limbo_debugger.cpp Normal file
View File

@ -0,0 +1,144 @@
/* limbo_debugger.cpp */
#include "limbo_debugger.h"
#include "behavior_tree_data.h"
#include "core/debugger/engine_debugger.h"
#include "core/error/error_macros.h"
#include "core/string/node_path.h"
#include "modules/limboai/bt/bt_task.h"
#include "scene/main/scene_tree.h"
#include "scene/main/window.h"
//// LimboDebugger
LimboDebugger *LimboDebugger::singleton = nullptr;
LimboDebugger *LimboDebugger::get_singleton() {
return singleton;
}
LimboDebugger::LimboDebugger() {
singleton = this;
#ifdef DEBUG_ENABLED
EngineDebugger::register_message_capture("limboai", EngineDebugger::Capture(nullptr, LimboDebugger::parse_message));
#endif
}
LimboDebugger::~LimboDebugger() {
singleton = nullptr;
}
void LimboDebugger::initialize() {
if (EngineDebugger::is_active()) {
memnew(LimboDebugger);
}
}
void LimboDebugger::deinitialize() {
if (singleton) {
memdelete(singleton);
}
}
#ifdef DEBUG_ENABLED
Error LimboDebugger::parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured) {
r_captured = true;
if (p_msg == "track_bt_updates") {
singleton->_track_tree(p_args[0]);
} else if (p_msg == "untrack_bt_updates") {
// unregister bt for updates
} else if (p_msg == "start_session") {
singleton->session_active = true;
singleton->_send_active_behavior_trees();
} else if (p_msg == "stop_session") {
singleton->session_active = false;
} else {
r_captured = false;
}
return OK;
}
void LimboDebugger::register_bt_instance(Ref<BTTask> p_instance, NodePath p_path) {
ERR_FAIL_COND(active_trees.has(p_path));
active_trees.insert(p_path, p_instance);
if (session_active) {
_send_active_behavior_trees();
}
}
void LimboDebugger::unregister_bt_instance(Ref<BTTask> p_instance, NodePath p_path) {
ERR_FAIL_COND(!active_trees.has(p_path));
if (tracked_tree == p_path) {
_untrack_tree();
}
active_trees.erase(p_path);
if (session_active) {
_send_active_behavior_trees();
}
}
void LimboDebugger::_track_tree(NodePath p_path) {
ERR_FAIL_COND(!active_trees.has(p_path));
if (!tracked_tree.is_empty()) {
_untrack_tree();
}
Node *node = SceneTree::get_singleton()->get_root()->get_node(p_path);
ERR_FAIL_COND(node == nullptr);
tracked_tree = p_path;
if (node->is_class("BTPlayer")) {
node->connect(SNAME("updated"), callable_mp(this, &LimboDebugger::_on_bt_updated).bind(p_path));
} else if (node->is_class("BTState")) {
node->connect(SNAME("updated"), callable_mp(this, &LimboDebugger::_on_state_updated).bind(p_path));
}
}
void LimboDebugger::_untrack_tree() {
if (tracked_tree.is_empty()) {
return;
}
NodePath was_tracking = tracked_tree;
tracked_tree = NodePath();
Node *node = SceneTree::get_singleton()->get_root()->get_node(was_tracking);
ERR_FAIL_COND(node == nullptr);
if (node->is_class("BTPlayer")) {
node->disconnect(SNAME("updated"), callable_mp(this, &LimboDebugger::_on_bt_updated));
} else if (node->is_class("BTState")) {
node->disconnect(SNAME("updated"), callable_mp(this, &LimboDebugger::_on_state_updated));
}
}
void LimboDebugger::_send_active_behavior_trees() {
Array arr;
for (KeyValue<NodePath, Ref<BTTask>> kv : active_trees) {
arr.append(kv.key);
}
EngineDebugger::get_singleton()->send_message("limboai:active_behavior_trees", arr);
}
void LimboDebugger::_on_bt_updated(int _status, NodePath p_path) {
if (p_path != tracked_tree) {
return;
}
Array arr;
BehaviorTreeData(active_trees.get(tracked_tree)).serialize(arr);
EngineDebugger::get_singleton()->send_message("limboai:bt_update", arr);
}
void LimboDebugger::_on_state_updated(float _delta, NodePath p_path) {
if (p_path != tracked_tree) {
return;
}
Array arr;
BehaviorTreeData(active_trees.get(tracked_tree)).serialize(arr);
EngineDebugger::get_singleton()->send_message("limboai:bt_update", arr);
}
#endif // DEBUG_ENABLED

47
debugger/limbo_debugger.h Normal file
View File

@ -0,0 +1,47 @@
/* limbo_debugger.h */
#ifndef LIMBO_DEBUGGER_H
#define LIMBO_DEBUGGER_H
#include "core/object/class_db.h"
#include "core/object/object.h"
#include "core/string/node_path.h"
#include "modules/limboai/bt/bt_task.h"
class LimboDebugger : public Object {
GDCLASS(LimboDebugger, Object);
private:
static LimboDebugger *singleton;
LimboDebugger();
public:
static void initialize();
static void deinitialize();
static LimboDebugger *get_singleton();
~LimboDebugger();
#ifdef DEBUG_ENABLED
private:
HashMap<NodePath, Ref<BTTask>> active_trees;
NodePath tracked_tree;
bool session_active = false;
void _track_tree(NodePath p_path);
void _untrack_tree();
void _send_active_behavior_trees();
void _on_bt_updated(int status, NodePath p_path);
void _on_state_updated(float _delta, NodePath p_path);
public:
static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
void register_bt_instance(Ref<BTTask> p_instance, NodePath p_path);
void unregister_bt_instance(Ref<BTTask> p_instance, NodePath p_path);
#endif // DEBUG_ENABLED
};
#endif // LIMBO_DEBUGGER

View File

@ -0,0 +1,152 @@
/* limbo_debugger_view.h */
#include "limbo_debugger_plugin.h"
#include "core/debugger/engine_debugger.h"
#include "core/math/math_defs.h"
#include "core/object/callable_method_pointer.h"
#include "core/os/memory.h"
#include "core/string/print_string.h"
#include "core/variant/array.h"
#include "editor/editor_scale.h"
#include "editor/plugins/editor_debugger_plugin.h"
#include "limbo_debugger.h"
#include "modules/limboai/debugger/behavior_tree_data.h"
#include "modules/limboai/debugger/behavior_tree_view.h"
#include "scene/gui/box_container.h"
#include "scene/gui/control.h"
#include "scene/gui/item_list.h"
#include "scene/gui/label.h"
#include "scene/gui/split_container.h"
#include "scene/gui/texture_rect.h"
/////////////////////// LimboDebuggerTab
void LimboDebuggerTab::start_session() {
bt_list->clear();
bt_view->clear();
info_box->hide();
hsc->show();
message->hide();
session->send_message("limboai:start_session", Array());
}
void LimboDebuggerTab::stop_session() {
hsc->hide();
message->show();
session->send_message("limboai:stop_session", Array());
}
void LimboDebuggerTab::update_bt_list(const Array &p_items) {
// Remember selected item.
String selected_bt = "";
if (bt_list->is_anything_selected()) {
selected_bt = bt_list->get_item_text(bt_list->get_selected_items().get(0));
}
bt_list->clear();
int select_idx = -1;
for (int i = 0; i < p_items.size(); i++) {
bt_list->add_item(p_items[i]);
if (p_items[i] == selected_bt) {
select_idx = i;
}
}
// Restore selected item.
if (select_idx > -1) {
bt_list->select(select_idx);
} else if (!selected_bt.is_empty()) {
_set_info_message(TTR("Node instance is gone"));
}
}
void LimboDebuggerTab::_set_info_message(const String &p_message) {
info_message->set_text(p_message);
info_icon->set_texture(get_theme_icon(SNAME("NodeInfo"), SNAME("EditorIcons")));
info_box->set_visible(!p_message.is_empty());
}
void LimboDebuggerTab::_bt_selected(int p_idx) {
info_box->hide();
bt_view->clear();
NodePath path = bt_list->get_item_text(p_idx);
Array data;
data.push_back(path);
session->send_message("limboai:track_bt_updates", data);
}
LimboDebuggerTab::LimboDebuggerTab(Ref<EditorDebuggerSession> p_session) {
session = p_session;
hsc = memnew(HSplitContainer);
add_child(hsc);
bt_list = memnew(ItemList);
hsc->add_child(bt_list);
bt_list->set_custom_minimum_size(Size2(280.0 * EDSCALE, 0.0));
bt_list->connect(SNAME("item_selected"), callable_mp(this, &LimboDebuggerTab::_bt_selected));
view_box = memnew(VBoxContainer);
{
hsc->add_child(view_box);
bt_view = memnew(BehaviorTreeView);
view_box->add_child(bt_view);
bt_view->set_h_size_flags(Control::SIZE_EXPAND_FILL);
bt_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
info_box = memnew(HBoxContainer);
view_box->add_child(info_box);
info_box->hide();
info_icon = memnew(TextureRect);
info_box->add_child(info_icon);
info_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
info_message = memnew(Label);
info_box->add_child(info_message);
info_message->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
}
message = memnew(Label);
add_child(message);
message->set_text(TTR("Run project to start debugging"));
message->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
message->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
message->set_anchors_preset(Control::PRESET_CENTER);
stop_session();
}
//////////////////////// LimboDebuggerPlugin
void LimboDebuggerPlugin::setup_session(int p_idx) {
Ref<EditorDebuggerSession> session = get_session(p_idx);
tab = memnew(LimboDebuggerTab(session));
tab->set_name("LimboAI");
session->connect(SNAME("started"), callable_mp(tab, &LimboDebuggerTab::start_session));
session->connect(SNAME("stopped"), callable_mp(tab, &LimboDebuggerTab::stop_session));
session->add_session_tab(tab);
}
bool LimboDebuggerPlugin::capture(const String &p_message, const Array &p_data, int p_session) {
bool captured = true;
if (p_message == "limboai:active_behavior_trees") {
tab->update_bt_list(p_data);
} else if (p_message == "limboai:bt_update") {
BehaviorTreeData data = BehaviorTreeData();
data.deserialize(p_data);
tab->get_behavior_tree_view()->update_tree(data);
} else {
captured = false;
}
return captured;
}
bool LimboDebuggerPlugin::has_capture(const String &p_capture) const {
return p_capture == "limboai";
}
LimboDebuggerPlugin::LimboDebuggerPlugin() {
tab = nullptr;
}

View File

@ -0,0 +1,60 @@
/* limbo_debugger_plugin.h */
#ifndef LIMBO_DEBUGGER_PLUGIN_H
#define LIMBO_DEBUGGER_PLUGIN_H
#include "core/object/class_db.h"
#include "core/object/object.h"
#include "core/typedefs.h"
#include "editor/plugins/editor_debugger_plugin.h"
#include "modules/limboai/debugger/behavior_tree_view.h"
#include "scene/gui/box_container.h"
#include "scene/gui/item_list.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/split_container.h"
#include "scene/gui/texture_rect.h"
class LimboDebuggerTab : public PanelContainer {
GDCLASS(LimboDebuggerTab, PanelContainer);
private:
Ref<EditorDebuggerSession> session;
HSplitContainer *hsc;
Label *message;
ItemList *bt_list;
BehaviorTreeView *bt_view;
VBoxContainer *view_box;
HBoxContainer *info_box;
TextureRect *info_icon;
Label *info_message;
_FORCE_INLINE_ void _set_info_message(const String &p_message);
void _bt_selected(int p_idx);
protected:
// void _notification(int p_notification);
public:
void start_session();
void stop_session();
void update_bt_list(const Array &p_items);
BehaviorTreeView *get_behavior_tree_view() { return bt_view; }
LimboDebuggerTab(Ref<EditorDebuggerSession> p_session);
};
class LimboDebuggerPlugin : public EditorDebuggerPlugin {
GDCLASS(LimboDebuggerPlugin, EditorDebuggerPlugin);
private:
LimboDebuggerTab *tab;
public:
void setup_session(int p_idx) override;
bool has_capture(const String &p_capture) const override;
bool capture(const String &p_message, const Array &p_data, int p_session) override;
LimboDebuggerPlugin();
};
#endif // LIMBO_DEBUGGER_PLUGIN

View File

@ -47,6 +47,8 @@
#include "modules/limboai/bt/composites/bt_parallel.h" #include "modules/limboai/bt/composites/bt_parallel.h"
#include "modules/limboai/bt/composites/bt_selector.h" #include "modules/limboai/bt/composites/bt_selector.h"
#include "modules/limboai/bt/composites/bt_sequence.h" #include "modules/limboai/bt/composites/bt_sequence.h"
#include "modules/limboai/debugger/limbo_debugger_plugin.h"
#include "modules/limboai/limbo_utility.h"
#include "scene/gui/box_container.h" #include "scene/gui/box_container.h"
#include "scene/gui/button.h" #include "scene/gui/button.h"
#include "scene/gui/control.h" #include "scene/gui/control.h"
@ -82,11 +84,13 @@ void TaskTree::_update_item(TreeItem *p_item) {
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());
String type_arg;
if (task->get_script_instance() && !task->get_script_instance()->get_script()->get_path().is_empty()) { if (task->get_script_instance() && !task->get_script_instance()->get_script()->get_path().is_empty()) {
p_item->set_icon(0, LimboAIEditor::get_task_icon(task->get_script_instance()->get_script()->get_path())); type_arg = task->get_script_instance()->get_script()->get_path();
} else { } else {
p_item->set_icon(0, LimboAIEditor::get_task_icon(task->get_class())); type_arg = task->get_class();
} }
p_item->set_icon(0, LimboUtility::get_singleton()->get_task_icon(type_arg));
p_item->set_editable(0, false); p_item->set_editable(0, false);
for (int i = 0; i < p_item->get_button_count(0); i++) { for (int i = 0; i < p_item->get_button_count(0); i++) {
@ -455,7 +459,7 @@ void TaskPanel::refresh() {
TaskSection *sec = memnew(TaskSection(cat)); TaskSection *sec = memnew(TaskSection(cat));
for (String task_meta : tasks) { for (String task_meta : tasks) {
Ref<Texture> icon = LimboAIEditor::get_task_icon(task_meta); Ref<Texture> icon = LimboUtility::get_singleton()->get_task_icon(task_meta);
String tname; String tname;
if (task_meta.begins_with("res:")) { if (task_meta.begins_with("res:")) {
tname = task_meta.get_file().get_basename().trim_prefix("BT").to_pascal_case(); tname = task_meta.get_file().get_basename().trim_prefix("BT").to_pascal_case();
@ -1019,35 +1023,6 @@ void LimboAIEditor::apply_changes() {
} }
} }
Ref<Texture> LimboAIEditor::get_task_icon(String p_script_path_or_class) {
ERR_FAIL_COND_V_MSG(p_script_path_or_class.is_empty(), Variant(), "BTTask: script path or class cannot be empty.");
String base_type = p_script_path_or_class;
if (p_script_path_or_class.begins_with("res:")) {
Ref<Script> script = ResourceLoader::load(p_script_path_or_class, "Script");
Ref<Script> base_script = script;
while (base_script.is_valid()) {
StringName name = EditorNode::get_editor_data().script_class_get_name(base_script->get_path());
String icon_path = EditorNode::get_editor_data().script_class_get_icon_path(name);
if (!icon_path.is_empty()) {
Ref<Image> img = memnew(Image);
Error err = ImageLoader::load_image(icon_path, img);
if (err == OK) {
Ref<ImageTexture> icon = memnew(ImageTexture);
img->resize(16 * EDSCALE, 16 * EDSCALE, Image::INTERPOLATE_LANCZOS);
icon->create_from_image(img);
return icon;
}
}
base_script = base_script->get_base_script();
}
base_type = script->get_instance_base_type();
}
// TODO: Walk inheritance tree until icon is found.
return EditorNode::get_singleton()->get_class_icon(base_type, "BTTask");
}
void LimboAIEditor::_notification(int p_what) { void LimboAIEditor::_notification(int p_what) {
switch (p_what) { switch (p_what) {
case NOTIFICATION_ENTER_TREE: { case NOTIFICATION_ENTER_TREE: {
@ -1326,6 +1301,7 @@ LimboAIEditorPlugin::LimboAIEditorPlugin() {
limbo_ai_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL); limbo_ai_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL);
EditorNode::get_singleton()->get_main_screen_control()->add_child(limbo_ai_editor); EditorNode::get_singleton()->get_main_screen_control()->add_child(limbo_ai_editor);
limbo_ai_editor->hide(); limbo_ai_editor->hide();
add_debugger_plugin(memnew(LimboDebuggerPlugin));
} }
LimboAIEditorPlugin::~LimboAIEditorPlugin() { LimboAIEditorPlugin::~LimboAIEditorPlugin() {

View File

@ -182,8 +182,6 @@ protected:
void _notification(int p_what); void _notification(int p_what);
public: public:
static Ref<Texture> get_task_icon(String p_script_path_or_class);
void edit_bt(Ref<BehaviorTree> p_behavior_tree, bool p_force_refresh = false); void edit_bt(Ref<BehaviorTree> p_behavior_tree, bool p_force_refresh = false);
void apply_changes(); void apply_changes();

View File

@ -2,7 +2,11 @@
#include "limbo_utility.h" #include "limbo_utility.h"
#include "bt/bt_task.h" #include "bt/bt_task.h"
#include "core/io/image_loader.h"
#include "core/variant/variant.h" #include "core/variant/variant.h"
#include "editor/editor_node.h"
#include "editor/editor_scale.h"
#include "scene/resources/texture.h"
LimboUtility *LimboUtility::singleton = nullptr; LimboUtility *LimboUtility::singleton = nullptr;
@ -34,9 +38,39 @@ String LimboUtility::get_status_name(int p_status) const {
} }
} }
Ref<Texture2D> LimboUtility::get_task_icon(String p_class_or_script_path) const {
ERR_FAIL_COND_V_MSG(p_class_or_script_path.is_empty(), Variant(), "BTTask: script path or class cannot be empty.");
String base_type = p_class_or_script_path;
if (p_class_or_script_path.begins_with("res:")) {
Ref<Script> script = ResourceLoader::load(p_class_or_script_path, "Script");
Ref<Script> base_script = script;
while (base_script.is_valid()) {
StringName name = EditorNode::get_editor_data().script_class_get_name(base_script->get_path());
String icon_path = EditorNode::get_editor_data().script_class_get_icon_path(name);
if (!icon_path.is_empty()) {
Ref<Image> img = memnew(Image);
Error err = ImageLoader::load_image(icon_path, img);
if (err == OK) {
Ref<ImageTexture> icon = memnew(ImageTexture);
img->resize(16 * EDSCALE, 16 * EDSCALE, Image::INTERPOLATE_LANCZOS);
icon->create_from_image(img);
return icon;
}
}
base_script = base_script->get_base_script();
}
base_type = script->get_instance_base_type();
}
// TODO: Walk inheritance tree until icon is found.
return EditorNode::get_singleton()->get_class_icon(base_type, "BTTask");
}
void LimboUtility::_bind_methods() { void LimboUtility::_bind_methods() {
ClassDB::bind_method(D_METHOD("decorate_var", "p_variable"), &LimboUtility::decorate_var); ClassDB::bind_method(D_METHOD("decorate_var", "p_variable"), &LimboUtility::decorate_var);
ClassDB::bind_method(D_METHOD("get_status_name", "p_status"), &LimboUtility::get_status_name); ClassDB::bind_method(D_METHOD("get_status_name", "p_status"), &LimboUtility::get_status_name);
ClassDB::bind_method(D_METHOD("get_task_icon"), &LimboUtility::get_task_icon);
} }
LimboUtility::LimboUtility() { LimboUtility::LimboUtility() {

View File

@ -5,6 +5,7 @@
#include "core/object/class_db.h" #include "core/object/class_db.h"
#include "core/object/object.h" #include "core/object/object.h"
#include "scene/resources/texture.h"
class LimboUtility : public Object { class LimboUtility : public Object {
GDCLASS(LimboUtility, Object); GDCLASS(LimboUtility, Object);
@ -18,6 +19,7 @@ public:
String decorate_var(String p_variable) const; String decorate_var(String p_variable) const;
String get_status_name(int p_status) const; String get_status_name(int p_status) const;
Ref<Texture2D> get_task_icon(String p_class_or_script_path) const;
LimboUtility(); LimboUtility();
~LimboUtility(); ~LimboUtility();

View File

@ -77,6 +77,7 @@
#include "limbo_state.h" #include "limbo_state.h"
#include "limbo_string_names.h" #include "limbo_string_names.h"
#include "limbo_utility.h" #include "limbo_utility.h"
#include "modules/limboai/debugger/limbo_debugger.h"
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
#include "editor/limbo_ai_editor_plugin.h" #include "editor/limbo_ai_editor_plugin.h"
@ -86,6 +87,8 @@ static LimboUtility *_limbo_utility = nullptr;
void initialize_limboai_module(ModuleInitializationLevel p_level) { void initialize_limboai_module(ModuleInitializationLevel p_level) {
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
LimboDebugger::initialize();
GDREGISTER_CLASS(Blackboard); GDREGISTER_CLASS(Blackboard);
GDREGISTER_CLASS(LimboState); GDREGISTER_CLASS(LimboState);
@ -180,6 +183,7 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) {
void uninitialize_limboai_module(ModuleInitializationLevel p_level) { void uninitialize_limboai_module(ModuleInitializationLevel p_level) {
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
LimboDebugger::deinitialize();
LimboStringNames::free(); LimboStringNames::free();
memdelete(_limbo_utility); memdelete(_limbo_utility);
} }