Implement BTInstance - runtime instance of BehaviorTree

This commit is contained in:
Serhii Snitsaruk 2024-08-03 11:07:06 +02:00
parent a2a62f636b
commit fc26f51ff2
No known key found for this signature in database
GPG Key ID: A965EF8799FFEC2D
17 changed files with 367 additions and 238 deletions

View File

@ -25,7 +25,7 @@ void BBVariable::set_value(const Variant &p_value) {
data->value_changed = true;
if (is_bound()) {
Object *obj = ObjectDB::get_instance(ObjectID(data->bound_object));
Object *obj = OBJECT_DB_GET_INSTANCE(data->bound_object);
ERR_FAIL_COND_MSG(!obj, "Blackboard: Failed to get bound object.");
#ifdef LIMBOAI_MODULE
bool r_valid;
@ -39,7 +39,7 @@ void BBVariable::set_value(const Variant &p_value) {
Variant BBVariable::get_value() const {
if (is_bound()) {
Object *obj = ObjectDB::get_instance(ObjectID(data->bound_object));
Object *obj = OBJECT_DB_GET_INSTANCE(data->bound_object);
ERR_FAIL_COND_V_MSG(!obj, data->value, "Blackboard: Failed to get bound object.");
#ifdef LIMBOAI_MODULE
bool r_valid;

View File

@ -77,13 +77,15 @@ void BehaviorTree::copy_other(const Ref<BehaviorTree> &p_other) {
root_task = p_other->get_root_task();
}
Ref<BTTask> BehaviorTree::instantiate(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) const {
ERR_FAIL_COND_V_MSG(root_task == nullptr, memnew(BTTask), "Trying to instance a behavior tree with no valid root task.");
ERR_FAIL_NULL_V_MSG(p_agent, memnew(BTTask), "Trying to instance a behavior tree with no valid agent.");
ERR_FAIL_NULL_V_MSG(p_scene_root, memnew(BTTask), "Trying to instance a behavior tree with no valid scene root.");
Ref<BTTask> inst = root_task->clone();
inst->initialize(p_agent, p_blackboard, p_scene_root);
return inst;
Ref<BTInstance> BehaviorTree::instantiate(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_instance_owner) const {
ERR_FAIL_COND_V_MSG(root_task == nullptr, nullptr, "BehaviorTree: Instantiation failed - BT has no valid root task.");
ERR_FAIL_NULL_V_MSG(p_agent, nullptr, "BehaviorTree: Instantiation failed - agent can't be null.");
ERR_FAIL_NULL_V_MSG(p_instance_owner, nullptr, "BehaviorTree: Instantiation failed -- instance owner can't be null.");
Node *scene_root = p_instance_owner->get_owner();
ERR_FAIL_NULL_V_MSG(scene_root, nullptr, "BehaviorTree: Instantiation failed - can't get scene root, because instance_owner not owned by a scene node. Hint: Try my_player.set_owner(get_owner()).");
Ref<BTTask> root_copy = root_task->clone();
root_copy->initialize(p_agent, p_blackboard, scene_root);
return BTInstance::create(root_copy, get_path(), p_instance_owner);
}
void BehaviorTree::_plan_changed() {

View File

@ -13,6 +13,7 @@
#define BEHAVIOR_TREE_H
#include "../blackboard/blackboard_plan.h"
#include "bt_instance.h"
#include "tasks/bt_task.h"
#ifdef LIMBOAI_MODULE
@ -58,7 +59,7 @@ public:
Ref<BehaviorTree> clone() const;
void copy_other(const Ref<BehaviorTree> &p_other);
Ref<BTTask> instantiate(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) const;
Ref<BTInstance> instantiate(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_instance_owner) const;
BehaviorTree();
~BehaviorTree();

138
bt/bt_instance.cpp Normal file
View File

@ -0,0 +1,138 @@
/**
* bt_instance.cpp
* =============================================================================
* Copyright 2021-2024 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_instance.h"
#include "../editor/debugger/limbo_debugger.h"
#include "behavior_tree.h"
#ifdef LIMBOAI_MODULE
#include "core/os/time.h"
#include "main/performance.h"
#endif
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/performance.hpp>
#include <godot_cpp/classes/time.hpp>
#endif
Ref<BTInstance> BTInstance::create(Ref<BTTask> p_root_task, String p_source_bt_path, Node *p_owner_node) {
ERR_FAIL_NULL_V(p_root_task, nullptr);
ERR_FAIL_NULL_V(p_owner_node, nullptr);
Ref<BTInstance> inst;
inst.instantiate();
inst->root_task = p_root_task;
inst->owner_node_id = p_owner_node->get_instance_id();
inst->source_bt_path = p_source_bt_path;
return inst;
}
void BTInstance::update(double p_delta) {
ERR_FAIL_COND(!root_task.is_valid());
#ifdef DEBUG_ENABLED
double start = Time::get_singleton()->get_ticks_usec();
#endif
last_status = root_task->execute(p_delta);
emit_signal(LimboStringNames::get_singleton()->updated, last_status);
#ifdef DEBUG_ENABLED
double end = Time::get_singleton()->get_ticks_usec();
update_time_acc += (end - start);
update_time_n += 1.0;
#endif
}
void BTInstance::set_monitor_performance(bool p_monitor) {
#ifdef DEBUG_ENABLED
monitor_performance = p_monitor;
if (monitor_performance) {
_add_custom_monitor();
} else {
_remove_custom_monitor();
}
#endif
}
bool BTInstance::get_monitor_performance() const {
#ifdef DEBUG_ENABLED
return monitor_performance;
#else
return false;
#endif
}
void BTInstance::register_with_debugger() {
#ifdef DEBUG_ENABLED
LimboDebugger::get_singleton()->register_bt_instance(get_instance_id());
#endif
}
#ifdef DEBUG_ENABLED
double BTInstance::_get_mean_update_time_msec_and_reset() {
if (update_time_n) {
double mean_time_msec = (update_time_acc * 0.001) / update_time_n;
update_time_acc = 0.0;
update_time_n = 0.0;
return mean_time_msec;
}
return 0.0;
}
void BTInstance::_add_custom_monitor() {
ERR_FAIL_NULL(get_owner_node());
ERR_FAIL_NULL(root_task);
ERR_FAIL_NULL(root_task->get_agent());
if (monitor_id == StringName()) {
monitor_id = vformat("LimboAI/update_ms|%s_%s_%s", root_task->get_agent()->get_name(), get_owner_node()->get_name(),
String(itos(get_instance_id())).md5_text().substr(0, 4));
}
if (!Performance::get_singleton()->has_custom_monitor(monitor_id)) {
PERFORMANCE_ADD_CUSTOM_MONITOR(monitor_id, callable_mp(this, &BTInstance::_get_mean_update_time_msec_and_reset));
}
}
void BTInstance::_remove_custom_monitor() {
if (monitor_id != StringName() && Performance::get_singleton()->has_custom_monitor(monitor_id)) {
Performance::get_singleton()->remove_custom_monitor(monitor_id);
}
}
#endif // * DEBUG_ENABLED
void BTInstance::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_root_task"), &BTInstance::get_root_task);
ClassDB::bind_method(D_METHOD("get_owner_node"), &BTInstance::get_owner_node);
ClassDB::bind_method(D_METHOD("get_last_status"), &BTInstance::get_last_status);
ClassDB::bind_method(D_METHOD("get_source_bt_path"), &BTInstance::get_source_bt_path);
ClassDB::bind_method(D_METHOD("set_monitor_performance", "monitor"), &BTInstance::set_monitor_performance);
ClassDB::bind_method(D_METHOD("get_monitor_performance"), &BTInstance::get_monitor_performance);
ClassDB::bind_method(D_METHOD("update", "delta"), &BTInstance::update);
ClassDB::bind_method(D_METHOD("register_with_debugger"), &BTInstance::register_with_debugger);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "monitor_performance"), "set_monitor_performance", "get_monitor_performance");
ADD_SIGNAL(MethodInfo("updated", PropertyInfo(Variant::INT, "status")));
ADD_SIGNAL(MethodInfo("freed"));
}
BTInstance::~BTInstance() {
emit_signal(LW_NAME(freed));
#ifdef DEBUG_ENABLED
_remove_custom_monitor();
#endif
}

60
bt/bt_instance.h Normal file
View File

@ -0,0 +1,60 @@
/**
* bt_instance.h
* =============================================================================
* Copyright 2021-2024 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_INSTANCE_H
#define BT_INSTANCE_H
#include "tasks/bt_task.h"
class BTInstance : public RefCounted {
GDCLASS(BTInstance, RefCounted);
private:
Ref<BTTask> root_task;
uint64_t owner_node_id;
String source_bt_path;
BTTask::Status last_status = BTTask::FRESH;
#ifdef DEBUG_ENABLED
bool monitor_performance = false;
StringName monitor_id;
double update_time_acc = 0.0;
double update_time_n = 0.0;
double _get_mean_update_time_msec_and_reset();
void _add_custom_monitor();
void _remove_custom_monitor();
#endif // * DEBUG_ENABLED
protected:
static void _bind_methods();
public:
_FORCE_INLINE_ Ref<BTTask> get_root_task() const { return root_task; }
_FORCE_INLINE_ Node *get_owner_node() const { return Object::cast_to<Node>(OBJECT_DB_GET_INSTANCE(owner_node_id)); }
_FORCE_INLINE_ BTTask::Status get_last_status() const { return last_status; }
_FORCE_INLINE_ bool is_instance_valid() const { return root_task.is_valid(); }
_FORCE_INLINE_ String get_source_bt_path() const { return source_bt_path; }
void update(double p_delta);
void set_monitor_performance(bool p_monitor);
bool get_monitor_performance() const;
void register_with_debugger();
static Ref<BTInstance> create(Ref<BTTask> p_root_task, String p_source_bt_path, Node *p_owner_node);
BTInstance() = default;
~BTInstance();
};
#endif // BT_INSTANCE_H

View File

@ -11,7 +11,6 @@
#include "bt_player.h"
#include "../editor/debugger/limbo_debugger.h"
#include "../util/limbo_compat.h"
#include "../util/limbo_string_names.h"
@ -44,24 +43,18 @@
VARIANT_ENUM_CAST(BTPlayer::UpdateMode);
void BTPlayer::_load_tree() {
#ifdef DEBUG_ENABLED
if (tree_instance.is_valid() && IS_DEBUGGER_ACTIVE()) {
LimboDebugger::get_singleton()->unregister_bt_instance(tree_instance, get_path());
}
#endif
tree_instance.unref();
bt_instance.unref();
ERR_FAIL_COND_MSG(!behavior_tree.is_valid(), "BTPlayer: Initialization failed - needs a valid behavior tree.");
ERR_FAIL_COND_MSG(!behavior_tree->get_root_task().is_valid(), "BTPlayer: Initialization failed - behavior tree has no valid root task.");
Node *agent = GET_NODE(this, agent_node);
ERR_FAIL_NULL_MSG(agent, vformat("BTPlayer: Initialization failed - can't get agent with path '%s'.", agent_node));
Node *scene_root = get_owner();
ERR_FAIL_NULL_MSG(scene_root, "BTPlayer: Initialization failed - can't get scene root (make sure the BTPlayer's owner property is set).");
tree_instance = behavior_tree->instantiate(agent, blackboard, scene_root);
bt_instance = behavior_tree->instantiate(agent, blackboard, this);
#ifdef DEBUG_ENABLED
if (IS_DEBUGGER_ACTIVE()) {
LimboDebugger::get_singleton()->register_bt_instance(tree_instance, get_path());
}
#endif
bt_instance->set_monitor_performance(monitor_performance);
bt_instance->register_with_debugger();
#endif // DEBUG_ENABLED
}
void BTPlayer::_update_blackboard_plan() {
@ -95,7 +88,7 @@ void BTPlayer::set_behavior_tree(const Ref<BehaviorTree> &p_tree) {
void BTPlayer::set_agent_node(const NodePath &p_agent_node) {
agent_node = p_agent_node;
if (tree_instance.is_valid()) {
if (bt_instance.is_valid()) {
ERR_PRINT("BTPlayer: Agent node cannot be set after the behavior tree is instantiated. This change will not affect the behavior tree instance.");
}
}
@ -119,33 +112,19 @@ void BTPlayer::set_active(bool p_active) {
}
void BTPlayer::update(double p_delta) {
if (!tree_instance.is_valid()) {
if (!bt_instance.is_valid()) {
ERR_PRINT_ONCE(vformat("BTPlayer doesn't have a behavior tree with a valid root task to execute (owner: %s)", get_owner()));
return;
}
#ifdef DEBUG_ENABLED
double start = GET_TICKS_USEC();
#endif
if (active) {
last_status = tree_instance->execute(p_delta);
emit_signal(LimboStringNames::get_singleton()->updated, last_status);
if (last_status == BTTask::SUCCESS || last_status == BTTask::FAILURE) {
emit_signal(LimboStringNames::get_singleton()->behavior_tree_finished, last_status);
}
bt_instance->update(p_delta);
}
#ifdef DEBUG_ENABLED
double end = GET_TICKS_USEC();
update_time_acc += (end - start);
update_time_n += 1.0;
#endif
}
void BTPlayer::restart() {
ERR_FAIL_COND_MSG(tree_instance.is_null(), "BTPlayer: Restart failed - no valid tree instance. Make sure the BTPlayer has a valid behavior tree with a valid root task.");
tree_instance->abort();
ERR_FAIL_COND_MSG(bt_instance.is_null(), "BTPlayer: Restart failed - no valid tree instance. Make sure the BTPlayer has a valid behavior tree with a valid root task.");
bt_instance->get_root_task()->abort();
set_active(true);
}
@ -154,42 +133,9 @@ void BTPlayer::restart() {
void BTPlayer::_set_monitor_performance(bool p_monitor_performance) {
monitor_performance = p_monitor_performance;
if (!get_owner() && monitor_performance) {
// Don't add custom monitor if not in scene.
return;
if (bt_instance.is_valid()) {
bt_instance->set_monitor_performance(monitor_performance);
}
if (monitor_performance) {
_add_custom_monitor();
} else {
_remove_custom_monitor();
}
}
void BTPlayer::_add_custom_monitor() {
if (monitor_id == StringName()) {
monitor_id = vformat("LimboAI/update_ms|%s_%s_%s", get_owner()->get_name(), get_name(),
String(itos(get_instance_id())).md5_text().substr(0, 4));
}
if (!Performance::get_singleton()->has_custom_monitor(monitor_id)) {
PERFORMANCE_ADD_CUSTOM_MONITOR(monitor_id, callable_mp(this, &BTPlayer::_get_mean_update_time_msec));
}
}
void BTPlayer::_remove_custom_monitor() {
if (monitor_id != StringName() && Performance::get_singleton()->has_custom_monitor(monitor_id)) {
Performance::get_singleton()->remove_custom_monitor(monitor_id);
}
}
double BTPlayer::_get_mean_update_time_msec() {
if (update_time_n) {
double mean_time_msec = (update_time_acc * 0.001) / update_time_n;
update_time_acc = 0.0;
update_time_n = 0.0;
return mean_time_msec;
}
return 0.0;
}
#endif // ! DEBUG_ENABLED
@ -222,21 +168,15 @@ void BTPlayer::_notification(int p_notification) {
} break;
case NOTIFICATION_ENTER_TREE: {
#ifdef DEBUG_ENABLED
if (tree_instance.is_valid() && IS_DEBUGGER_ACTIVE()) {
LimboDebugger::get_singleton()->register_bt_instance(tree_instance, get_path());
}
if (monitor_performance) {
_add_custom_monitor();
if (bt_instance.is_valid()) {
bt_instance->set_monitor_performance(monitor_performance);
}
#endif // DEBUG_ENABLED
} break;
case NOTIFICATION_EXIT_TREE: {
#ifdef DEBUG_ENABLED
if (tree_instance.is_valid() && IS_DEBUGGER_ACTIVE()) {
LimboDebugger::get_singleton()->unregister_bt_instance(tree_instance, get_path());
}
if (monitor_performance) {
_remove_custom_monitor();
if (bt_instance.is_valid()) {
bt_instance->set_monitor_performance(false);
}
#endif // DEBUG_ENABLED
@ -266,9 +206,8 @@ void BTPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("update", "delta"), &BTPlayer::update);
ClassDB::bind_method(D_METHOD("restart"), &BTPlayer::restart);
ClassDB::bind_method(D_METHOD("get_last_status"), &BTPlayer::get_last_status);
ClassDB::bind_method(D_METHOD("get_tree_instance"), &BTPlayer::get_tree_instance);
ClassDB::bind_method(D_METHOD("get_bt_instance"), &BTPlayer::get_bt_instance);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "behavior_tree", PROPERTY_HINT_RESOURCE_TYPE, "BehaviorTree"), "set_behavior_tree", "get_behavior_tree");
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "agent_node"), "set_agent_node", "get_agent_node");

View File

@ -15,6 +15,7 @@
#include "../blackboard/blackboard.h"
#include "../blackboard/blackboard_plan.h"
#include "behavior_tree.h"
#include "bt_instance.h"
#include "tasks/bt_task.h"
#ifdef LIMBOAI_MODULE
@ -42,9 +43,8 @@ private:
UpdateMode update_mode = UpdateMode::PHYSICS;
bool active = true;
Ref<Blackboard> blackboard;
int last_status = -1;
Ref<BTTask> tree_instance;
Ref<BTInstance> bt_instance;
void _load_tree();
void _update_blackboard_plan();
@ -75,9 +75,8 @@ public:
void update(double p_delta);
void restart();
int get_last_status() const { return last_status; }
Ref<BTTask> get_tree_instance() { return tree_instance; }
Ref<BTInstance> get_bt_instance() { return bt_instance; }
BTPlayer();
~BTPlayer();
@ -86,19 +85,11 @@ public:
private:
bool monitor_performance = false;
StringName monitor_id;
double update_time_acc = 0.0;
double update_time_n = 0.0;
void _set_monitor_performance(bool p_monitor_performance);
bool _get_monitor_performance() const { return monitor_performance; }
void _add_custom_monitor();
void _remove_custom_monitor();
double _get_mean_update_time_msec();
#endif // DEBUG_ENABLED
#endif // * DEBUG_ENABLED
};
#endif // BT_PLAYER_H

View File

@ -17,14 +17,15 @@
//**** BehaviorTreeData
Array BehaviorTreeData::serialize(const Ref<BTTask> &p_tree_instance, const NodePath &p_player_path, const String &p_bt_resource_path) {
Array BehaviorTreeData::serialize(const Ref<BTInstance> &p_instance) {
Array arr;
arr.push_back(p_player_path);
arr.push_back(p_bt_resource_path);
arr.push_back(uint64_t(p_instance->get_instance_id()));
arr.push_back(p_instance->get_owner_node() ? p_instance->get_owner_node()->get_path() : NodePath());
arr.push_back(p_instance->get_source_bt_path());
// Flatten tree into list depth first
List<Ref<BTTask>> stack;
stack.push_back(p_tree_instance);
stack.push_back(p_instance->get_root_task());
while (stack.size()) {
Ref<BTTask> task = stack.front()->get();
stack.pop_front();
@ -54,15 +55,17 @@ Array BehaviorTreeData::serialize(const Ref<BTTask> &p_tree_instance, const Node
}
Ref<BehaviorTreeData> BehaviorTreeData::deserialize(const Array &p_array) {
ERR_FAIL_COND_V(p_array.size() < 2, nullptr);
ERR_FAIL_COND_V(p_array[0].get_type() != Variant::NODE_PATH, nullptr);
ERR_FAIL_COND_V(p_array[1].get_type() != Variant::STRING, nullptr);
ERR_FAIL_COND_V(p_array.size() < 3, nullptr);
ERR_FAIL_COND_V(p_array[0].get_type() != Variant::INT, nullptr);
ERR_FAIL_COND_V(p_array[1].get_type() != Variant::NODE_PATH, nullptr);
ERR_FAIL_COND_V(p_array[2].get_type() != Variant::STRING, nullptr);
Ref<BehaviorTreeData> data = memnew(BehaviorTreeData);
data->bt_player_path = p_array[0];
data->bt_resource_path = p_array[1];
data->bt_instance_id = uint64_t(p_array[0]);
data->node_owner_path = p_array[1];
data->source_bt_path = p_array[2];
int idx = 2;
int idx = 3;
while (p_array.size() > idx + 1) {
ERR_FAIL_COND_V(p_array.size() < idx + 7, nullptr);
ERR_FAIL_COND_V(p_array[idx].get_type() != Variant::INT, nullptr);
@ -80,12 +83,16 @@ Ref<BehaviorTreeData> BehaviorTreeData::deserialize(const Array &p_array) {
return data;
}
Ref<BehaviorTreeData> BehaviorTreeData::create_from_tree_instance(const Ref<BTTask> &p_tree_instance) {
Ref<BehaviorTreeData> BehaviorTreeData::create_from_bt_instance(const Ref<BTInstance> &p_bt_instance) {
Ref<BehaviorTreeData> data = memnew(BehaviorTreeData);
data->bt_instance_id = p_bt_instance->get_instance_id();
data->node_owner_path = p_bt_instance->get_owner_node() ? p_bt_instance->get_owner_node()->get_path() : NodePath();
data->source_bt_path = p_bt_instance->get_source_bt_path();
// Flatten tree into list depth first
List<Ref<BTTask>> stack;
stack.push_back(p_tree_instance);
stack.push_back(p_bt_instance->get_root_task());
while (stack.size()) {
Ref<BTTask> task = stack.front()->get();
stack.pop_front();
@ -115,9 +122,7 @@ Ref<BehaviorTreeData> BehaviorTreeData::create_from_tree_instance(const Ref<BTTa
}
void BehaviorTreeData::_bind_methods() {
// ClassDB::bind_static_method("BehaviorTreeData", D_METHOD("serialize", "p_tree_instance", "p_player_path", "p_bt_resource_path"), &BehaviorTreeData::serialize);
// ClassDB::bind_static_method("BehaviorTreeData", D_METHOD("deserialize", "p_array"), &BehaviorTreeData::deserialize);
ClassDB::bind_static_method("BehaviorTreeData", D_METHOD("create_from_tree_instance", "tree_instance"), &BehaviorTreeData::create_from_tree_instance);
ClassDB::bind_static_method("BehaviorTreeData", D_METHOD("create_from_bt_instance", "bt_instance"), &BehaviorTreeData::create_from_bt_instance);
}
BehaviorTreeData::BehaviorTreeData() {

View File

@ -12,6 +12,7 @@
#ifndef BEHAVIOR_TREE_DATA_H
#define BEHAVIOR_TREE_DATA_H
#include "../../bt/bt_instance.h"
#include "../../bt/tasks/bt_task.h"
class BehaviorTreeData : public RefCounted {
@ -46,13 +47,14 @@ public:
};
List<TaskData> tasks;
NodePath bt_player_path;
String bt_resource_path;
uint64_t bt_instance_id;
NodePath node_owner_path;
String source_bt_path;
public:
static Array serialize(const Ref<BTTask> &p_tree_instance, const NodePath &p_player_path, const String &p_bt_resource_path);
static Array serialize(const Ref<BTInstance> &p_instance);
static Ref<BehaviorTreeData> deserialize(const Array &p_array);
static Ref<BehaviorTreeData> create_from_tree_instance(const Ref<BTTask> &p_tree_instance);
static Ref<BehaviorTreeData> create_from_bt_instance(const Ref<BTInstance> &p_bt_instance);
BehaviorTreeData();
};

View File

@ -11,13 +11,13 @@
#include "limbo_debugger.h"
#include "../../bt/bt_instance.h"
#include "../../bt/tasks/bt_task.h"
#include "../../util/limbo_compat.h"
#include "behavior_tree_data.h"
#ifdef LIMBOAI_MODULE
#include "core/debugger/engine_debugger.h"
#include "core/error/error_macros.h"
#include "core/io/resource.h"
#include "core/string/node_path.h"
#include "scene/main/scene_tree.h"
@ -63,14 +63,6 @@ void LimboDebugger::deinitialize() {
}
void LimboDebugger::_bind_methods() {
#ifdef DEBUG_ENABLED
#ifdef LIMBOAI_GDEXTENSION
ClassDB::bind_method(D_METHOD("parse_message_gdext"), &LimboDebugger::parse_message_gdext);
#endif
ClassDB::bind_method(D_METHOD("_on_bt_updated", "status", "path"), &LimboDebugger::_on_bt_updated);
ClassDB::bind_method(D_METHOD("_on_state_updated", "delta", "path"), &LimboDebugger::_on_state_updated);
#endif // ! DEBUG_ENABLED
}
#ifdef DEBUG_ENABLED
@ -99,100 +91,87 @@ bool LimboDebugger::parse_message_gdext(const String &p_msg, const Array &p_args
}
#endif // LIMBOAI_GDEXTENSION
void LimboDebugger::register_bt_instance(Ref<BTTask> p_instance, NodePath p_player_path) {
ERR_FAIL_COND(p_instance.is_null());
ERR_FAIL_COND(p_player_path.is_empty());
if (active_trees.has(p_player_path)) {
void LimboDebugger::register_bt_instance(uint64_t p_instance_id) {
ERR_FAIL_COND(p_instance_id == 0);
BTInstance *inst = Object::cast_to<BTInstance>(OBJECT_DB_GET_INSTANCE(p_instance_id));
ERR_FAIL_NULL(inst);
ERR_FAIL_COND(!inst->is_instance_valid());
if (active_bt_instances.has(p_instance_id)) {
return;
}
active_trees.insert(p_player_path, p_instance);
if (!inst->is_connected(LW_NAME(freed), callable_mp(this, &LimboDebugger::unregister_bt_instance).bind(p_instance_id))) {
inst->connect(LW_NAME(freed), callable_mp(this, &LimboDebugger::unregister_bt_instance).bind(p_instance_id));
}
active_bt_instances.insert(p_instance_id);
if (session_active) {
_send_active_bt_players();
}
}
void LimboDebugger::unregister_bt_instance(Ref<BTTask> p_instance, NodePath p_player_path) {
ERR_FAIL_COND(p_instance.is_null());
ERR_FAIL_COND(p_player_path.is_empty());
ERR_FAIL_COND(!active_trees.has(p_player_path));
void LimboDebugger::unregister_bt_instance(uint64_t p_instance_id) {
if (!active_bt_instances.has(p_instance_id)) {
return;
}
if (tracked_player == p_player_path) {
if (tracked_instance_id == p_instance_id) {
_untrack_tree();
}
active_trees.erase(p_player_path);
active_bt_instances.erase(p_instance_id);
if (session_active) {
_send_active_bt_players();
}
}
void LimboDebugger::_track_tree(NodePath p_path) {
ERR_FAIL_COND(!active_trees.has(p_path));
void LimboDebugger::_track_tree(uint64_t p_instance_id) {
ERR_FAIL_COND(!active_bt_instances.has(p_instance_id));
if (!tracked_player.is_empty()) {
_untrack_tree();
}
_untrack_tree();
Node *node = SCENE_TREE()->get_root()->get_node_or_null(p_path);
ERR_FAIL_COND(node == nullptr);
tracked_instance_id = p_instance_id;
tracked_player = p_path;
Ref<Resource> bt = node->get(LW_NAME(behavior_tree));
if (bt.is_valid()) {
bt_resource_path = bt->get_path();
} else {
bt_resource_path = "";
}
if (node->is_class("BTPlayer")) {
node->connect(LW_NAME(updated), callable_mp(this, &LimboDebugger::_on_bt_updated).bind(p_path));
} else if (node->is_class("BTState")) {
node->connect(LW_NAME(updated), callable_mp(this, &LimboDebugger::_on_state_updated).bind(p_path));
}
BTInstance *inst = Object::cast_to<BTInstance>(OBJECT_DB_GET_INSTANCE(p_instance_id));
ERR_FAIL_NULL(inst);
inst->connect(LW_NAME(updated), callable_mp(this, &LimboDebugger::_on_bt_instance_updated).bind(p_instance_id));
}
void LimboDebugger::_untrack_tree() {
if (tracked_player.is_empty()) {
if (tracked_instance_id == 0) {
return;
}
NodePath was_tracking = tracked_player;
tracked_player = NodePath();
Node *node = SCENE_TREE()->get_root()->get_node_or_null(was_tracking);
ERR_FAIL_COND(node == nullptr);
if (node->is_class("BTPlayer")) {
node->disconnect(LW_NAME(updated), callable_mp(this, &LimboDebugger::_on_bt_updated));
} else if (node->is_class("BTState")) {
node->disconnect(LW_NAME(updated), callable_mp(this, &LimboDebugger::_on_state_updated));
BTInstance *inst = Object::cast_to<BTInstance>(OBJECT_DB_GET_INSTANCE(tracked_instance_id));
if (inst) {
inst->disconnect(LW_NAME(updated), callable_mp(this, &LimboDebugger::_on_bt_instance_updated));
}
tracked_instance_id = 0;
}
void LimboDebugger::_send_active_bt_players() {
Array arr;
for (KeyValue<NodePath, Ref<BTTask>> kv : active_trees) {
arr.append(kv.key);
for (uint64_t instance_id : active_bt_instances) {
arr.append(instance_id);
BTInstance *inst = Object::cast_to<BTInstance>(OBJECT_DB_GET_INSTANCE(instance_id));
if (inst == nullptr) {
ERR_PRINT("LimboDebugger::_send_active_bt_players: Registered BTInstance not found (no longer exists?).");
continue;
}
Node *owner_node = inst->get_owner_node();
arr.append(owner_node ? owner_node->get_path() : NodePath());
}
EngineDebugger::get_singleton()->send_message("limboai:active_bt_players", arr);
}
void LimboDebugger::_on_bt_updated(int _status, NodePath p_path) {
if (p_path != tracked_player) {
void LimboDebugger::_on_bt_instance_updated(int _status, uint64_t p_instance_id) {
if (p_instance_id != tracked_instance_id) {
return;
}
Array arr = BehaviorTreeData::serialize(active_trees.get(tracked_player), tracked_player, bt_resource_path);
EngineDebugger::get_singleton()->send_message("limboai:bt_update", arr);
}
void LimboDebugger::_on_state_updated(float _delta, NodePath p_path) {
if (p_path != tracked_player) {
return;
}
Array arr = BehaviorTreeData::serialize(active_trees.get(tracked_player), tracked_player, bt_resource_path);
BTInstance *inst = Object::cast_to<BTInstance>(OBJECT_DB_GET_INSTANCE(p_instance_id));
ERR_FAIL_NULL(inst);
Array arr = BehaviorTreeData::serialize(inst);
EngineDebugger::get_singleton()->send_message("limboai:bt_update", arr);
}

View File

@ -12,6 +12,7 @@
#ifndef LIMBO_DEBUGGER_H
#define LIMBO_DEBUGGER_H
#include "../../bt/bt_instance.h"
#include "../../bt/tasks/bt_task.h"
#ifdef LIMBOAI_MODULE
@ -23,6 +24,7 @@
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/object.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/templates/hash_set.hpp>
#include <godot_cpp/variant/node_path.hpp>
#endif // LIMBOAI_GDEXTENSION
@ -46,17 +48,15 @@ protected:
#ifdef DEBUG_ENABLED
private:
HashMap<NodePath, Ref<BTTask>> active_trees;
NodePath tracked_player;
String bt_resource_path;
HashSet<uint64_t> active_bt_instances;
uint64_t tracked_instance_id;
bool session_active = false;
void _track_tree(NodePath p_path);
void _track_tree(uint64_t p_instance_id);
void _untrack_tree();
void _send_active_bt_players();
void _on_bt_updated(int status, NodePath p_path);
void _on_state_updated(float _delta, NodePath p_path);
void _on_bt_instance_updated(int status, uint64_t p_instance_id);
public:
static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
@ -64,8 +64,8 @@ public:
bool parse_message_gdext(const String &p_msg, const Array &p_args);
#endif
void register_bt_instance(Ref<BTTask> p_instance, NodePath p_player_path);
void unregister_bt_instance(Ref<BTTask> p_instance, NodePath p_player_path);
void register_bt_instance(uint64_t p_instance_id);
void unregister_bt_instance(uint64_t p_instance_id);
#endif // ! DEBUG_ENABLED
};

View File

@ -58,7 +58,7 @@
//**** LimboDebuggerTab
void LimboDebuggerTab::_reset_controls() {
bt_player_list->clear();
bt_instance_list->clear();
bt_view->clear();
alert_box->hide();
info_message->set_text(TTR("Run project to start debugging."));
@ -68,7 +68,7 @@ void LimboDebuggerTab::_reset_controls() {
}
void LimboDebuggerTab::start_session() {
bt_player_list->clear();
bt_instance_list->clear();
bt_view->clear();
alert_box->hide();
info_message->set_text(TTR("Pick a player from the list to display behavior tree."));
@ -81,23 +81,25 @@ void LimboDebuggerTab::stop_session() {
session->send_message("limboai:stop_session", Array());
}
void LimboDebuggerTab::update_active_bt_players(const Array &p_node_paths) {
active_bt_players.clear();
for (int i = 0; i < p_node_paths.size(); i++) {
active_bt_players.push_back(p_node_paths[i]);
void LimboDebuggerTab::update_active_bt_instances(const Array &p_data) {
active_bt_instances.clear();
for (int i = 0; i < p_data.size(); i += 2) {
BTInstanceInfo info{ p_data[i], p_data[i + 1] };
active_bt_instances.push_back(info);
}
_update_bt_player_list(active_bt_players, filter_players->get_text());
_update_bt_instance_list(active_bt_instances, filter_players->get_text());
}
String LimboDebuggerTab::get_selected_bt_player() {
if (!bt_player_list->is_anything_selected()) {
return "";
uint64_t LimboDebuggerTab::get_selected_bt_instance_id() {
if (!bt_instance_list->is_anything_selected()) {
return 0;
}
return bt_player_list->get_item_text(bt_player_list->get_selected_items()[0]);
return bt_instance_list->get_item_metadata(bt_instance_list->get_selected_items()[0]);
}
void LimboDebuggerTab::update_behavior_tree(const Ref<BehaviorTreeData> &p_data) {
resource_header->set_text(p_data->bt_resource_path);
resource_header->set_text(p_data->source_bt_path);
resource_header->set_disabled(false);
bt_view->update_tree(p_data);
info_message->hide();
@ -108,58 +110,58 @@ void LimboDebuggerTab::_show_alert(const String &p_message) {
alert_box->set_visible(!p_message.is_empty());
}
void LimboDebuggerTab::_update_bt_player_list(const List<String> &p_node_paths, const String &p_filter) {
void LimboDebuggerTab::_update_bt_instance_list(const Vector<BTInstanceInfo> &p_instances, const String &p_filter) {
// Remember selected item.
String selected_player = "";
if (bt_player_list->is_anything_selected()) {
selected_player = bt_player_list->get_item_text(bt_player_list->get_selected_items()[0]);
uint64_t selected_instance_id = 0;
if (bt_instance_list->is_anything_selected()) {
selected_instance_id = uint64_t(bt_instance_list->get_item_metadata(bt_instance_list->get_selected_items()[0]));
}
bt_player_list->clear();
bt_instance_list->clear();
int select_idx = -1;
bool selection_filtered_out = false;
for (const String &p : p_node_paths) {
if (p_filter.is_empty() || p.contains(p_filter)) {
int idx = bt_player_list->add_item(p);
for (const BTInstanceInfo &info : p_instances) {
if (p_filter.is_empty() || info.owner_node_path.contains(p_filter)) {
int idx = bt_instance_list->add_item(info.owner_node_path);
bt_instance_list->set_item_metadata(idx, info.instance_id);
// Make item text shortened from the left, e.g ".../Agent/BTPlayer".
bt_player_list->set_item_text_direction(idx, TEXT_DIRECTION_RTL);
if (p == selected_player) {
bt_instance_list->set_item_text_direction(idx, TEXT_DIRECTION_RTL);
if (info.instance_id == selected_instance_id) {
select_idx = idx;
}
} else if (p == selected_player) {
} else if (info.instance_id == selected_instance_id) {
selection_filtered_out = true;
}
}
// Restore selected item.
if (select_idx > -1) {
bt_player_list->select(select_idx);
} else if (!selected_player.is_empty()) {
bt_instance_list->select(select_idx);
} else if (selected_instance_id != 0) {
if (selection_filtered_out) {
session->send_message("limboai:untrack_bt_player", Array());
bt_view->clear();
_show_alert("");
} else {
_show_alert("BehaviorTree player is no longer present.");
_show_alert(TTR("Behavior tree instance is no longer present."));
}
}
}
void LimboDebuggerTab::_bt_selected(int p_idx) {
void LimboDebuggerTab::_bt_instance_selected(int p_idx) {
alert_box->hide();
bt_view->clear();
info_message->set_text(TTR("Waiting for behavior tree update."));
info_message->show();
resource_header->set_text(TTR("Waiting for data"));
resource_header->set_disabled(true);
NodePath path = bt_player_list->get_item_text(p_idx);
Array msg_data;
msg_data.push_back(path);
msg_data.push_back(bt_instance_list->get_item_metadata(p_idx));
session->send_message("limboai:track_bt_player", msg_data);
}
void LimboDebuggerTab::_filter_changed(String p_text) {
_update_bt_player_list(active_bt_players, p_text);
_update_bt_instance_list(active_bt_instances, p_text);
}
void LimboDebuggerTab::_window_visibility_changed(bool p_visible) {
@ -185,7 +187,7 @@ void LimboDebuggerTab::_notification(int p_what) {
case NOTIFICATION_READY: {
resource_header->connect(LW_NAME(pressed), callable_mp(this, &LimboDebuggerTab::_resource_header_pressed));
filter_players->connect(LW_NAME(text_changed), callable_mp(this, &LimboDebuggerTab::_filter_changed));
bt_player_list->connect(LW_NAME(item_selected), callable_mp(this, &LimboDebuggerTab::_bt_selected));
bt_instance_list->connect(LW_NAME(item_selected), callable_mp(this, &LimboDebuggerTab::_bt_instance_selected));
update_interval->connect("value_changed", callable_mp(bt_view, &BehaviorTreeView::set_update_interval_msec));
Ref<ConfigFile> cf;
@ -271,11 +273,11 @@ LimboDebuggerTab::LimboDebuggerTab() {
filter_players->set_placeholder(TTR("Filter Players"));
list_box->add_child(filter_players);
bt_player_list = memnew(ItemList);
bt_player_list->set_custom_minimum_size(Size2(240.0 * EDSCALE, 0.0));
bt_player_list->set_h_size_flags(SIZE_FILL);
bt_player_list->set_v_size_flags(SIZE_EXPAND_FILL);
list_box->add_child(bt_player_list);
bt_instance_list = memnew(ItemList);
bt_instance_list->set_custom_minimum_size(Size2(240.0 * EDSCALE, 0.0));
bt_instance_list->set_h_size_flags(SIZE_FILL);
bt_instance_list->set_v_size_flags(SIZE_EXPAND_FILL);
list_box->add_child(bt_instance_list);
view_box = memnew(VBoxContainer);
hsc->add_child(view_box);
@ -352,10 +354,10 @@ bool LimboDebuggerPlugin::_capture(const String &p_message, const Array &p_data,
ERR_FAIL_NULL_V(tab, false);
bool captured = true;
if (p_message == "limboai:active_bt_players") {
tab->update_active_bt_players(p_data);
tab->update_active_bt_instances(p_data);
} else if (p_message == "limboai:bt_update") {
Ref<BehaviorTreeData> data = BehaviorTreeData::deserialize(p_data);
if (data->bt_player_path == NodePath(tab->get_selected_bt_player())) {
if (data->bt_instance_id == tab->get_selected_bt_instance_id()) {
tab->update_behavior_tree(data);
}
} else {

View File

@ -51,13 +51,18 @@ class LimboDebuggerTab : public PanelContainer {
GDCLASS(LimboDebuggerTab, PanelContainer);
private:
List<String> active_bt_players;
struct BTInstanceInfo {
uint64_t instance_id;
String owner_node_path;
};
Vector<BTInstanceInfo> active_bt_instances;
Ref<EditorDebuggerSession> session;
VBoxContainer *root_vb = nullptr;
HBoxContainer *toolbar = nullptr;
HSplitContainer *hsc = nullptr;
Label *info_message = nullptr;
ItemList *bt_player_list = nullptr;
ItemList *bt_instance_list = nullptr;
BehaviorTreeView *bt_view = nullptr;
VBoxContainer *view_box = nullptr;
HBoxContainer *alert_box = nullptr;
@ -71,8 +76,8 @@ private:
void _reset_controls();
void _show_alert(const String &p_message);
void _update_bt_player_list(const List<String> &p_node_paths, const String &p_filter);
void _bt_selected(int p_idx);
void _update_bt_instance_list(const Vector<BTInstanceInfo> &p_instances, const String &p_filter);
void _bt_instance_selected(int p_idx);
void _filter_changed(String p_text);
void _window_visibility_changed(bool p_visible);
void _resource_header_pressed();
@ -84,9 +89,9 @@ protected:
public:
void start_session();
void stop_session();
void update_active_bt_players(const Array &p_node_paths);
void update_active_bt_instances(const Array &p_data);
BehaviorTreeView *get_behavior_tree_view() const { return bt_view; }
String get_selected_bt_player();
uint64_t get_selected_bt_instance_id();
void update_behavior_tree(const Ref<BehaviorTreeData> &p_data);
void setup(Ref<EditorDebuggerSession> p_session, CompatWindowWrapper *p_wrapper);

View File

@ -148,6 +148,7 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) {
GDREGISTER_ABSTRACT_CLASS(BT);
GDREGISTER_ABSTRACT_CLASS(BTTask);
GDREGISTER_CLASS(BehaviorTree);
GDREGISTER_CLASS(BTInstance);
GDREGISTER_CLASS(BTPlayer);
GDREGISTER_CLASS(BTState);

View File

@ -54,6 +54,7 @@
#define GET_SCRIPT(m_obj) (m_obj->get_script_instance() ? m_obj->get_script_instance()->get_script() : nullptr)
#define ADD_STYLEBOX_OVERRIDE(m_control, m_name, m_stylebox) (m_control->add_theme_style_override(m_name, m_stylebox))
#define GET_NODE(m_parent, m_path) m_parent->get_node(m_path)
#define OBJECT_DB_GET_INSTANCE(m_id) ObjectDB::get_instance(ObjectID(m_id))
_FORCE_INLINE_ bool OBJECT_HAS_PROPERTY(Object *p_obj, const StringName &p_prop) {
bool r_valid;
@ -112,6 +113,7 @@ using namespace godot;
#define GET_SCRIPT(m_obj) (m_obj->get_script())
#define ADD_STYLEBOX_OVERRIDE(m_control, m_name, m_stylebox) (m_control->add_theme_stylebox_override(m_name, m_stylebox))
#define GET_NODE(m_parent, m_path) m_parent->get_node_internal(m_path)
#define OBJECT_DB_GET_INSTANCE(m_id) ObjectDB::get_instance(m_id)
_FORCE_INLINE_ bool OBJECT_HAS_PROPERTY(Object *p_obj, const StringName &p_prop) {
return Variant(p_obj).has_key(p_prop);

View File

@ -85,6 +85,7 @@ LimboStringNames::LimboStringNames() {
font_color = SN("font_color");
font_size = SN("font_size");
Forward = SN("Forward");
freed = SN("freed");
gui_input = SN("gui_input");
GuiOptionArrow = SN("GuiOptionArrow");
GuiTreeArrowDown = SN("GuiTreeArrowDown");

View File

@ -101,6 +101,7 @@ public:
StringName font_size;
StringName font;
StringName Forward;
StringName freed;
StringName gui_input;
StringName GuiOptionArrow;
StringName GuiTreeArrowDown;