Add `agent` parameter to `BTPlayer` to propagate upon `BehaviorTree` initialization, and add `scene_root` property to `BTTask`

`scene_root` is useful to resolve exported NodePath properties in `BTTask` instances (and for BBNode parameters).
This commit is contained in:
Serhii Snitsaruk 2024-05-01 23:20:17 +02:00
parent 75e8e68da4
commit 5dff2e537b
No known key found for this signature in database
GPG Key ID: A965EF8799FFEC2D
29 changed files with 73 additions and 41 deletions

View File

@ -71,10 +71,10 @@ 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) const {
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.");
Ref<BTTask> inst = root_task->clone();
inst->initialize(p_agent, p_blackboard);
inst->initialize(p_agent, p_blackboard, p_scene_root);
return inst;
}

View File

@ -53,7 +53,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) const;
Ref<BTTask> instantiate(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) const;
BehaviorTree();
~BehaviorTree();

View File

@ -50,9 +50,13 @@ void BTPlayer::_load_tree() {
}
#endif
tree_instance.unref();
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.");
tree_instance = behavior_tree->instantiate(get_owner(), blackboard);
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_node = GET_NODE(this, agent);
ERR_FAIL_NULL_MSG(agent_node, vformat("BTPlayer: Initialization failed - can't get agent by provided path '%s'.", agent));
Node *scene_root = get_owner();
ERR_FAIL_NULL_MSG(scene_root, "BTPlayer: Initialization failed - can't get scene root (make sure the BTPlayer.owner is set).");
tree_instance = behavior_tree->instantiate(agent_node, blackboard, scene_root);
#ifdef DEBUG_ENABLED
if (IS_DEBUGGER_ACTIVE()) {
LimboDebugger::get_singleton()->register_bt_instance(tree_instance, get_path());
@ -228,6 +232,8 @@ void BTPlayer::_notification(int p_notification) {
void BTPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_behavior_tree", "behavior_tree"), &BTPlayer::set_behavior_tree);
ClassDB::bind_method(D_METHOD("get_behavior_tree"), &BTPlayer::get_behavior_tree);
ClassDB::bind_method(D_METHOD("set_agent", "agent"), &BTPlayer::set_agent);
ClassDB::bind_method(D_METHOD("get_agent"), &BTPlayer::get_agent);
ClassDB::bind_method(D_METHOD("set_update_mode", "update_mode"), &BTPlayer::set_update_mode);
ClassDB::bind_method(D_METHOD("get_update_mode"), &BTPlayer::get_update_mode);
ClassDB::bind_method(D_METHOD("set_active", "active"), &BTPlayer::set_active);
@ -245,6 +251,7 @@ void BTPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_tree_instance"), &BTPlayer::get_tree_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"), "set_agent", "get_agent");
ADD_PROPERTY(PropertyInfo(Variant::INT, "update_mode", PROPERTY_HINT_ENUM, "Idle,Physics,Manual"), "set_update_mode", "get_update_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "active"), "set_active", "get_active");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "blackboard", PROPERTY_HINT_NONE, "Blackboard", 0), "set_blackboard", "get_blackboard");
@ -266,6 +273,7 @@ void BTPlayer::_bind_methods() {
BTPlayer::BTPlayer() {
blackboard = Ref<Blackboard>(memnew(Blackboard));
agent = LW_NAME(node_pp);
}
BTPlayer::~BTPlayer() {

View File

@ -37,6 +37,7 @@ public:
private:
Ref<BehaviorTree> behavior_tree;
NodePath agent;
Ref<BlackboardPlan> blackboard_plan;
UpdateMode update_mode = UpdateMode::PHYSICS;
bool active = true;
@ -57,6 +58,9 @@ public:
void set_behavior_tree(const Ref<BehaviorTree> &p_tree);
Ref<BehaviorTree> get_behavior_tree() const { return behavior_tree; };
void set_agent(const NodePath &p_agent) { agent = p_agent; }
NodePath get_agent() const { return agent; }
void set_blackboard_plan(const Ref<BlackboardPlan> &p_plan);
Ref<BlackboardPlan> get_blackboard_plan() const { return blackboard_plan; }

View File

@ -53,7 +53,7 @@ void BTState::_setup() {
LimboState::_setup();
ERR_FAIL_COND_MSG(behavior_tree.is_null(), "BTState: BehaviorTree is not assigned.");
// TODO: BBNode relies on agent to be scene owner, so if the user provides anything else, the behavior tree can break.
tree_instance = behavior_tree->instantiate(get_agent(), get_blackboard());
tree_instance = behavior_tree->instantiate(get_agent(), get_blackboard(), get_owner());
#ifdef DEBUG_ENABLED
if (tree_instance.is_valid() && IS_DEBUGGER_ACTIVE()) {

View File

@ -159,13 +159,15 @@ void BTTask::set_custom_name(const String &p_name) {
}
};
void BTTask::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard) {
ERR_FAIL_COND(p_agent == nullptr);
ERR_FAIL_COND(p_blackboard == nullptr);
void BTTask::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) {
ERR_FAIL_NULL(p_agent);
ERR_FAIL_NULL(p_blackboard);
ERR_FAIL_NULL(p_scene_root);
data.agent = p_agent;
data.blackboard = p_blackboard;
data.scene_root = p_scene_root;
for (int i = 0; i < data.children.size(); i++) {
get_child(i)->initialize(p_agent, p_blackboard);
get_child(i)->initialize(p_agent, p_blackboard, p_scene_root);
}
VCALL_OR_NATIVE(_setup);
@ -399,6 +401,7 @@ void BTTask::_bind_methods() {
// Properties, setters and getters.
ClassDB::bind_method(D_METHOD("get_agent"), &BTTask::get_agent);
ClassDB::bind_method(D_METHOD("set_agent", "agent"), &BTTask::set_agent);
ClassDB::bind_method(D_METHOD("get_scene_root"), &BTTask::get_scene_root);
ClassDB::bind_method(D_METHOD("_get_children"), &BTTask::_get_children);
ClassDB::bind_method(D_METHOD("_set_children", "children"), &BTTask::_set_children);
ClassDB::bind_method(D_METHOD("get_blackboard"), &BTTask::get_blackboard);
@ -410,6 +413,7 @@ void BTTask::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::STRING, "custom_name"), "set_custom_name", "get_custom_name");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "agent", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "set_agent", "get_agent");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "scene_root", PROPERTY_HINT_NODE_TYPE, "Node", PROPERTY_USAGE_NONE), "", "get_scene_root");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "blackboard", PROPERTY_HINT_RESOURCE_TYPE, "Blackboard", PROPERTY_USAGE_NONE), "", "get_blackboard");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "children", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_children", "_get_children");
ADD_PROPERTY(PropertyInfo(Variant::INT, "status", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "", "get_status");

View File

@ -75,6 +75,7 @@ private:
int index = -1;
String custom_name;
Node *agent = nullptr;
Node *scene_root = nullptr;
Ref<Blackboard> blackboard;
BTTask *parent = nullptr;
Vector<Ref<BTTask>> children;
@ -116,6 +117,8 @@ public:
_FORCE_INLINE_ Node *get_agent() const { return data.agent; }
void set_agent(Node *p_agent) { data.agent = p_agent; }
_FORCE_INLINE_ Node *get_scene_root() const { return data.scene_root; }
void set_display_collapsed(bool p_display_collapsed);
bool is_displayed_collapsed() const;
@ -126,7 +129,7 @@ public:
Ref<BTTask> get_root() const;
virtual Ref<BTTask> clone() const;
virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard);
virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root);
virtual PackedStringArray get_configuration_warnings(); // ! Native version.
Status execute(double p_delta);

