feat: implemented behaviour tree

This commit is contained in:
Sara Gerretsen 2026-03-12 21:37:29 +01:00
parent fc11921776
commit c2aa6690c3
15 changed files with 465 additions and 3 deletions

View file

@ -0,0 +1,31 @@
#include "behaviour_action.h"
#include "core/object/object.h"
void BehaviourAction::_bind_methods() {
ClassDB::add_virtual_method(get_class_static(), _gdvirtual__execute_get_method_info());
ClassDB::add_virtual_method(get_class_static(), _gdvirtual__exit_get_method_info());
ClassDB::add_virtual_method(get_class_static(), _gdvirtual__enter_get_method_info());
}
void BehaviourAction::enter() {
GDVIRTUAL_CALL(_enter);
}
void BehaviourAction::execute() {
Status out_status{ Fail };
GDVIRTUAL_CALL(_execute, out_status);
set_status(out_status);
}
void BehaviourAction::exit() {
GDVIRTUAL_CALL(_exit);
}
BehaviourNode *BehaviourAction::get_next() {
switch (get_status()) {
case Running:
return this;
default:
return cast_to<BehaviourNode>(get_parent());
}
}

View file

@ -0,0 +1,17 @@
#pragma once
#include "behaviour_nodes/behaviour_node.h"
class BehaviourAction : public BehaviourNode {
GDCLASS(BehaviourAction, BehaviourNode);
static void _bind_methods();
public:
void enter() override;
void execute() override;
void exit() override;
BehaviourNode *get_next() override;
GDVIRTUAL0R_REQUIRED(Status, _execute);
GDVIRTUAL0(_enter);
GDVIRTUAL0(_exit);
};

View file

@ -0,0 +1,28 @@
#include "behaviour_composite.h"
void BehaviourComposite::_bind_methods() {}
void BehaviourComposite::child_order_changed() {
this->child_behaviours.clear();
for (Variant var : get_children()) {
if (BehaviourNode * node{ cast_to<BehaviourNode>(var) }) {
this->child_behaviours.push_back(node);
}
}
}
void BehaviourComposite::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_READY:
child_order_changed();
set_leaf(get_child_behaviours().is_empty());
return;
case NOTIFICATION_CHILD_ORDER_CHANGED:
if (is_ready()) {
child_order_changed();
}
return;
}
}

View file

@ -0,0 +1,18 @@
#pragma once
#include "behaviour_nodes/behaviour_node.h"
class BehaviourComposite : public BehaviourNode {
GDCLASS(BehaviourComposite, BehaviourNode);
static void _bind_methods();
void child_order_changed();
protected:
void _notification(int what);
private:
Vector<BehaviourNode *> child_behaviours{};
public:
Vector<BehaviourNode *> const &get_child_behaviours() const { return this->child_behaviours; }
GET_SET_REF_FNS(Vector<BehaviourNode *>, child_behaviours);
};

View file

@ -0,0 +1,29 @@
#include "behaviour_node.h"
#include "behaviour_nodes/behaviour_tree.h"
#include "core/config/engine.h"
void BehaviourNode::_bind_methods() {
BIND_ENUM_CONSTANT(Fail);
BIND_ENUM_CONSTANT(Running);
BIND_ENUM_CONSTANT(Success);
}
void BehaviourNode::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
Node *parent{ get_parent() };
this->parent = cast_to<BehaviourNode>(parent);
while (this->behaviour_tree == nullptr && parent != nullptr) {
if ((this->behaviour_tree = cast_to<BehaviourTree>(parent))) {
break;
}
parent = parent->get_parent();
}
return;
}
}

View file

@ -0,0 +1,34 @@
#pragma once
#include "macros.h"
#include "scene/main/node.h"
class BehaviourNode : public Node {
GDCLASS(BehaviourNode, Node);
static void _bind_methods();
public:
GDENUM(Status, Fail, Running, Success);
protected:
void _notification(int what);
public:
virtual void enter() {}
virtual void execute() {}
virtual void exit() {}
virtual BehaviourNode *get_next() { return this; }
private:
class BehaviourTree *behaviour_tree{ nullptr };
BehaviourNode *parent{ nullptr };
Status status{};
bool leaf{ false };
public:
GET_SET_FNS(class BehaviourTree *, behaviour_tree);
GET_SET_FNS(Status, status);
GET_SET_FNS(bool, leaf);
};
MAKE_TYPE_INFO(BehaviourNode::Status, Variant::INT);

View file

