Add BTPlayer and BehaviorTree classes, and fix leaking refs

This commit is contained in:
Serhii Snitsaruk 2022-08-30 18:48:49 +02:00
parent 6105c17f09
commit a236aee22b
20 changed files with 423 additions and 22 deletions

View File

@ -0,0 +1,45 @@
/* behavior_tree.cpp */
#include "behavior_tree.h"
#include "core/class_db.h"
#include "core/list.h"
#include "core/object.h"
#include "core/variant.h"
#include <cstddef>
void BehaviorTree::init() {
List<BTTask *> stack;
BTTask *task = root_task.ptr();
while (task != nullptr) {
for (int i = 0; i < task->get_child_count(); i++) {
task->get_child(i)->_parent = task;
stack.push_back(task->get_child(i).ptr());
}
task = nullptr;
if (!stack.empty()) {
task = stack.front()->get();
stack.pop_front();
}
}
}
Ref<BehaviorTree> BehaviorTree::clone() const {
Ref<BehaviorTree> copy = duplicate(false);
copy->set_path("");
if (root_task.is_valid()) {
copy->root_task = root_task->clone();
}
return copy;
}
void BehaviorTree::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_description", "p_value"), &BehaviorTree::set_description);
ClassDB::bind_method(D_METHOD("get_description"), &BehaviorTree::get_description);
ClassDB::bind_method(D_METHOD("set_root_task", "p_value"), &BehaviorTree::set_root_task);
ClassDB::bind_method(D_METHOD("get_root_task"), &BehaviorTree::get_root_task);
ClassDB::bind_method(D_METHOD("init"), &BehaviorTree::init);
ClassDB::bind_method(D_METHOD("clone"), &BehaviorTree::clone);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "description", PROPERTY_HINT_MULTILINE_TEXT), "set_description", "get_description");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "root_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"), "set_root_task", "get_root_task");
}

View File

@ -0,0 +1,38 @@
/* behavior_tree.h */
#ifndef BEHAVIOR_TREE_H
#define BEHAVIOR_TREE_H
#include "core/object.h"
#include "core/resource.h"
#include "bt_task.h"
class BehaviorTree : public Resource {
GDCLASS(BehaviorTree, Resource);
private:
String description;
Ref<BTTask> root_task;
protected:
static void _bind_methods();
public:
void set_description(String p_value) {
description = p_value;
emit_changed();
}
String get_description() const { return description; }
void set_root_task(const Ref<BTTask> &p_value) {
root_task = p_value;
emit_changed();
}
Ref<BTTask> get_root_task() const { return root_task; }
void init();
Ref<BehaviorTree> clone() const;
};
#endif // BEHAVIOR_TREE_H

111
limboai/bt/bt_player.cpp Normal file
View File