View File

@ -20,7 +20,7 @@ void BTNewScope::set_blackboard_plan(const Ref<BlackboardPlan> &p_plan) {
emit_changed();
}
void BTNewScope::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard) {
void BTNewScope::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) {
ERR_FAIL_COND(p_agent == nullptr);
ERR_FAIL_COND(p_blackboard == nullptr);
@ -33,7 +33,7 @@ void BTNewScope::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard)
bb->set_parent(p_blackboard);
BTDecorator::initialize(p_agent, bb);
BTDecorator::initialize(p_agent, bb, p_scene_root);
}
BT::Status BTNewScope::_tick(double p_delta) {

View File

@ -34,7 +34,7 @@ protected:
virtual Status _tick(double p_delta) override;
public:
virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard) override;
virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) override;
};
#endif // BT_NEW_SCOPE_H

View File

@ -44,14 +44,14 @@ String BTSubtree::_generate_name() {
return vformat("Subtree %s", s);
}
void BTSubtree::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard) {
void BTSubtree::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) {
ERR_FAIL_COND_MSG(!subtree.is_valid(), "Subtree is not assigned.");
ERR_FAIL_COND_MSG(!subtree->get_root_task().is_valid(), "Subtree root task is not valid.");
ERR_FAIL_COND_MSG(get_child_count() != 0, "Subtree task shouldn't have children during initialization.");
add_child(subtree->get_root_task()->clone());
BTNewScope::initialize(p_agent, p_blackboard);
BTNewScope::initialize(p_agent, p_blackboard, p_scene_root);
}
BT::Status BTSubtree::_tick(double p_delta) {

View File

@ -35,7 +35,7 @@ public:
void set_subtree(const Ref<BehaviorTree> &p_value);
Ref<BehaviorTree> get_subtree() const { return subtree; }
virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard) override;
virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node *p_scene_root) override;
virtual PackedStringArray get_configuration_warnings() override;
BTSubtree() = default;

