diff --git a/limboai/bt/behavior_tree.cpp b/limboai/bt/behavior_tree.cpp new file mode 100644 index 0000000..c89252b --- /dev/null +++ b/limboai/bt/behavior_tree.cpp @@ -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 + +void BehaviorTree::init() { + List 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::clone() const { + Ref 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"); +} \ No newline at end of file diff --git a/limboai/bt/behavior_tree.h b/limboai/bt/behavior_tree.h new file mode 100644 index 0000000..23dffa4 --- /dev/null +++ b/limboai/bt/behavior_tree.h @@ -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 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 &p_value) { + root_task = p_value; + emit_changed(); + } + Ref get_root_task() const { return root_task; } + + void init(); + Ref clone() const; +}; + +#endif // BEHAVIOR_TREE_H \ No newline at end of file diff --git a/limboai/bt/bt_player.cpp b/limboai/bt/bt_player.cpp new file mode 100644 index 0000000..971f891 --- /dev/null +++ b/limboai/bt/bt_player.cpp @@ -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 + +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 &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); +} \ No newline at end of file diff --git a/limboai/bt/bt_player.h b/limboai/bt/bt_player.h new file mode 100644 index 0000000..d17aacb --- /dev/null +++ b/limboai/bt/bt_player.h @@ -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 + +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 behavior_tree; + UpdateMode update_mode = UpdateMode::IDLE; + bool active = false; + bool auto_restart = false; + Dictionary blackboard; + + Ref _loaded_tree; + Ref _root_task; + + void _load_tree(); + +protected: + static void _bind_methods(); + + void _notification(int p_notification); + +public: + void set_behavior_tree(const Ref &p_tree); + Ref 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 \ No newline at end of file diff --git a/limboai/bt/bt_task.cpp b/limboai/bt/bt_task.cpp index 94ca285..3fc7cb7 100644 --- a/limboai/bt/bt_task.cpp +++ b/limboai/bt/bt_task.cpp @@ -2,14 +2,14 @@ #include "bt_task.h" +#include "../limbo_string_names.h" +#include "../limbo_utility.h" #include "core/class_db.h" #include "core/object.h" #include "core/script_language.h" #include "core/variant.h" #include "editor/editor_node.h" - -#include "../limbo_string_names.h" -#include "../limbo_utility.h" +#include String BTTask::_generate_name() const { if (get_script_instance()) { @@ -58,7 +58,7 @@ String BTTask::get_task_name() const { Ref BTTask::get_root() const { const BTTask *task = this; while (!task->is_root()) { - task = task->get_parent().ptr(); + task = task->_parent; } return Ref(task); } @@ -86,11 +86,11 @@ void BTTask::initialize(Object *p_agent, Dictionary p_blackboard) { Ref BTTask::clone() const { Ref inst = duplicate(true); - inst.ptr()->_parent.unref(); - CRASH_COND(inst.ptr()->get_parent().is_valid()); + inst->_parent = nullptr; + CRASH_COND(inst->get_parent().is_valid()); for (int i = 0; i < _children.size(); i++) { Ref c = get_child(i)->clone(); - c->_parent = inst; + c->_parent = inst.ptr(); inst->_children.set(i, c); } return inst; @@ -152,19 +152,19 @@ int BTTask::get_child_count() const { } void BTTask::add_child(Ref p_child) { - ERR_FAIL_COND_MSG(p_child.ptr()->get_parent().is_valid(), "p_child already has a parent!"); - p_child->_parent = Ref(this); + ERR_FAIL_COND_MSG(p_child->get_parent().is_valid(), "p_child already has a parent!"); + p_child->_parent = this; _children.push_back(p_child); emit_changed(); } void BTTask::add_child_at_index(Ref 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()) { p_idx = _children.size(); } _children.insert(p_idx, p_child); - p_child->_parent = Ref(this); + p_child->_parent = this; emit_changed(); } @@ -174,7 +174,7 @@ void BTTask::remove_child(Ref p_child) { ERR_FAIL_MSG("p_child not found!"); } else { _children.remove(idx); - p_child->_parent.unref(); + p_child->_parent = nullptr; emit_changed(); } } @@ -188,7 +188,7 @@ int BTTask::get_child_index(const Ref &p_child) const { } Ref BTTask::next_sibling() const { - if (_parent.is_valid()) { + if (_parent != nullptr) { int idx = _parent->get_child_index(Ref(this)); if (idx != -1 && _parent->get_child_count() > (idx + 1)) { return _parent->get_child(idx + 1); @@ -276,7 +276,16 @@ void BTTask::_bind_methods() { BTTask::BTTask() { _custom_name = String(); _agent = nullptr; + _parent = nullptr; _blackboard = Dictionary(); _children = Vector>(); _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(); + } } \ No newline at end of file diff --git a/limboai/bt/bt_task.h b/limboai/bt/bt_task.h index 368016d..07441a1 100644 --- a/limboai/bt/bt_task.h +++ b/limboai/bt/bt_task.h @@ -10,6 +10,7 @@ #include "core/ustring.h" #include "core/vector.h" #include "scene/resources/texture.h" +#include class BTTask : public Resource { GDCLASS(BTTask, Resource); @@ -21,18 +22,14 @@ public: FAILURE, SUCCESS, }; - enum TaskType { - ACTION, - CONDITION, - COMPOSITE, - DECORATOR, - }; private: + friend class BehaviorTree; + String _custom_name; Object *_agent; Dictionary _blackboard; - Ref _parent; + BTTask *_parent; Vector> _children; int _status; @@ -51,8 +48,8 @@ protected: public: Object *get_agent() const { return _agent; }; Dictionary get_blackboard() const { return _blackboard; }; - Ref get_parent() const { return _parent; }; - bool is_root() const { return _parent.is_null(); }; + Ref get_parent() const { return Ref(_parent); }; + bool is_root() const { return _parent == nullptr; }; Ref get_root() const; int get_status() const { return _status; }; String get_custom_name() const { return _custom_name; }; @@ -76,6 +73,7 @@ public: void print_tree(int p_initial_tabs = 0) const; BTTask(); + ~BTTask(); }; #endif // BTTASK_H \ No newline at end of file diff --git a/limboai/icons/icon_b_t_player.svg b/limboai/icons/icon_b_t_player.svg new file mode 100644 index 0000000..166edff --- /dev/null +++ b/limboai/icons/icon_b_t_player.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/limboai/icons/icon_behavior_tree.svg b/limboai/icons/icon_behavior_tree.svg new file mode 100644 index 0000000..f8cd473 --- /dev/null +++ b/limboai/icons/icon_behavior_tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/limboai/limbo_string_names.cpp b/limboai/limbo_string_names.cpp index f3d6beb..abe8fd1 100644 --- a/limboai/limbo_string_names.cpp +++ b/limboai/limbo_string_names.cpp @@ -1,6 +1,7 @@ /* limbo_string_names.cpp */ #include "limbo_string_names.h" +#include "core/string_name.h" LimboStringNames *LimboStringNames::singleton = nullptr; @@ -10,4 +11,5 @@ LimboStringNames::LimboStringNames() { _enter = StaticCString::create("_enter"); _exit = StaticCString::create("_exit"); _tick = StaticCString::create("_tick"); + behavior_tree_finished = StaticCString::create("behavior_tree_finished"); } \ No newline at end of file diff --git a/limboai/limbo_string_names.h b/limboai/limbo_string_names.h index d29211b..f3ef0f3 100644 --- a/limboai/limbo_string_names.h +++ b/limboai/limbo_string_names.h @@ -28,6 +28,7 @@ public: StringName _enter; StringName _exit; StringName _tick; + StringName behavior_tree_finished; }; #endif // LIMBO_STRING_NAMES_H \ No newline at end of file diff --git a/limboai/register_types.cpp b/limboai/register_types.cpp index 8ec1b90..13f2be5 100644 --- a/limboai/register_types.cpp +++ b/limboai/register_types.cpp @@ -4,6 +4,7 @@ #include "core/class_db.h" +#include "bt/behavior_tree.h" #include "bt/bt_action.h" #include "bt/bt_always_fail.h" #include "bt/bt_always_succeed.h" @@ -17,6 +18,7 @@ #include "bt/bt_fail.h" #include "bt/bt_invert.h" #include "bt/bt_parallel.h" +#include "bt/bt_player.h" #include "bt/bt_probability.h" #include "bt/bt_random_selector.h" #include "bt/bt_random_sequence.h" @@ -36,6 +38,8 @@ void register_limboai_types() { ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); diff --git a/test/Agent.gd b/test/Agent.gd new file mode 100644 index 0000000..f32da4d --- /dev/null +++ b/test/Agent.gd @@ -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 diff --git a/test/Agent.tscn b/test/Agent.tscn new file mode 100644 index 0000000..c67aa37 --- /dev/null +++ b/test/Agent.tscn @@ -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 diff --git a/test/Test.tscn b/test/Test.tscn new file mode 100644 index 0000000..3311850 --- /dev/null +++ b/test/Test.tscn @@ -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 )] diff --git a/test/ai/tasks/BTPrintLine.gd b/test/ai/tasks/BTPrintLine.gd new file mode 100644 index 0000000..e376612 --- /dev/null +++ b/test/ai/tasks/BTPrintLine.gd @@ -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 diff --git a/test/ai/trees/test.tres b/test/ai/trees/test.tres new file mode 100644 index 0000000..174eb38 --- /dev/null +++ b/test/ai/trees/test.tres @@ -0,0 +1,6 @@ +[gd_resource type="BehaviorTree" load_steps=2 format=2] + +[sub_resource type="BTSequence" id=1] + +[resource] +root_task = SubResource( 1 ) diff --git a/test/default_env.tres b/test/default_env.tres new file mode 100644 index 0000000..20207a4 --- /dev/null +++ b/test/default_env.tres @@ -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 ) diff --git a/test/icon.png b/test/icon.png new file mode 100644 index 0000000..c98fbb6 Binary files /dev/null and b/test/icon.png differ diff --git a/test/icon.png.import b/test/icon.png.import new file mode 100644 index 0000000..a4c02e6 --- /dev/null +++ b/test/icon.png.import @@ -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 diff --git a/test/project.godot b/test/project.godot new file mode 100644 index 0000000..2bb9304 --- /dev/null +++ b/test/project.godot @@ -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"