@ -0,0 +1,111 @@
/* bt_player.cpp */
#include "bt_player.h"
#include "../limbo_string_names.h"
#include "bt_task.h"
#include "core/class_db.h"
#include "core/engine.h"
#include "core/io/resource_loader.h"
#include "core/object.h"
#include <cstddef>
VARIANT_ENUM_CAST(BTPlayer::UpdateMode);
void BTPlayer::_load_tree() {
_loaded_tree.unref();
_root_task.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(), "Behavior tree has no valid root task.");
_loaded_tree = behavior_tree;
_root_task = _loaded_tree->get_root_task()->clone();
_root_task->initialize(get_owner(), blackboard);
}
void BTPlayer::set_behavior_tree(const Ref<BehaviorTree> &p_tree) {
behavior_tree = p_tree;
if (Engine::get_singleton()->is_editor_hint() == false) {
_load_tree();
set_update_mode(update_mode);
}
}
void BTPlayer::set_update_mode(UpdateMode p_mode) {
update_mode = p_mode;
set_active(active);
}
void BTPlayer::set_active(bool p_active) {
active = p_active;
if (!Engine::get_singleton()->is_editor_hint()) {
set_process(update_mode == UpdateMode::IDLE);
set_physics_process(update_mode == UpdateMode::PHYSICS);
}
}
void BTPlayer::update(float p_delta) {
if (!_root_task.is_valid()) {
ERR_PRINT_ONCE(vformat("BTPlayer has no root task to update (owner: %s)", get_owner()));
return;
}
if (active) {
int status = _root_task->execute(p_delta);
if (status == BTTask::SUCCESS || status == BTTask::FAILURE) {
set_active(auto_restart);
emit_signal(LimboStringNames::get_singleton()->behavior_tree_finished, status);
}
}
}
void BTPlayer::restart() {
_root_task->cancel();
set_active(true);
}
void BTPlayer::_notification(int p_notification) {
switch (p_notification) {
case NOTIFICATION_PROCESS: {
if (active) {
Variant time = get_process_delta_time();
update(time);
}
} break;
case NOTIFICATION_PHYSICS_PROCESS: {
if (active) {
Variant time = get_process_delta_time();
update(time);
}
} break;
case NOTIFICATION_READY: {
if (!Engine::get_singleton()->is_editor_hint()) {
if (behavior_tree.is_valid()) {
_load_tree();
}
set_active(active);
}
} break;
}
}
void BTPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_behavior_tree", "p_path"), &BTPlayer::set_behavior_tree);
ClassDB::bind_method(D_METHOD("get_behavior_tree"), &BTPlayer::get_behavior_tree);
ClassDB::bind_method(D_METHOD("set_update_mode", "p_mode"), &BTPlayer::set_update_mode);
ClassDB::bind_method(D_METHOD("get_update_mode"), &BTPlayer::get_update_mode);
ClassDB::bind_method(D_METHOD("set_active", "p_active"), &BTPlayer::set_active);
ClassDB::bind_method(D_METHOD("get_active"), &BTPlayer::get_active);
ClassDB::bind_method(D_METHOD("set_auto_restart", "p_value"), &BTPlayer::set_auto_restart);
ClassDB::bind_method(D_METHOD("get_auto_restart"), &BTPlayer::get_auto_restart);
ClassDB::bind_method(D_METHOD("update", "p_delta"), &BTPlayer::update);
ClassDB::bind_method(D_METHOD("restart"), &BTPlayer::restart);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "behavior_tree", PROPERTY_HINT_RESOURCE_TYPE, "BehaviorTree"), "set_behavior_tree", "get_behavior_tree");
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::BOOL, "auto_restart"), "set_auto_restart", "get_auto_restart");
BIND_ENUM_CONSTANT(IDLE);
BIND_ENUM_CONSTANT(PHYSICS);
BIND_ENUM_CONSTANT(MANUAL);
}

57
limboai/bt/bt_player.h Normal file
View File

@ -0,0 +1,57 @@
/* bt_player.h */
#ifndef BT_PLAYER_H
#define BT_PLAYER_H
#include "core/object.h"
#include "scene/main/node.h"
#include "behavior_tree.h"
#include "bt_task.h"
#include <pulse/proplist.h>
class BTPlayer : public Node {
GDCLASS(BTPlayer, Node);
public:
enum UpdateMode : unsigned int {
IDLE, // automatically call update() during NOTIFICATION_PROCESS
PHYSICS, //# automatically call update() during NOTIFICATION_PHYSICS
MANUAL, // manually update state machine, user must call update(delta)
};
private:
Ref<BehaviorTree> behavior_tree;
UpdateMode update_mode = UpdateMode::IDLE;
bool active = false;
bool auto_restart = false;
Dictionary blackboard;
Ref<BehaviorTree> _loaded_tree;
Ref<BTTask> _root_task;
void _load_tree();
protected:
static void _bind_methods();
void _notification(int p_notification);
public:
void set_behavior_tree(const Ref<BehaviorTree> &p_tree);
Ref<BehaviorTree> get_behavior_tree() const { return behavior_tree; };
void set_update_mode(UpdateMode p_mode);
UpdateMode get_update_mode() const { return update_mode; }
void set_active(bool p_active);
bool get_active() const { return active; }
void set_auto_restart(bool p_value) { auto_restart = p_value; }
bool get_auto_restart() const { return auto_restart; }
void update(float p_delta);
void restart();
};
#endif // BT_PLAYER_H

View File