View File

@ -51,13 +51,13 @@ TEST_CASE("[SceneTree][LimboAI] BTAwaitAnimation") {
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
awa->initialize(dummy, bb);
awa->initialize(dummy, bb, dummy);
CHECK(awa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
awa->initialize(dummy, bb);
awa->initialize(dummy, bb, dummy);
SUBCASE("When AnimationPlayer is not playing") {
REQUIRE_FALSE(player->is_playing());

View File

@ -47,7 +47,7 @@ TEST_CASE("[Modules][LimboAI] BTCallMethod") {
node_param->set_variable("object");
cm->set_method("callback");
cm->initialize(dummy, bb);
cm->initialize(dummy, bb, dummy);
SUBCASE("When method is empty") {
cm->set_method("");

View File

@ -39,7 +39,7 @@ TEST_CASE("[Modules][LimboAI] BTCheckAgentProperty") {
Ref<BTCheckAgentProperty> cap = memnew(BTCheckAgentProperty);
Node *agent = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
cap->initialize(agent, bb);
cap->initialize(agent, bb, agent);
StringName agent_name = "SimpleNode";
agent->set_name(agent_name);

View File

@ -24,7 +24,7 @@ TEST_CASE("[Modules][LimboAI] BTCheckTrigger") {
Node *dummy = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
ct->initialize(dummy, bb);
ct->initialize(dummy, bb, dummy);
SUBCASE("Empty") {
ERR_PRINT_OFF;

View File

@ -36,7 +36,7 @@ TEST_CASE("[Modules][LimboAI] BTCheckVar") {
Ref<BTCheckVar> cv = memnew(BTCheckVar);
Ref<Blackboard> bb = memnew(Blackboard);
Node *dummy = memnew(Node);
cv->initialize(dummy, bb);
cv->initialize(dummy, bb, dummy);
SUBCASE("Check with empty variable and value") {
cv->set_variable("");

View File

@ -47,7 +47,7 @@ TEST_CASE("[Modules][LimboAI] BTEvaluateExpression") {
node_param->set_variable("object");
ee->set_expression_string("callback()");
ee->initialize(dummy, bb);
ee->initialize(dummy, bb, dummy);
SUBCASE("When expression string is empty") {
ee->set_expression_string("");

View File

@ -23,7 +23,7 @@ TEST_CASE("[Modules][LimboAI] BTForEach") {
Ref<BTForEach> fe = memnew(BTForEach);
Node *dummy = memnew(Node);
Ref<Blackboard> blackboard = memnew(Blackboard);
fe->initialize(dummy, blackboard);
fe->initialize(dummy, blackboard, dummy);
Array arr;
arr.append("apple");

View File

@ -26,7 +26,7 @@ TEST_CASE("[Modules][LimboAI] BTNewScope") {
SUBCASE("When empty") {
ERR_PRINT_OFF;
ns->initialize(dummy, parent_bb);
ns->initialize(dummy, parent_bb, dummy);
CHECK(ns->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
@ -45,7 +45,7 @@ TEST_CASE("[Modules][LimboAI] BTNewScope") {
REQUIRE(parent_bb->has_var("vegetable"));
REQUIRE(parent_bb->get_var("vegetable", "wetgoop") == "carrot");
parent->initialize(dummy, parent_bb);
parent->initialize(dummy, parent_bb, dummy);
CHECK(ns->get_blackboard() != parent->get_blackboard());
CHECK(ns->get_blackboard() == child->get_blackboard());

View File

@ -48,13 +48,13 @@ TEST_CASE("[SceneTree][LimboAI] BTPauseAnimation") {
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
pa->initialize(dummy, bb);
pa->initialize(dummy, bb, dummy);
CHECK(pa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
pa->initialize(dummy, bb);
pa->initialize(dummy, bb, dummy);
SUBCASE("When AnimationPlayer is not playing") {
REQUIRE_FALSE(player->is_playing());

View File

@ -49,13 +49,13 @@ TEST_CASE("[SceneTree][LimboAI] BTPlayAnimation") {
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
pa->initialize(dummy, bb);
pa->initialize(dummy, bb, dummy);
CHECK(pa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
pa->initialize(dummy, bb);
pa->initialize(dummy, bb, dummy);
SUBCASE("When not waiting to finish") {
pa->set_await_completion(0.0);

View File

@ -28,7 +28,7 @@ TEST_CASE("[Modules][LimboAI] BTSetAgentProperty") {
Ref<BTSetAgentProperty> sap = memnew(BTSetAgentProperty);
Node *agent = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
sap->initialize(agent, bb);
sap->initialize(agent, bb, agent);
sap->set_property("process_priority"); // * property that will be set by the task
Ref<BBVariant> value = memnew(BBVariant);

View File

@ -29,7 +29,7 @@ TEST_CASE("[Modules][LimboAI] BTSetVar") {
Ref<Blackboard> bb = memnew(Blackboard);
Node *dummy = memnew(Node);
sv->initialize(dummy, bb);
sv->initialize(dummy, bb, dummy);
SUBCASE("When variable is not set") {
ERR_PRINT_OFF;

View File

@ -48,13 +48,13 @@ TEST_CASE("[SceneTree][LimboAI] BTStopAnimation") {
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
sa->initialize(dummy, bb);
sa->initialize(dummy, bb, dummy);
CHECK(sa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
sa->initialize(dummy, bb);
sa->initialize(dummy, bb, dummy);
SUBCASE("When AnimationPlayer is not playing") {
REQUIRE_FALSE(player->is_playing());

View File

@ -29,7 +29,7 @@ TEST_CASE("[Modules][LimboAI] BTSubtree") {
SUBCASE("When empty") {
ERR_PRINT_OFF;
st->initialize(dummy, bb);
st->initialize(dummy, bb, dummy);
CHECK(st->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
@ -41,7 +41,7 @@ TEST_CASE("[Modules][LimboAI] BTSubtree") {
st->set_subtree(bt);
CHECK(st->get_child_count() == 0);
st->initialize(dummy, bb);
st->initialize(dummy, bb, dummy);
CHECK(st->get_child_count() == 1);
CHECK(st->get_child(0) != task);

View File

@ -162,7 +162,7 @@ TEST_CASE("[Modules][LimboAI] BTTask") {
Node *dummy = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
SUBCASE("With valid parameters") {
task->initialize(dummy, bb);
task->initialize(dummy, bb, dummy);
CHECK(task->get_agent() == dummy);
CHECK(task->get_blackboard() == bb);
CHECK(child1->get_agent() == dummy);
@ -174,12 +174,17 @@ TEST_CASE("[Modules][LimboAI] BTTask") {
}
SUBCASE("Test if not crashes when agent is null") {
ERR_PRINT_OFF;
task->initialize(nullptr, bb);
task->initialize(nullptr, bb, dummy);
ERR_PRINT_ON;
}
SUBCASE("Test if not crashes when scene_owner is null") {
ERR_PRINT_OFF;
task->initialize(dummy, bb, nullptr);
ERR_PRINT_ON;
}
SUBCASE("Test if not crashes when BB is null") {
ERR_PRINT_OFF;
task->initialize(dummy, nullptr);
task->initialize(dummy, nullptr, dummy);
ERR_PRINT_ON;
}
memdelete(dummy);

View File

@ -53,6 +53,7 @@
#define PERFORMANCE_ADD_CUSTOM_MONITOR(m_id, m_callable) (Performance::get_singleton()->add_custom_monitor(m_id, m_callable, Variant()))
#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)
_FORCE_INLINE_ bool OBJECT_HAS_PROPERTY(Object *p_obj, const StringName &p_prop) {
bool r_valid;
@ -137,6 +138,7 @@ using namespace godot;
#define PERFORMANCE_ADD_CUSTOM_MONITOR(m_id, m_callable) (Performance::get_singleton()->add_custom_monitor(m_id, m_callable))
#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)
_FORCE_INLINE_ bool OBJECT_HAS_PROPERTY(Object *p_obj, const StringName &p_prop) {
return Variant(p_obj).has_key(p_prop);

View File

@ -166,4 +166,6 @@ LimboStringNames::LimboStringNames() {
repeat_forever.parse_utf8("Repeat ∞");
output_var_prefix.parse_utf8("");
node_pp = NodePath("..");
}

View File

@ -13,6 +13,7 @@
#define LIMBO_STRING_NAMES_H
#ifdef LIMBOAI_MODULE
#include "core/string/node_path.h"
#include "core/string/string_name.h"
#include "core/typedefs.h"
#include "modules/register_module_types.h"
@ -20,6 +21,7 @@
#ifdef LIMBOAI_GDEXTENSION
#include "godot_cpp/variant/string.hpp"
#include <godot_cpp/variant/node_path.hpp>
#include <godot_cpp/variant/string_name.hpp>
using namespace godot;
#endif // LIMBOAI_GDEXTENSION
@ -181,6 +183,8 @@ public:
String repeat_forever;
String output_var_prefix;
NodePath node_pp;
};
#define LW_NAME(m_arg) LimboStringNames::get_singleton()->m_arg