├── icon.png ├── addons └── GDBehavior │ ├── icon.png │ ├── plugin.cfg │ ├── Decorators.gd │ ├── Decorator │ ├── Succeeder.gd │ ├── Invert.gd │ └── MultiCondition.gd │ ├── bt_plugin.gd │ ├── Base │ ├── BTAction.gd │ ├── BTComposite.gd │ ├── BTDecorator.gd │ ├── BTCondition.gd │ └── BTNode.gd │ ├── Composite │ ├── Sequencer.gd │ ├── Selector.gd │ ├── SelectorMemory.gd │ ├── SequencerMemory.gd │ └── Parallel.gd │ ├── Composites.gd │ ├── TreeRunner.gd │ ├── Tick.gd │ ├── icon.png.import │ ├── GDBehavior.gd │ ├── SaveLoad.gd │ └── Utils.gd ├── default_env.tres ├── examples ├── SaveLoad │ ├── Actions │ │ ├── ConditionRandom.gd │ │ ├── StopSpeaking.gd │ │ ├── SayRandom.gd │ │ ├── Print.gd │ │ ├── AlwaysCondition.gd │ │ ├── WaitDelta.gd │ │ └── GotoRandom.gd │ ├── Actor.gd │ ├── Node2D.tscn │ ├── Actor.tscn │ └── SaveLoadExample.gd ├── MainFeatures │ ├── Actor.gd │ ├── Actor.tscn │ ├── Node2D.tscn │ └── MainFeaturesExample.gd └── print_hello_world.gd ├── .gitignore ├── project.godot ├── icon.png.import ├── LICENSE └── README.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Dan/gd-behavior/HEAD/icon.png -------------------------------------------------------------------------------- /addons/GDBehavior/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Dan/gd-behavior/HEAD/addons/GDBehavior/icon.png -------------------------------------------------------------------------------- /addons/GDBehavior/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="GDBehavior" 4 | description="An addon for writing behavior trees in GDScript." 5 | author="D Clarke" 6 | version="0.0.1" 7 | script="bt_plugin.gd" -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /examples/SaveLoad/Actions/ConditionRandom.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTCondition.gd" 2 | 3 | # random 4 | 5 | func _init().("random-condition"): 6 | pass 7 | 8 | func _validate(tick): 9 | return randf() > 0.5 -------------------------------------------------------------------------------- /addons/GDBehavior/Decorators.gd: -------------------------------------------------------------------------------- 1 | const Succeeder = preload("res://addons/GDBehavior/Decorator/Succeeder.gd") 2 | const Invert = preload("res://addons/GDBehavior/Decorator/Invert.gd") 3 | const MultiCondition = preload("res://addons/GDBehavior/Decorator/MultiCondition.gd") 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .#* 3 | *~ 4 | 5 | # Godot-specific ignores 6 | .import/ 7 | addons/GDBehavior/Composite/ParallelSimple.gd 8 | addons/GDBehavior/Decorator/MeetsConditionsCount.gd 9 | addons/GDBehavior/TickLogging.gd 10 | examples/logger 11 | # default_env.tres 12 | tests/ -------------------------------------------------------------------------------- /addons/GDBehavior/Decorator/Succeeder.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTDecorator.gd" 2 | 3 | func _init(child=null).(child, "succeeder decorator"): 4 | pass 5 | 6 | func _exe(tick): 7 | var r = _get_child().exe(tick) 8 | if r != RUNNING: 9 | return SUCCESS 10 | return RUNNING -------------------------------------------------------------------------------- /addons/GDBehavior/bt_plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | const LIB_NAME = 'GDBehavior' 5 | 6 | func _enter_tree(): 7 | self.add_autoload_singleton(LIB_NAME, "res://addons/GDBehavior/GDBehavior.gd") 8 | 9 | func _exit_tree(): 10 | self.remove_autoload_singleton(LIB_NAME) 11 | -------------------------------------------------------------------------------- /addons/GDBehavior/Base/BTAction.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTNode.gd" 2 | 3 | # this is no different from BTNode but serves as a way to differentiate 4 | # between actions, decorators, composites and conditionals i.e. if node is BTAction: ... 5 | func _init(name:String).(name): 6 | pass 7 | -------------------------------------------------------------------------------- /addons/GDBehavior/Base/BTComposite.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTNode.gd" 2 | 3 | var children:Array = [] 4 | func _init(children:Array, name:String).(name): 5 | # assert(children.size() > 0) 6 | self.children = children 7 | 8 | func set_children(children): 9 | self.children=children 10 | -------------------------------------------------------------------------------- /addons/GDBehavior/Base/BTDecorator.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTComposite.gd" 2 | 3 | func _init(child, name:String).([child], name): 4 | # assert(child != null) 5 | pass 6 | 7 | func _exe(tick): 8 | return children[0].exe(tick) 9 | 10 | func _get_child(): 11 | return children[0] 12 | -------------------------------------------------------------------------------- /addons/GDBehavior/Base/BTCondition.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTNode.gd" 2 | 3 | func _init(name:String).(name): 4 | pass 5 | 6 | func _exe(tick): 7 | if _validate(tick): 8 | return SUCCESS 9 | return FAILURE 10 | 11 | # override this function 12 | func _validate(tick): 13 | return true 14 | -------------------------------------------------------------------------------- /examples/SaveLoad/Actions/StopSpeaking.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTAction.gd" 2 | 3 | func _init().("stop speaking"): 4 | pass 5 | 6 | func _exe(tick): 7 | tick.actor.speech = "" 8 | return SUCCESS 9 | 10 | 11 | func to_dict(): 12 | return {} 13 | 14 | func from_dict(dict): 15 | return self -------------------------------------------------------------------------------- /addons/GDBehavior/Composite/Sequencer.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTComposite.gd" 2 | 3 | func _init(children:Array=[]).(children, "sequencer"): 4 | pass 5 | 6 | func _exe(tick): 7 | var result = SUCCESS 8 | for c in children: 9 | result = c.exe(tick) 10 | if result != SUCCESS: 11 | break 12 | return result -------------------------------------------------------------------------------- /addons/GDBehavior/Composite/Selector.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTComposite.gd" 2 | 3 | func _init(children:Array=[]).(children, "selector"): 4 | pass 5 | 6 | func _exe(tick): 7 | var result = FAILURE 8 | for c in children: 9 | result = c.exe(tick) 10 | if result != FAILURE: 11 | break 12 | return result 13 | -------------------------------------------------------------------------------- /addons/GDBehavior/Decorator/Invert.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTDecorator.gd" 2 | 3 | func _init(child=null).(child, "invert decorator"): 4 | pass 5 | 6 | func _exe(tick): 7 | var r = _get_child().exe(tick) 8 | if r == SUCCESS: 9 | return FAILURE 10 | elif r == FAILURE: 11 | return SUCCESS 12 | return RUNNING 13 | -------------------------------------------------------------------------------- /examples/SaveLoad/Actions/SayRandom.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTAction.gd" 2 | 3 | func _init().("say random greeting"): 4 | pass 5 | 6 | func _exe(tick): 7 | var gr = tick.actor.greetings 8 | var i = randi() % gr.size() 9 | tick.actor.speech = gr[i] 10 | return SUCCESS 11 | 12 | func to_dict(): 13 | return {} 14 | 15 | func from_dict(dict): 16 | return self -------------------------------------------------------------------------------- /examples/SaveLoad/Actions/Print.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTAction.gd" 2 | 3 | var msg: String 4 | 5 | func _init(msg:String="").("print"): 6 | self.msg = msg 7 | 8 | func _exe(tick): 9 | print("%s: \"%s\"" % [tick.actor, msg]) 10 | return SUCCESS 11 | 12 | func to_dict(): 13 | return {msg=msg} 14 | 15 | func from_dict(dict): 16 | msg = dict.msg 17 | return self -------------------------------------------------------------------------------- /examples/SaveLoad/Actions/AlwaysCondition.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTCondition.gd" 2 | 3 | # always returns result 4 | 5 | var result: bool 6 | 7 | func _init(result:bool=true).("always-condition"): 8 | self.result = result 9 | 10 | func _validate(tick): 11 | return result 12 | 13 | func to_dict(): 14 | return {result=result} 15 | 16 | func from_dict(dict): 17 | result = dict.result 18 | return self -------------------------------------------------------------------------------- /addons/GDBehavior/Composites.gd: -------------------------------------------------------------------------------- 1 | const Parallel = preload("res://addons/GDBehavior/Composite/Parallel.gd") 2 | const Sequencer = preload("res://addons/GDBehavior/Composite/Sequencer.gd") 3 | const SequencerMem = preload("res://addons/GDBehavior/Composite/SequencerMemory.gd") 4 | const Selector = preload("res://addons/GDBehavior/Composite/Selector.gd") 5 | const SelectorMem = preload("res://addons/GDBehavior/Composite/SelectorMemory.gd") 6 | -------------------------------------------------------------------------------- /examples/SaveLoad/Actor.gd: -------------------------------------------------------------------------------- 1 | extends Sprite 2 | 3 | var target:Vector2 4 | 5 | var speech: String setget _set_speech 6 | onready var label = $PanelContainer/Label 7 | 8 | export (Array, String) var greetings = [ 9 | "Hello", "Good Day", "Yoyo-yo", "Wassuuuup", 10 | "Aight", "Howdy", "EZ", "Hola", "Hey"] 11 | 12 | func _set_speech(val): 13 | if val == "": 14 | $PanelContainer.hide() 15 | else: 16 | $PanelContainer.show() 17 | label.text = str(val) 18 | speech = val 19 | -------------------------------------------------------------------------------- /addons/GDBehavior/TreeRunner.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | 3 | #const Tick = preload("res://addons/GDBehavior/Tick.gd") 4 | const BTComposite = preload("res://addons/GDBehavior/Base/BTComposite.gd") 5 | const BTNode = preload("res://addons/GDBehavior/Base/BTNode.gd") 6 | 7 | var root: BTComposite 8 | 9 | func _init(root:BTComposite): 10 | self.root = root 11 | 12 | func exe(tick): 13 | tick.enter_tree(root) 14 | var result = root.exe(tick) 15 | tick.exit_tree(root, result) 16 | return result 17 | -------------------------------------------------------------------------------- /addons/GDBehavior/Composite/SelectorMemory.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTComposite.gd" 2 | 3 | func _init(children:Array=[]).(children, "selector-memory"): 4 | pass 5 | 6 | func _open(tick): 7 | tick.running[self] = 0 8 | 9 | func _exe(tick): 10 | var result = FAILURE 11 | var idx = tick.running[self] 12 | 13 | for i in range(idx, children.size(), 1): 14 | tick.running[self] = i 15 | var next = children[i] 16 | result = next.exe(tick) 17 | if result != FAILURE: 18 | break 19 | return result -------------------------------------------------------------------------------- /addons/GDBehavior/Composite/SequencerMemory.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTComposite.gd" 2 | 3 | func _init(children:Array=[]).(children, "sequencer-memory"): 4 | pass 5 | 6 | func _open(tick): 7 | tick.running[self] = 0 8 | 9 | func _exe(tick): 10 | var result = SUCCESS 11 | var idx = tick.running[self] 12 | 13 | for i in range(idx, children.size(), 1): 14 | tick.running[self] = i 15 | var next = children[i] 16 | result = next.exe(tick) 17 | if result != SUCCESS: 18 | break 19 | return result -------------------------------------------------------------------------------- /examples/SaveLoad/Node2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://examples/SaveLoad/SaveLoadExample.gd" type="Script" id=1] 4 | [ext_resource path="res://examples/SaveLoad/Actor.tscn" type="PackedScene" id=2] 5 | 6 | [node name="Node2D" type="Node2D"] 7 | script = ExtResource( 1 ) 8 | 9 | [node name="Actors" type="Node2D" parent="."] 10 | 11 | [node name="Sprite" parent="Actors" instance=ExtResource( 2 )] 12 | position = Vector2( 351, 297 ) 13 | greetings = [ "Saving?", "Loading?", "Moving...", "I'm over here!" ] 14 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="GDBehavior" 19 | config/icon="res://icon.png" 20 | 21 | [rendering] 22 | 23 | environment/default_environment="res://default_env.tres" 24 | -------------------------------------------------------------------------------- /examples/SaveLoad/Actions/WaitDelta.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTAction.gd" 2 | 3 | var duration: float 4 | 5 | func _init(duration_secs:float=1.0).("wait_delta_time"): 6 | self.duration = duration_secs 7 | 8 | func _open(tick): 9 | tick.time_waited[self] = 0.0 10 | 11 | func _exe(tick): 12 | tick.time_waited[self] += tick.delta 13 | if tick.time_waited[self] > duration: 14 | return SUCCESS 15 | return RUNNING 16 | 17 | func to_dict(): 18 | return {duration=duration} 19 | 20 | func from_dict(dict): 21 | duration = float(dict.duration) 22 | return self -------------------------------------------------------------------------------- /addons/GDBehavior/Tick.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | 3 | const BTNode = preload("res://addons/GDBehavior/Base/BTNode.gd") 4 | 5 | var running = {} 6 | 7 | func enter_tree(root): 8 | pass 9 | 10 | func exit_tree(root, result): 11 | if result != BTNode.RUNNING: 12 | exit_running() 13 | 14 | func exe(node): 15 | pass 16 | 17 | func on_result(node, result): 18 | pass 19 | 20 | func enter(node): 21 | pass 22 | 23 | func exit(node): 24 | pass 25 | 26 | func open(node): 27 | running[node] = -1 28 | 29 | func close(node): 30 | running.erase(node) 31 | 32 | func exit_running(): 33 | for n in running: 34 | n._exit(self) 35 | running = {} 36 | -------------------------------------------------------------------------------- /addons/GDBehavior/Decorator/MultiCondition.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTDecorator.gd" 2 | 3 | # NOTE: Due to simplicity of saving system. conditions are saved as unexecuted children. 4 | func _init(conditions:Array=[], child=null).(child, "multi-condition-decorator"): 5 | children += conditions 6 | pass 7 | 8 | func _exe(tick): 9 | var passing = 0 10 | var run_child = false 11 | for i in range(1, children.size()): 12 | var c = children[i] 13 | if not c.exe(tick) == SUCCESS: 14 | return FAILURE 15 | 16 | var r = _get_child().exe(tick) 17 | if r != RUNNING: 18 | return SUCCESS 19 | return RUNNING 20 | 21 | # func to_dict(): 22 | # return 23 | -------------------------------------------------------------------------------- /examples/SaveLoad/Actor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://icon.png" type="Texture" id=1] 4 | [ext_resource path="res://examples/SaveLoad/Actor.gd" type="Script" id=2] 5 | 6 | [node name="Sprite" type="Sprite"] 7 | z_index = -1 8 | texture = ExtResource( 1 ) 9 | script = ExtResource( 2 ) 10 | 11 | [node name="PanelContainer" type="PanelContainer" parent="."] 12 | visible = false 13 | margin_left = 4.0 14 | margin_top = -63.0 15 | margin_right = 18.0 16 | margin_bottom = -35.0 17 | 18 | [node name="Label" type="Label" parent="PanelContainer"] 19 | margin_left = 7.0 20 | margin_top = 7.0 21 | margin_right = 7.0 22 | margin_bottom = 21.0 23 | -------------------------------------------------------------------------------- /examples/MainFeatures/Actor.gd: -------------------------------------------------------------------------------- 1 | extends Sprite 2 | 3 | var target:Vector2 4 | 5 | var speech: String setget _set_speech 6 | onready var label = $PanelContainer/Label 7 | 8 | export var actor_name := "" setget _set_name, _get_name 9 | export (Array, String) var greetings = [ 10 | "Hello", "Good Day", "Yoyo-yo", "Wassuuuup", 11 | "Aight", "Howdy", "EZ", "Hola", "Hey"] 12 | 13 | func _ready(): 14 | $NamePanel/Label.text = _get_name() 15 | 16 | func _set_speech(val): 17 | if val == "": 18 | $PanelContainer.hide() 19 | else: 20 | $PanelContainer.show() 21 | label.text = str(val) 22 | speech = val 23 | 24 | func _set_name(s): 25 | actor_name = s 26 | $NamePanel/Label.text = s 27 | 28 | func _get_name() -> String: 29 | if actor_name.empty(): 30 | return name 31 | return actor_name 32 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=false 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/GDBehavior/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-be1b7949b36aead098360a3858e91695.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/GDBehavior/icon.png" 13 | dest_files=[ "res://.import/icon.png-be1b7949b36aead098360a3858e91695.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /addons/GDBehavior/GDBehavior.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const BTNode = preload("res://addons/GDBehavior/Base/BTNode.gd") 4 | const BTDecorator = preload("res://addons/GDBehavior/Base/BTDecorator.gd") 5 | const BTComposite = preload("res://addons/GDBehavior/Base/BTComposite.gd") 6 | 7 | const TreeRunner = preload("res://addons/GDBehavior/TreeRunner.gd") 8 | 9 | const Tick = preload("res://addons/GDBehavior/Tick.gd") 10 | 11 | const Composites = preload("res://addons/GDBehavior/Composites.gd") 12 | const Decorators = preload("res://addons/GDBehavior/Decorators.gd") 13 | 14 | const Seq = Composites.Sequencer 15 | const SeqMem = Composites.SequencerMem 16 | const Sel = Composites.Selector 17 | const SelMem = Composites.SelectorMem 18 | const Parallel = Composites.Parallel 19 | 20 | const Succeeder = Decorators.Succeeder 21 | const Invert = Decorators.Invert 22 | const MultiCondition = Decorators.MultiCondition 23 | -------------------------------------------------------------------------------- /examples/SaveLoad/Actions/GotoRandom.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTAction.gd" 2 | 3 | var area: Vector2 4 | var spd: float 5 | 6 | func _init(area:Vector2=Vector2.ZERO, spd=40.0).("goto_pos"): 7 | self.area = area 8 | self.spd = spd 9 | 10 | func _open(tick): 11 | tick.actor.target = random_pos() 12 | 13 | func _exe(tick): 14 | var acc = tick.delta * spd 15 | var d = tick.actor.target - tick.actor.position 16 | if d.length() < acc: 17 | tick.actor.position = tick.actor.target 18 | return SUCCESS 19 | else: 20 | var vel = d.normalized() * acc 21 | tick.actor.position += vel 22 | return RUNNING 23 | 24 | func random_pos(): 25 | var x = randf() * area.x 26 | var y = randf() * area.y 27 | return Vector2(x,y) 28 | 29 | func to_dict(): 30 | return {area=var2str(area), spd=spd} 31 | 32 | func from_dict(dict): 33 | area = str2var(dict.area) 34 | spd = dict.spd 35 | return self -------------------------------------------------------------------------------- /addons/GDBehavior/Composite/Parallel.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/GDBehavior/Base/BTComposite.gd" 2 | 3 | # amount of nodes returning SUCCESS required before exit 4 | # fails if n_failed > n_children - success_count 5 | var success_count: int 6 | 7 | func _init(children:Array=[], success_count:int=1).(children, "parallel"): 8 | self.success_count = min(success_count, children.size()) 9 | 10 | func _exe(tick): 11 | var succ = 0 12 | var fail = 0 13 | 14 | for c in children: 15 | var result = c.exe(tick) 16 | if result == SUCCESS: 17 | succ += 1 18 | elif result == FAILURE: 19 | fail += 1 20 | 21 | var result = RUNNING 22 | if succ >= success_count: 23 | result = SUCCESS 24 | elif fail > children.size() - success_count: 25 | result = FAILURE 26 | return result 27 | return result 28 | 29 | func to_dict(): 30 | return {success_count=success_count} 31 | 32 | func from_dict(dict): 33 | success_count = dict.success_count 34 | return self -------------------------------------------------------------------------------- /addons/GDBehavior/SaveLoad.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | 3 | """ 4 | 5 | """ 6 | 7 | const Utils = preload("res://addons/GDBehavior/Utils.gd") 8 | 9 | static func save_tree(root, filename:String): 10 | var data = Utils.to_data(root) 11 | var save_game = File.new() 12 | assert(filename != "") 13 | save_game.open(filename, File.WRITE) 14 | save_game.store_line(to_json(data)) 15 | save_game.close() 16 | if save_game.file_exists(filename): 17 | return true 18 | return false 19 | 20 | static func load_tree(filename): 21 | var save_game = File.new() 22 | if not save_game.file_exists(filename): 23 | return [] # Error! We don't have a save to load. 24 | 25 | save_game.open(filename, File.READ) 26 | var data = parse_json(save_game.get_line()) 27 | save_game.close() 28 | return Utils.from_data(data) 29 | 30 | 31 | static func delete_file(filename:String): 32 | var dir = Directory.new() 33 | if dir.file_exists(filename): 34 | dir.remove(filename) 35 | return true 36 | return false 37 | 38 | -------------------------------------------------------------------------------- /examples/print_hello_world.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorScript 3 | 4 | """ 5 | Simple example 6 | 7 | Usage: 8 | File > Run 9 | Check 'Output' panel 10 | """ 11 | 12 | const GDB = preload("res://addons/GDBehavior/GDBehavior.gd") 13 | const Seq = GDB.Composites.Sequencer 14 | 15 | class Print: 16 | extends "res://addons/GDBehavior/Base/BTAction.gd" 17 | var msg 18 | 19 | func _init(msg).("print"): 20 | self.msg = msg 21 | 22 | func _exe(tick): 23 | print(msg) 24 | # !!! don't forget to return a result !!! 25 | return SUCCESS 26 | 27 | 28 | var tick = GDB.Tick.new() 29 | 30 | func print_hello_world(): 31 | var root = Seq.new([Print.new("Hello"), Print.new("World!")]) 32 | var tree_runner = GDB.TreeRunner.new(root) 33 | 34 | # prints "Hello", "World!"; result = BTNode.SUCCESS 35 | var result = tree_runner.exe(tick) 36 | print(result == GDB.BTNode.SUCCESS) 37 | # root.exe(tick) # would also work as no running memory is required in tree 38 | 39 | func _run(): 40 | print_hello_world() 41 | -------------------------------------------------------------------------------- /examples/MainFeatures/Actor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://icon.png" type="Texture" id=1] 4 | [ext_resource path="res://examples/MainFeatures/Actor.gd" type="Script" id=2] 5 | 6 | [node name="Sprite" type="Sprite"] 7 | z_index = -1 8 | texture = ExtResource( 1 ) 9 | script = ExtResource( 2 ) 10 | 11 | [node name="PanelContainer" type="PanelContainer" parent="."] 12 | visible = false 13 | margin_left = 4.0 14 | margin_top = -63.0 15 | margin_right = 18.0 16 | margin_bottom = -35.0 17 | 18 | [node name="Label" type="Label" parent="PanelContainer"] 19 | margin_left = 7.0 20 | margin_top = 7.0 21 | margin_right = 7.0 22 | margin_bottom = 21.0 23 | 24 | [node name="NamePanel" type="PanelContainer" parent="."] 25 | modulate = Color( 1, 1, 1, 0.576471 ) 26 | margin_left = -38.0 27 | margin_top = 14.0 28 | margin_right = -24.0 29 | margin_bottom = 42.0 30 | __meta__ = { 31 | "_edit_use_anchors_": false 32 | } 33 | 34 | [node name="Label" type="Label" parent="NamePanel"] 35 | margin_left = 7.0 36 | margin_top = 7.0 37 | margin_right = 7.0 38 | margin_bottom = 21.0 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dan Clarke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/GDBehavior/Base/BTNode.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | 3 | const FAILURE = -1 4 | const SUCCESS = 1 5 | const RUNNING = 2 6 | 7 | var name:String 8 | func _init(name:String): 9 | assert(not name.empty()) 10 | self.name = name 11 | 12 | func exe(tick): 13 | tick.enter(self) 14 | _enter(tick) 15 | 16 | if not self in tick.running: 17 | tick.open(self) 18 | _open(tick) 19 | 20 | tick.exe(self) 21 | var result = _exe(tick) 22 | assert(result != null) # Did you remember to return a result?.. 23 | tick.on_result(self, result) 24 | 25 | if _should_close(result): 26 | tick.close(self) 27 | _close(tick) 28 | 29 | _exit(tick) 30 | tick.exit(self) 31 | 32 | return result 33 | 34 | # ======================================== 35 | # Extend these 36 | 37 | func _exe(tick): 38 | return SUCCESS 39 | 40 | func _enter(tick): 41 | pass 42 | 43 | func _exit(tick): 44 | pass 45 | 46 | func _open(tick): 47 | pass 48 | 49 | func _close(tick): 50 | pass 51 | 52 | # ======================================== 53 | 54 | func _should_close(result): 55 | return result != RUNNING 56 | 57 | # ======================================== 58 | 59 | func to_dict(): 60 | return {} 61 | 62 | func from_dict(dict): 63 | return self 64 | -------------------------------------------------------------------------------- /examples/MainFeatures/Node2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://examples/MainFeatures/MainFeaturesExample.gd" type="Script" id=1] 4 | [ext_resource path="res://examples/MainFeatures/Actor.tscn" type="PackedScene" id=2] 5 | 6 | [node name="Node2D" type="Node2D"] 7 | script = ExtResource( 1 ) 8 | 9 | [node name="Actors" type="Node2D" parent="."] 10 | 11 | [node name="Sprite" parent="Actors" instance=ExtResource( 2 )] 12 | 13 | [node name="Sprite2" parent="Actors" instance=ExtResource( 2 )] 14 | position = Vector2( 100, 225 ) 15 | 16 | [node name="Sprite3" parent="Actors" instance=ExtResource( 2 )] 17 | position = Vector2( 223, 71 ) 18 | 19 | [node name="Sprite4" parent="Actors" instance=ExtResource( 2 )] 20 | position = Vector2( 223, 71 ) 21 | 22 | [node name="Sprite5" parent="Actors" instance=ExtResource( 2 )] 23 | position = Vector2( 223, 71 ) 24 | 25 | [node name="Sprite6" parent="Actors" instance=ExtResource( 2 )] 26 | position = Vector2( 223, 71 ) 27 | 28 | [node name="Sprite7" parent="Actors" instance=ExtResource( 2 )] 29 | position = Vector2( 223, 71 ) 30 | 31 | [node name="Sprite8" parent="Actors" instance=ExtResource( 2 )] 32 | position = Vector2( 223, 71 ) 33 | 34 | [node name="Sprite9" parent="Actors" instance=ExtResource( 2 )] 35 | position = Vector2( 223, 71 ) 36 | 37 | [node name="Sprite10" parent="Actors" instance=ExtResource( 2 )] 38 | position = Vector2( 223, 71 ) 39 | -------------------------------------------------------------------------------- /addons/GDBehavior/Utils.gd: -------------------------------------------------------------------------------- 1 | const Composite = preload("res://addons/GDBehavior/Base/BTComposite.gd") 2 | 3 | static func get_nodes_dfs(root:Composite): 4 | var nodes = [root] 5 | for c in root.children: 6 | if c is Composite: 7 | nodes += get_nodes_dfs(c) 8 | else: 9 | nodes.append(c) 10 | 11 | return nodes 12 | 13 | # result.children is the offset from parent to child not absolute position in data 14 | static func get_nodes_dfs_data(root:Composite, depth=0, i=[0]): 15 | var nodes = [{node=root, depth=depth, children=[], index=i[0]}] 16 | depth += 1 17 | var temp_i = i[0] 18 | for c in root.children: 19 | i[0] += 1 20 | nodes[0].children.append(i[0]-temp_i) 21 | if c is Composite: 22 | nodes += get_nodes_dfs_data(c, depth, i) 23 | else: 24 | nodes.append({node=c, depth=depth, index=i[0]}) 25 | return nodes 26 | 27 | # ================================================================ 28 | 29 | static func to_data(root:Composite): 30 | var nodes = get_nodes_dfs_data(root) 31 | var result = [] 32 | var actions = {} 33 | var i = 0 34 | for n in nodes: 35 | var nd = n.node 36 | if not nd.name in actions: 37 | actions[nd.name] = nd.get_script().resource_path 38 | var nd_data = {name=nd.name, id=n.index} 39 | 40 | if nd.has_method("to_dict"): 41 | var args = nd.to_dict() 42 | for k in args: 43 | nd_data[k] = args[k] 44 | 45 | if nd is Composite: 46 | var args = nd.to_dict() 47 | for k in args: 48 | nd_data[k] = args[k] 49 | var children = [] 50 | nd_data["children"] = n.children 51 | i+=1 52 | result.append(nd_data) 53 | 54 | return {tree=result, actions=actions} 55 | 56 | static func from_data(data): 57 | var nodes = [] 58 | var actions = data.actions 59 | for i in range(data.tree.size()-1, -1, -1): 60 | var nd_data = data.tree[i] 61 | var script = load(actions[nd_data.name]) 62 | var node 63 | if "children" in nd_data: 64 | var children = [] 65 | for j in nd_data.children: 66 | children.append(nodes[nodes.size()-j]) 67 | node = script.new().from_dict(nd_data) 68 | node.set_children(children) 69 | else: 70 | node = script.new().from_dict(nd_data) 71 | node.name = nd_data.name 72 | nodes.append(node) 73 | 74 | return nodes.back() 75 | 76 | -------------------------------------------------------------------------------- /examples/SaveLoad/SaveLoadExample.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | """ 4 | An example showing the save system. 5 | Note that nodes need to be defined in seperate files as the script-path needs to be saved. 6 | Also if a file is moved or renamed post-save, loading will fail. 7 | """ 8 | 9 | # ================================================================ 10 | const Tick = preload("res://addons/GDBehavior/Tick.gd") 11 | 12 | class TestTick: 13 | extends Tick 14 | 15 | var time_waited = {} 16 | var delta: float 17 | var actor 18 | 19 | func _init(actor): 20 | self.actor = actor 21 | 22 | # ================================================================ 23 | 24 | const BTRunner = preload("res://addons/GDBehavior/TreeRunner.gd") 25 | 26 | const Composites = preload("res://addons/GDBehavior/Composites.gd") 27 | const SeqMem = Composites.SequencerMem 28 | const Parallel = Composites.Parallel 29 | 30 | const Wait = preload("res://examples/SaveLoad/Actions/WaitDelta.gd") 31 | const GotoRandom = preload("res://examples/SaveLoad/Actions/GotoRandom.gd") 32 | const Speak = preload("res://examples/SaveLoad/Actions/SayRandom.gd") 33 | const StopSpeaking = preload("res://examples/SaveLoad/Actions/StopSpeaking.gd") 34 | 35 | const Utils = preload("res://addons/GDBehavior/Utils.gd") 36 | const SaveLoad = preload("res://addons/GDBehavior/SaveLoad.gd") 37 | 38 | onready var actors = $Actors.get_children() 39 | 40 | var ticks = [] 41 | var tree_runner: BTRunner 42 | 43 | # ================================================================ 44 | 45 | func _ready() -> void: 46 | setup_behavior() 47 | 48 | func _process(delta): 49 | run_behavior(delta) 50 | 51 | # ================================================================ 52 | 53 | func setup_behavior(): 54 | var vp_sz = get_viewport_rect().size 55 | for a in actors: 56 | var tick = TestTick.new(a) 57 | ticks.append(tick) 58 | a.position = Vector2(randf() * vp_sz.x, randf() * vp_sz.y) 59 | 60 | var goto = SeqMem.new([ 61 | GotoRandom.new(vp_sz, 400.0), 62 | Wait.new(1.0)]) 63 | 64 | var speak = SeqMem.new([ 65 | Speak.new(), 66 | Wait.new(2.0), 67 | StopSpeaking.new(), 68 | Wait.new(3.0)]) 69 | 70 | var root = Parallel.new([goto, speak], 2) 71 | 72 | # just so you know it all works... 73 | 74 | # DATA 75 | 76 | # to_data returns a dictionary of tree data and actions 77 | # this is all handled in SaveLoad class and is not a necessary step. 78 | var data_save = Utils.to_data(root) 79 | 80 | print("-ACTIONS-") 81 | print(data_save.actions) 82 | print("-NODE DATA-") 83 | print(data_save.tree) 84 | 85 | # returns the root of a tree generated from 'data_save' 86 | root = Utils.from_data(data_save) 87 | 88 | 89 | # SAVING + LOADING 90 | 91 | var fldr_path = "user://gdbehavior/example/" 92 | var test_filename = fldr_path + "bt_save_test.bt" 93 | 94 | var dir = Directory.new() 95 | if not dir.dir_exists(fldr_path): 96 | dir.make_dir_recursive(fldr_path) 97 | 98 | var saved = SaveLoad.save_tree(root, test_filename) 99 | var root_loaded = SaveLoad.load_tree(test_filename) 100 | 101 | # var destroyed = SaveLoad.delete_file(test_filename) 102 | # dir.remove(fldr_path) 103 | 104 | tree_runner = BTRunner.new(root_loaded) 105 | 106 | 107 | func run_behavior(delta): 108 | for t in ticks: 109 | t.delta = delta 110 | tree_runner.exe(t) 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDBehavior 2 | 3 | An addon for writing behavior trees in GDScript. 4 | Written in Godot 3.1. 5 | 6 | ## Influences 7 | 8 | * https://github.com/godot-addons/godot-behavior-tree-plugin 9 | * [Behavior Trees in Robotics and AI](https://arxiv.org/pdf/1709.00084.pdf) 10 | 11 | ## Philosophy 12 | 13 | * Simple 14 | * Extendable 15 | * Good for: 16 | * learning 17 | * rapid prototyping 18 | * experimentation 19 | 20 | ## Installation 21 | 22 | Download from the Godot Asset Store or clone and place into the 'addons' folder in your project. 23 | 24 | ```gdscript 25 | # Not enabled 26 | const GDB = preload("res://addons/GDBehavior/GDBehavior.gd") 27 | var seq = GDB.Composites.Sequencer.new(children) 28 | 29 | # Enabled from Plugins menu 30 | var seq = GDBehavior.Composites.Sequencer.new(children) 31 | ``` 32 | ## Usage 33 | 34 | [Examples](https://github.com/Dr-Dan/gd-behavior/blob/master/examples) 35 | 36 | Result constants are defined in the BTNode class (SUCCESS, FAILURE, RUNNING). 37 | 38 | Base node types (Action, Conditional, Composite, Decorator) are in GDBehavior/Base folder. 39 | 40 | ### The Tick 41 | 42 | ```gdscript 43 | var tick = Tick.new() 44 | var result = root.exe(tick) 45 | ``` 46 | 47 | The tick passes through each node in the tree in order of execution. By default, it holds a history of all nodes currently in a RUNNING state but it has other uses such as holding a blackboard or logging tree history for debugging. 48 | 49 | ### Leaf Nodes 50 | #### Action 51 | 52 | ```gdscript 53 | class Print: 54 | extends "res://addons/GDBehavior/Base/BTAction.gd" 55 | var msg 56 | 57 | func _init(msg).("print"): 58 | self.msg = msg 59 | 60 | func _exe(tick): 61 | print(msg) 62 | return SUCCESS 63 | 64 | ``` 65 | 66 | Override the _exe function to apply an action's effects and be sure to return a result on completion. 67 | 68 | _open, _closed, _enter, _exit functions are also inherited from BTNode via BTAction. 69 | 70 | #### Condition 71 | ```gdscript 72 | class AlwaysCondition: 73 | extends "res://addons/GDBehavior/Base/BTCondition.gd" 74 | var value:bool 75 | 76 | func _init(value:bool).("always-condition"): 77 | self.value = value 78 | 79 | func _validate(tick): 80 | return value 81 | ``` 82 | 83 | Identical to BTAction except _validate is overriden (and returns a bool) instead of _exe. 84 | This class is optional and will operate the same as an action return SUCCESS/FAILURE in place of true/false. 85 | 86 | ### Composites 87 | 88 | ```gdscript 89 | # execute all 3 actions in order while result==SUCCESS 90 | var wander = SequencerMem.new([ 91 | GotoRandom.new(), 92 | Wait.new(2.0), 93 | LookAround() 94 | ]) 95 | 96 | var stay_safe = Selector.new([ 97 | IsSafe.new(), 98 | # nesting is cool 99 | SequencerMem.new([ 100 | EvadeThreat.new(), 101 | LookAround() 102 | ]) 103 | ]) 104 | ``` 105 | 106 | Composites expect children as an array of BTNodes. 107 | 108 | If you are going to use composites with memory (i.e. SequencerMem) then: 109 | * Call Tick.exit_tree(root, result) after each pass of the tree 110 | * Or use TreeRunner.exe(tick) which will do so automatically 111 | 112 | ### Decorators 113 | ```gdscript 114 | # SUCCESS = FAILURE and vice versa 115 | var is_not_speaking = Invert.new(IsSpeaking.new()) 116 | 117 | MultiCondition.new( 118 | # if all conditions return SUCCESS 119 | [is_not_speaking, ActorInRange.new(80.0)], 120 | # do this. (can be any type of node.) 121 | SayRandom.new()) 122 | ``` 123 | 124 | Have a single child. Some example uses are guarding execution, modifying a result from the child and repeating execution of children. 125 | 126 | ### Saving and Loading 127 | ```gdscript 128 | const SaveLoad = preload("res://addons/GDBehavior/SaveLoad.gd") 129 | var file_path = "user://gdbehavior/example/bt_save_test.bt" 130 | 131 | var saved = SaveLoad.save_tree(root_node, file_path) 132 | 133 | # returns root of loaded tree 134 | var root_loaded = SaveLoad.load_tree(file_path) 135 | ``` 136 | 137 | [Saving and Loading Example](https://github.com/Dr-Dan/gd-behavior/blob/master/examples/SaveLoad/SaveLoadExample.gd) 138 | 139 | The save and load implementation is in early stages and will likely change in the near future. -------------------------------------------------------------------------------- /examples/MainFeatures/MainFeaturesExample.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | """ 4 | An example where actors walk around and say something if within range. 5 | """ 6 | 7 | # ================================================================ 8 | # The tick 9 | # passed to each node as they execute 10 | # handles the running memory and closing hanging nodes 11 | # also used to pass data into and between nodes (as shown below) 12 | const Tick = preload("res://addons/GDBehavior/Tick.gd") 13 | 14 | class TestTick: 15 | extends Tick 16 | 17 | # time_waited could also be an actor variable 18 | # inversely all actor data could reside in the tick 19 | var time_waited = {} 20 | var delta: float 21 | var actor 22 | var actors = [] 23 | var say_target 24 | 25 | func _init(actor, actors=[]): 26 | self.actor = actor 27 | self.actors = actors 28 | 29 | # ================================================================ 30 | # CUSTOM NODES 31 | const BTAction = preload("res://addons/GDBehavior/Base/BTAction.gd") 32 | 33 | # Note: using time_waited[self] would overwrite if using an object multiple times in the tree. 34 | # It would have the same effect on the running list 35 | # this is fine: Parallel.new([WaitDelta.new(2), WaitDelta.new(2)], 2) 36 | # this is not: var w = WaitDelta.new(2) ... Parallel.new([w,w], 2) 37 | 38 | # Classes extending BTAction should override functions beginning with _ i.e. _exe, _open 39 | 40 | class WaitDelta: 41 | extends BTAction 42 | var duration: float 43 | 44 | func _init(duration_secs:float).("wait_delta_time"): 45 | self.duration = duration_secs 46 | 47 | func _open(tick): 48 | tick.time_waited[self] = 0.0 49 | 50 | func _exe(tick): 51 | tick.time_waited[self] += tick.delta 52 | if tick.time_waited[self] > duration: 53 | return SUCCESS 54 | return RUNNING 55 | 56 | class GotoRandom: 57 | extends BTAction 58 | var area: Vector2 59 | var stop_dist: float 60 | var spd: float 61 | 62 | func _init(area:Vector2, spd=40.0, stop_dist=8.0).("goto_pos"): 63 | self.area = area 64 | self.stop_dist = stop_dist 65 | self.spd = spd 66 | 67 | func _open(tick): 68 | tick.actor.target = random_pos() 69 | 70 | func _exe(tick): 71 | var acc = tick.delta * spd 72 | var d = tick.actor.target - tick.actor.position 73 | if d.length() < acc: 74 | tick.actor.position = tick.actor.target 75 | return SUCCESS 76 | else: 77 | var vel = d.normalized() * acc 78 | tick.actor.position += vel 79 | return RUNNING 80 | 81 | 82 | 83 | func random_pos(): 84 | var x = randf() * area.x 85 | var y = randf() * area.y 86 | return Vector2(x,y) 87 | 88 | class ColorRandom: 89 | extends BTAction 90 | 91 | func _init().("color random"): 92 | pass 93 | 94 | func _exe(tick): 95 | tick.actor.self_modulate = random_color() 96 | return SUCCESS 97 | 98 | func random_color(): 99 | return Color(randf(), randf(), randf()) 100 | 101 | class StopSpeaking: 102 | extends BTAction 103 | 104 | func _init().("stop speaking"): 105 | pass 106 | 107 | func _exe(tick): 108 | tick.actor.speech = "" 109 | return SUCCESS 110 | 111 | class SayRandom: 112 | extends BTAction 113 | 114 | func _init().("say random greeting"): 115 | pass 116 | 117 | func _exe(tick): 118 | var gr = tick.actor.greetings 119 | var i = randi() % gr.size() 120 | tick.actor.speech = gr[i] 121 | return SUCCESS 122 | 123 | class SayRandomToStored: 124 | extends BTAction 125 | 126 | func _init().("say random greeting"): 127 | pass 128 | 129 | func _exe(tick): 130 | if tick.say_target != null: 131 | var gr = tick.actor.greetings 132 | var i = randi() % gr.size() 133 | tick.actor.speech = gr[i] + (" %s" % tick.say_target.actor_name) 134 | return SUCCESS 135 | return FAILURE 136 | 137 | 138 | class StoreActorInRange: 139 | extends BTCondition 140 | 141 | var distance: float 142 | 143 | func _init(distance).("store actor in range?"): 144 | self.distance = distance 145 | 146 | func _exe(tick): 147 | for a in tick.actors: 148 | if a == tick.actor: continue 149 | if tick.actor.position.distance_to(a.position) < distance: 150 | tick.say_target = a 151 | return SUCCESS 152 | return FAILURE 153 | 154 | class ClearSayTarget: 155 | extends BTCondition 156 | 157 | func _init().("clear tick var"): 158 | pass 159 | 160 | func _exe(tick): 161 | if tick.say_target != null: 162 | tick.say_target = null 163 | return SUCCESS 164 | return FAILURE 165 | 166 | # ================================================================ 167 | # CONDITIONALS 168 | const BTCondition = preload("res://addons/GDBehavior/Base/BTCondition.gd") 169 | 170 | # work similarly to BTAction except _validate is overriden instead of _exe 171 | # unlike _exe; _validate returns a bool 172 | class AlwaysCondition: 173 | extends BTCondition 174 | var value:bool 175 | 176 | func _init(value:bool).("always return"): 177 | self.value = value 178 | 179 | func _validate(tick): 180 | return value 181 | 182 | class ActorInRange: 183 | extends BTCondition 184 | 185 | var distance: float 186 | 187 | func _init(distance).("actor in range?"): 188 | self.distance = distance 189 | 190 | func _validate(tick): 191 | for a in tick.actors: 192 | if a == tick.actor: continue 193 | if tick.actor.position.distance_to(a.position) < distance: 194 | return true 195 | return false 196 | 197 | class IsSpeaking: 198 | extends BTCondition 199 | 200 | func _init().("is speaking?"): 201 | pass 202 | 203 | func _validate(tick): 204 | if tick.actor.speech != "": 205 | return true 206 | return false 207 | 208 | 209 | # ================================================================ 210 | 211 | func _ready() -> void: 212 | setup_actor_goto() 213 | 214 | func _process(delta): 215 | test_actor_goto(delta) 216 | 217 | # ================================================================ 218 | 219 | const BTRunner = preload("res://addons/GDBehavior/TreeRunner.gd") 220 | const Composites = preload("res://addons/GDBehavior/Composites.gd") 221 | const Decorators = preload("res://addons/GDBehavior/Decorators.gd") 222 | const SeqMem = Composites.SequencerMem 223 | const Parallel = Composites.Parallel 224 | const Succeeder = Decorators.Succeeder 225 | 226 | const MultiCondition = Decorators.MultiCondition 227 | const Invert = Decorators.Invert 228 | 229 | onready var actors = $Actors.get_children() 230 | 231 | var ticks = [] 232 | var tree_runner 233 | 234 | func setup_actor_goto(): 235 | var vp_sz = get_viewport_rect().size 236 | for a in actors: 237 | var tick = TestTick.new(a, actors) 238 | ticks.append(tick) 239 | a.position = Vector2(randf() * vp_sz.x, randf() * vp_sz.y) 240 | 241 | # Setup behaviours 242 | var speak = SeqMem.new([ 243 | # Multi-Condition decorator; not necessary in this case, here for demonstration. 244 | MultiCondition.new( 245 | # if all return SUCCESS 246 | [Invert.new(IsSpeaking.new()), AlwaysCondition.new(true)], 247 | # do this. (can be any type of node) 248 | StoreActorInRange.new(80.0)), 249 | SayRandomToStored.new(), 250 | WaitDelta.new(2.5), 251 | StopSpeaking.new(), 252 | ClearSayTarget.new() 253 | ]) 254 | 255 | var goto = SeqMem.new([ 256 | GotoRandom.new(vp_sz, 120.0), 257 | WaitDelta.new(2.0), 258 | ColorRandom.new() 259 | ]) 260 | 261 | # Succeeder will stop any FAILUREs from interrupting parallel node 262 | # a RUNNING result will still get through however 263 | var root = Parallel.new([Succeeder.new(speak), goto], 2) 264 | tree_runner = BTRunner.new(root) 265 | 266 | # Note: here for convenience. This can also be handled from each actor's _process(delta) function 267 | func test_actor_goto(delta): 268 | for t in ticks: 269 | t.delta = delta 270 | # TreeRunner returns an integer for SUCCESS, FAILURE, RUNNING 271 | tree_runner.exe(t) 272 | --------------------------------------------------------------------------------