@ -2,14 +2,14 @@
#include "bt_task.h" #include "bt_task.h"
#include "../limbo_string_names.h"
#include "../limbo_utility.h"
#include "core/class_db.h" #include "core/class_db.h"
#include "core/object.h" #include "core/object.h"
#include "core/script_language.h" #include "core/script_language.h"
#include "core/variant.h" #include "core/variant.h"
#include "editor/editor_node.h" #include "editor/editor_node.h"
#include <cstddef>
#include "../limbo_string_names.h"
#include "../limbo_utility.h"
String BTTask::_generate_name() const { String BTTask::_generate_name() const {
if (get_script_instance()) { if (get_script_instance()) {
@ -58,7 +58,7 @@ String BTTask::get_task_name() const {
Ref<BTTask> BTTask::get_root() const { Ref<BTTask> BTTask::get_root() const {
const BTTask *task = this; const BTTask *task = this;
while (!task->is_root()) { while (!task->is_root()) {
task = task->get_parent().ptr(); task = task->_parent;
} }
return Ref<BTTask>(task); return Ref<BTTask>(task);
} }
@ -86,11 +86,11 @@ void BTTask::initialize(Object *p_agent, Dictionary p_blackboard) {
Ref<BTTask> BTTask::clone() const { Ref<BTTask> BTTask::clone() const {
Ref<BTTask> inst = duplicate(true); Ref<BTTask> inst = duplicate(true);
inst.ptr()->_parent.unref(); inst->_parent = nullptr;
CRASH_COND(inst.ptr()->get_parent().is_valid()); CRASH_COND(inst->get_parent().is_valid());
for (int i = 0; i < _children.size(); i++) { for (int i = 0; i < _children.size(); i++) {
Ref<BTTask> c = get_child(i)->clone(); Ref<BTTask> c = get_child(i)->clone();
c->_parent = inst; c->_parent = inst.ptr();
inst->_children.set(i, c); inst->_children.set(i, c);
} }
return inst; return inst;
@ -152,19 +152,19 @@ int BTTask::get_child_count() const {
} }
void BTTask::add_child(Ref<BTTask> p_child) { void BTTask::add_child(Ref<BTTask> p_child) {
ERR_FAIL_COND_MSG(p_child.ptr()->get_parent().is_valid(), "p_child already has a parent!"); ERR_FAIL_COND_MSG(p_child->get_parent().is_valid(), "p_child already has a parent!");
p_child->_parent = Ref<BTTask>(this); p_child->_parent = this;
_children.push_back(p_child); _children.push_back(p_child);
emit_changed(); emit_changed();
} }
void BTTask::add_child_at_index(Ref<BTTask> p_child, int p_idx) { void BTTask::add_child_at_index(Ref<BTTask> p_child, int p_idx) {
ERR_FAIL_COND_MSG(p_child.ptr()->get_parent().is_valid(), "p_child already has a parent!"); ERR_FAIL_COND_MSG(p_child->get_parent().is_valid(), "p_child already has a parent!");
if (p_idx < 0 || p_idx > _children.size()) { if (p_idx < 0 || p_idx > _children.size()) {
p_idx = _children.size(); p_idx = _children.size();
} }
_children.insert(p_idx, p_child); _children.insert(p_idx, p_child);
p_child->_parent = Ref<BTTask>(this); p_child->_parent = this;
emit_changed(); emit_changed();
} }
@ -174,7 +174,7 @@ void BTTask::remove_child(Ref<BTTask> p_child) {
ERR_FAIL_MSG("p_child not found!"); ERR_FAIL_MSG("p_child not found!");
} else { } else {
_children.remove(idx); _children.remove(idx);
p_child->_parent.unref(); p_child->_parent = nullptr;
emit_changed(); emit_changed();
} }
} }
@ -188,7 +188,7 @@ int BTTask::get_child_index(const Ref<BTTask> &p_child) const {
} }
Ref<BTTask> BTTask::next_sibling() const { Ref<BTTask> BTTask::next_sibling() const {
if (_parent.is_valid()) { if (_parent != nullptr) {
int idx = _parent->get_child_index(Ref<BTTask>(this)); int idx = _parent->get_child_index(Ref<BTTask>(this));
if (idx != -1 && _parent->get_child_count() > (idx + 1)) { if (idx != -1 && _parent->get_child_count() > (idx + 1)) {
return _parent->get_child(idx + 1); return _parent->get_child(idx + 1);
@ -276,7 +276,16 @@ void BTTask::_bind_methods() {
BTTask::BTTask() { BTTask::BTTask() {
_custom_name = String(); _custom_name = String();
_agent = nullptr; _agent = nullptr;
_parent = nullptr;
_blackboard = Dictionary(); _blackboard = Dictionary();
_children = Vector<Ref<BTTask>>(); _children = Vector<Ref<BTTask>>();
_status = FRESH; _status = FRESH;
} }
BTTask::~BTTask() {
for (int i = 0; i < get_child_count(); i++) {
ERR_FAIL_COND(!get_child(i).is_valid());
get_child(i)->_parent = nullptr;
get_child(i).unref();
}
}

View File

@ -10,6 +10,7 @@
#include "core/ustring.h" #include "core/ustring.h"
#include "core/vector.h" #include "core/vector.h"
#include "scene/resources/texture.h" #include "scene/resources/texture.h"
#include <cstddef>
class BTTask : public Resource { class BTTask : public Resource {
GDCLASS(BTTask, Resource); GDCLASS(BTTask, Resource);
@ -21,18 +22,14 @@ public:
FAILURE, FAILURE,
SUCCESS, SUCCESS,
}; };
enum TaskType {
ACTION,
CONDITION,
COMPOSITE,
DECORATOR,
};
private: private:
friend class BehaviorTree;
String _custom_name; String _custom_name;
Object *_agent; Object *_agent;
Dictionary _blackboard; Dictionary _blackboard;
Ref<BTTask> _parent; BTTask *_parent;
Vector<Ref<BTTask>> _children; Vector<Ref<BTTask>> _children;
int _status; int _status;
@ -51,8 +48,8 @@ protected:
public: public:
Object *get_agent() const { return _agent; }; Object *get_agent() const { return _agent; };
Dictionary get_blackboard() const { return _blackboard; }; Dictionary get_blackboard() const { return _blackboard; };
Ref<BTTask> get_parent() const { return _parent; }; Ref<BTTask> get_parent() const { return Ref<BTTask>(_parent); };
bool is_root() const { return _parent.is_null(); }; bool is_root() const { return _parent == nullptr; };
Ref<BTTask> get_root() const; Ref<BTTask> get_root() const;
int get_status() const { return _status; }; int get_status() const { return _status; };
String get_custom_name() const { return _custom_name; }; String get_custom_name() const { return _custom_name; };
@ -76,6 +73,7 @@ public:
void print_tree(int p_initial_tabs = 0) const; void print_tree(int p_initial_tabs = 0) const;
BTTask(); BTTask();
~BTTask();
}; };
#endif // BTTASK_H #endif // BTTASK_H

View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 16 16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#cea4f1"><path d="m0 12h4v4h-4z"/><path d="m6 12h4v4h-4z"/><path d="m5 0h6v4h-6z"/><path d="m7.5 2.5h1v10h-1z"/><path d="m12 12h4v4h-4z"/><path d="m14.5 14h-1v-5.5h-11v3.86h-1v-4.86h13z"/></g></svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 16 16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m16 10.03v-4h-6v1.5h-6.5v-3.53h2.5v-4h-6v4h2.5v10.5h.17.83 6.5v1.5h6v-4h-6v1.5h-6.5v-4.97h6.5v1.5z" fill="#cea4f1"/></svg>

After

Width:  |  Height:  |  Size: 225 B

View File

@ -1,6 +1,7 @@
/* limbo_string_names.cpp */ /* limbo_string_names.cpp */
#include "limbo_string_names.h" #include "limbo_string_names.h"
#include "core/string_name.h"
LimboStringNames *LimboStringNames::singleton = nullptr; LimboStringNames *LimboStringNames::singleton = nullptr;
@ -10,4 +11,5 @@ LimboStringNames::LimboStringNames() {
_enter = StaticCString::create("_enter"); _enter = StaticCString::create("_enter");
_exit = StaticCString::create("_exit"); _exit = StaticCString::create("_exit");
_tick = StaticCString::create("_tick"); _tick = StaticCString::create("_tick");
behavior_tree_finished = StaticCString::create("behavior_tree_finished");
} }

View File

@ -28,6 +28,7 @@ public:
StringName _enter; StringName _enter;
StringName _exit; StringName _exit;
StringName _tick; StringName _tick;
StringName behavior_tree_finished;
}; };
#endif // LIMBO_STRING_NAMES_H #endif // LIMBO_STRING_NAMES_H

View File

@ -4,6 +4,7 @@
#include "core/class_db.h" #include "core/class_db.h"
#include "bt/behavior_tree.h"
#include "bt/bt_action.h" #include "bt/bt_action.h"
#include "bt/bt_always_fail.h" #include "bt/bt_always_fail.h"
#include "bt/bt_always_succeed.h" #include "bt/bt_always_succeed.h"
@ -17,6 +18,7 @@
#include "bt/bt_fail.h" #include "bt/bt_fail.h"
#include "bt/bt_invert.h" #include "bt/bt_invert.h"
#include "bt/bt_parallel.h" #include "bt/bt_parallel.h"
#include "bt/bt_player.h"
#include "bt/bt_probability.h" #include "bt/bt_probability.h"
#include "bt/bt_random_selector.h" #include "bt/bt_random_selector.h"
#include "bt/bt_random_sequence.h" #include "bt/bt_random_sequence.h"
@ -36,6 +38,8 @@
void register_limboai_types() { void register_limboai_types() {
ClassDB::register_class<BTTask>(); ClassDB::register_class<BTTask>();
ClassDB::register_class<BehaviorTree>();
ClassDB::register_class<BTPlayer>();
ClassDB::register_class<BTComposite>(); ClassDB::register_class<BTComposite>();
ClassDB::register_class<BTDecorator>(); ClassDB::register_class<BTDecorator>();

19
test/Agent.gd Normal file
View File

@ -0,0 +1,19 @@
extends KinematicBody2D
onready var bt_player: BTPlayer = $BTPlayer
func _ready() -> void:
_configure_ai()
func _configure_ai() -> void:
var tree := BehaviorTree.new()
var seq := BTSequence.new()
var print_task := BTPrintLine.new("Hello world!")
seq.add_child(print_task)
tree.root_task = seq
bt_player.behavior_tree = tree
print("Assigning tree second time")
bt_player.behavior_tree = tree

9
test/Agent.tscn Normal file
View File

@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://Agent.gd" type="Script" id=2]
[node name="Agent" type="KinematicBody2D"]
script = ExtResource( 2 )
[node name="BTPlayer" type="BTPlayer" parent="."]
active = true

7
test/Test.tscn Normal file
View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://Agent.tscn" type="PackedScene" id=1]
[node name="Test" type="Node2D"]
[node name="Agent" parent="." instance=ExtResource( 1 )]

View File

@ -0,0 +1,14 @@
class_name BTPrintLine
extends BTTask
export var line: String
func _init(p_line: String = "") -> void:
line = p_line
func _tick(_delta: float) -> int:
print(line)
return SUCCESS

6
test/ai/trees/test.tres Normal file
View File

@ -0,0 +1,6 @@
[gd_resource type="BehaviorTree" load_steps=2 format=2]
[sub_resource type="BTSequence" id=1]
[resource]
root_task = SubResource( 1 )

7
test/default_env.tres Normal file
View File

@ -0,0 +1,7 @@
[gd_resource type="Environment" load_steps=2 format=2]
[sub_resource type="ProceduralSky" id=1]
[resource]
background_mode = 2
background_sky = SubResource( 1 )

BIN
test/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

35
test/icon.png.import Normal file
View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.png"
dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0

37
test/project.godot Normal file
View File

@ -0,0 +1,37 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=4
_global_script_classes=[ {
"base": "BTTask",
"class": "BTPrintLine",
"language": "GDScript",
"path": "res://ai/tasks/BTPrintLine.gd"
} ]
_global_script_class_icons={
"BTPrintLine": ""
}
[application]
config/name="LimboAI Test"
run/main_scene="res://Test.tscn"
config/icon="res://icon.png"
[gui]
common/drop_mouse_on_gui_input_disabled=true
[physics]
common/enable_pause_aware_picking=true
[rendering]
environment/default_environment="res://default_env.tres"