@ -0,0 +1,49 @@
#include "behaviour_tree.h"
#include "behaviour_nodes/behaviour_node.h"
#include "behaviour_nodes/control_nodes.h"
#include "core/config/engine.h"
void BehaviourTree::_bind_methods() {}
void BehaviourTree::process() {
this->current->execute();
while (execute_next()) {
this->current->execute();
}
}
void BehaviourTree::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_READY:
for (Variant var : get_children()) {
if ((this->current = cast_to<BehaviourNode>(var))) {
break;
}
}
ERR_FAIL_COND_EDMSG(this->current == nullptr, "No valid BehaviourNode in BehaviourTree");
set_process(true);
return;
case NOTIFICATION_PROCESS:
process();
return;
}
}
bool BehaviourTree::execute_next() {
BehaviourNode *next{ this->current->get_next() };
if (next == this->current) {
return false;
} else {
ERR_FAIL_COND_V_EDMSG(next == nullptr, false, vformat("%s::get_next returned a nullptr, repeating last node", this->current->get_class()));
print_line("new behaviour:", this->current->get_path());
this->current->exit();
this->current = next;
this->current->enter();
return cast_to<BehaviourComposite>(this->current);
}
}

View file

@ -0,0 +1,18 @@
#pragma once
#include "scene/main/node.h"
class BehaviourTree : public Node {
GDCLASS(BehaviourTree, Node);
static void _bind_methods();
void process();
protected:
void _notification(int what);
public:
bool execute_next();
private:
class BehaviourNode *current{ nullptr };
};

View file

@ -0,0 +1,98 @@
#include "control_nodes.h"
#include "behaviour_nodes/behaviour_node.h"
void BehaviourSequence::_bind_methods() {}
PackedStringArray BehaviourSequence::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (get_child_behaviours().is_empty()) {
warnings.push_back("Sequence cannot have zero children");
}
return warnings;
}
void BehaviourSequence::execute() {
if (get_child_behaviours().is_empty()) {
set_status(Fail);
ERR_FAIL_EDMSG("BehaviourSequence executed with no children.");
} else if (get_status() == Running && this->current >= 0 && this->current < get_child_behaviours().size()) {
switch (get_child_behaviours().get(this->current)->get_status()) {
case Running:
set_status(Running);
break;
case Fail:
set_status(Fail);
break;
case Success:
++this->current;
set_status(this->current >= get_child_behaviours().size() ? Success : Running);
break;
}
} else {
set_status(Running);
this->current = 0;
}
}
BehaviourNode *BehaviourSequence::get_next() {
return get_status() == Running
? get_child_behaviours().get(this->current)
: cast_to<BehaviourNode>(get_parent());
}
void BehaviourSelector::_bind_methods() {}
PackedStringArray BehaviourSelector::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (get_child_behaviours().is_empty()) {
warnings.push_back("Selector cannot have zero children");
}
return warnings;
}
void BehaviourSelector::execute() {
if (get_child_behaviours().is_empty()) {
set_status(Fail);
ERR_FAIL_EDMSG("BehaviourSelector execution with no children.");
} else if (get_status() == Running && this->current >= 0 && this->current < get_child_behaviours().size()) {
switch (get_child_behaviours().get(this->current)->get_status()) {
case Running:
set_status(Running);
break;
case Fail:
++this->current;
set_status(this->current >= get_child_behaviours().size() ? Fail : Running);
break;
case Success:
set_status(Success);
break;
}
} else {
set_status(Running);
this->current = 0;
}
}
BehaviourNode *BehaviourSelector::get_next() {
return get_status() == Running
? get_child_behaviours().get(this->current)
: cast_to<BehaviourNode>(get_parent());
}
void BehaviourRepeater::_bind_methods() {}
PackedStringArray BehaviourRepeater::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (get_child_behaviours().size() != 1) {
warnings.push_back(vformat("Repeater should have exactly one BehaviourNode child, has %d", get_child_behaviours().size()));
}
return warnings;
}
void BehaviourRepeater::execute() {
set_status(Running);
}
BehaviourNode *BehaviourRepeater::get_next() {
return get_child_behaviours().get(0);
}

View file

@ -0,0 +1,40 @@
#pragma once
#include "behaviour_nodes/behaviour_composite.h"
#include "behaviour_nodes/behaviour_node.h"
#include "core/variant/variant.h"
class BehaviourSequence : public BehaviourComposite {
GDCLASS(BehaviourSequence, BehaviourComposite);
static void _bind_methods();
public:
PackedStringArray get_configuration_warnings() const override;
void execute() override;
BehaviourNode *get_next() override;
private:
int current{ -1 };
};
class BehaviourSelector : public BehaviourComposite {
GDCLASS(BehaviourSelector, BehaviourNode);
static void _bind_methods();
public:
PackedStringArray get_configuration_warnings() const override;
void execute() override;
BehaviourNode *get_next() override;
private:
int current{ -1 };
};
class BehaviourRepeater : public BehaviourComposite {
GDCLASS(BehaviourRepeater, BehaviourNode);
static void _bind_methods();
public:
PackedStringArray get_configuration_warnings() const override;
void execute() override;
BehaviourNode *get_next() override;
};

View file

@ -0,0 +1,21 @@
#include "decorator_nodes.h"
#include "behaviour_nodes/behaviour_node.h"
void BehaviourAlwaysSuccess::_bind_methods() {}
void BehaviourAlwaysSuccess::execute() {
if (get_child_behaviours().is_empty()) {
set_status(Fail);
ERR_FAIL_EDMSG("BehaviourSequence executed with no children.");
} else if (get_status() == Running) {
set_status(get_child_behaviours().get(0)->get_status() == Running ? Running : Success);
} else {
set_status(Running);
}
}
BehaviourNode *BehaviourAlwaysSuccess::get_next() {
return get_status() == Running
? get_child_behaviours().get(0)
: cast_to<BehaviourNode>(get_parent());
}

View file

@ -0,0 +1,13 @@
#pragma once
#include "behaviour_nodes/behaviour_composite.h"
#include "behaviour_nodes/behaviour_node.h"
class BehaviourAlwaysSuccess : public BehaviourComposite {
GDCLASS(BehaviourAlwaysSuccess, BehaviourComposite);
static void _bind_methods();
public:
void execute() override;
BehaviourNode *get_next() override;
};

View file

@ -1,9 +1,20 @@
#include "behaviour_nodes/register_types.h"
#include "behaviour_nodes/behaviour_action.h"
#include "behaviour_nodes/behaviour_node.h"
#include "behaviour_nodes/behaviour_tree.h"
#include "behaviour_nodes/control_nodes.h"
#include "core/object/class_db.h"
void initialize_behaviour_nodes_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
ClassDB::register_class<BehaviourTree>();
ClassDB::register_abstract_class<BehaviourNode>();
ClassDB::register_class<BehaviourSequence>();
ClassDB::register_class<BehaviourRepeater>();
ClassDB::register_class<BehaviourSelector>();
ClassDB::register_class<BehaviourAction>();
}
void uninitialize_behaviour_nodes_module(ModuleInitializationLevel p_level) {

View file

@ -24,6 +24,13 @@
void set_##m_property(m_type value) { \
this->m_property = value; \
}
#define GET_SET_REF_FNS(m_type, m_property) \
m_type &get_##m_property() { \
return this->m_property; \
} \
void set_##m_property(m_type &value) { \
this->m_property = value; \
}
#define GET_SET_FNS_EX(m_type, m_property, m_ex) \
m_type get_##m_property() const { \

View file

@ -6,6 +6,40 @@
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_4iifp"]
[sub_resource type="GDScript" id="GDScript_0torn"]
script/source = "extends BehaviourAction
var done : bool = false
func is_done():
done = true
func _ready():
$Timer.timeout.connect(is_done)
func _enter():
$Timer.start(.1)
done = false
func _execute() -> int:
return Success if done else Running;
"
[sub_resource type="GDScript" id="GDScript_ilnb3"]
script/source = "extends BehaviourAction
func _execute() -> int:
return Fail
"
[sub_resource type="GDScript" id="GDScript_p8kq6"]
script/source = "extends BehaviourAction
func _execute() -> int:
push_error(\"THIS SHOULDN'T BE HAPPENING\")
return Running
"
[node name="PartyMember" type="Character" unique_id=2124931928]
data = ExtResource("1_0torn")
@ -16,9 +50,23 @@ skeleton = NodePath("../CollisionShape3D")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=705075802]
shape = SubResource("CapsuleShape3D_4iifp")
[node name="PartyMemberFollow" type="PartyMemberFollow" parent="." unique_id=1261360781]
start_active = true
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=1509674092]
unique_name_in_owner = true
avoidance_enabled = true
[node name="BehaviourTree" type="BehaviourTree" parent="." unique_id=1234785753]
[node name="BehaviourRepeater" type="BehaviourRepeater" parent="BehaviourTree" unique_id=144360337]
[node name="BehaviourSelector" type="BehaviourSelector" parent="BehaviourTree/BehaviourRepeater" unique_id=1728256939]
[node name="BehaviourSequence" type="BehaviourSequence" parent="BehaviourTree/BehaviourRepeater/BehaviourSelector" unique_id=698711477]
[node name="BehaviourAction" type="BehaviourAction" parent="BehaviourTree/BehaviourRepeater/BehaviourSelector/BehaviourSequence" unique_id=1778836105]
script = SubResource("GDScript_0torn")
[node name="BehaviourAction2" type="BehaviourAction" parent="BehaviourTree/BehaviourRepeater/BehaviourSelector/BehaviourSequence" unique_id=63024452]
script = SubResource("GDScript_ilnb3")
[node name="BehaviourAction3" type="BehaviourAction" parent="BehaviourTree/BehaviourRepeater/BehaviourSelector/BehaviourSequence" unique_id=1490528246]
script = SubResource("GDScript_p8kq6")