Started working on Godot version
21
Godot/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Peter DV
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
23
Godot/addons/Todo_Manager/CONTRIBUTING.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
## Contributing to TODO Manager
|
||||||
|
Firstly, thank you for being interested in contributing to the Godot TODO Manager plugin!
|
||||||
|
TODO Manager has benefitted greatly from enthusiastic users who have suggested new features, noticed bugs, and contributed code to the plugin.
|
||||||
|
|
||||||
|
### Code Style Guide
|
||||||
|
For the sake of clarity, TODO Manager takes advantage of GDScripts optional static typing in most circumstances.
|
||||||
|
In particular, when declaring variables use colons to infer the type where possible:
|
||||||
|
|
||||||
|
`todo := "#TODO"`
|
||||||
|
|
||||||
|
If the type is not obvious then explicit typing is desirable:
|
||||||
|
|
||||||
|
`items : PoolStringArray = todo.split()`
|
||||||
|
|
||||||
|
Typed arguments and return values for functions are required:
|
||||||
|
```
|
||||||
|
func example(name: String, amount: int) -> Array:
|
||||||
|
# code
|
||||||
|
return array_of_names
|
||||||
|
```
|
||||||
|
|
||||||
|
For more info on static typing in Godot please refer to the documentation.
|
||||||
|
https://docs.godotengine.org/en/stable/getting_started/scripting/gdscript/static_typing.html
|
17
Godot/addons/Todo_Manager/ColourPicker.gd
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@tool
|
||||||
|
extends HBoxContainer
|
||||||
|
|
||||||
|
var colour : Color
|
||||||
|
var title : String:
|
||||||
|
set = set_title
|
||||||
|
var index : int
|
||||||
|
|
||||||
|
@onready var colour_picker := $TODOColourPickerButton
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
$TODOColourPickerButton.color = colour
|
||||||
|
$Label.text = title
|
||||||
|
|
||||||
|
func set_title(value: String) -> void:
|
||||||
|
title = value
|
||||||
|
$Label.text = value
|
44
Godot/addons/Todo_Manager/Current.gd
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
@tool
|
||||||
|
extends Panel
|
||||||
|
|
||||||
|
signal tree_built # used for debugging
|
||||||
|
|
||||||
|
const Todo := preload("res://addons/Todo_Manager/todo_class.gd")
|
||||||
|
const TodoItem := preload("res://addons/Todo_Manager/todoItem_class.gd")
|
||||||
|
|
||||||
|
var _sort_alphabetical := true
|
||||||
|
|
||||||
|
@onready var tree := $Tree as Tree
|
||||||
|
|
||||||
|
func build_tree(todo_item : TodoItem, patterns : Array, cased_patterns : Array[String]) -> void:
|
||||||
|
tree.clear()
|
||||||
|
var root := tree.create_item()
|
||||||
|
root.set_text(0, "Scripts")
|
||||||
|
var script := tree.create_item(root)
|
||||||
|
script.set_text(0, todo_item.get_short_path() + " -------")
|
||||||
|
script.set_metadata(0, todo_item)
|
||||||
|
for todo in todo_item.todos:
|
||||||
|
var item := tree.create_item(script)
|
||||||
|
var content_header : String = todo.content
|
||||||
|
if "\n" in todo.content:
|
||||||
|
content_header = content_header.split("\n")[0] + "..."
|
||||||
|
item.set_text(0, "(%0) - %1".format([todo.line_number, content_header], "%_"))
|
||||||
|
item.set_tooltip_text(0, todo.content)
|
||||||
|
item.set_metadata(0, todo)
|
||||||
|
for i in range(0, len(cased_patterns)):
|
||||||
|
if cased_patterns[i] == todo.pattern:
|
||||||
|
item.set_custom_color(0, patterns[i][1])
|
||||||
|
emit_signal("tree_built")
|
||||||
|
|
||||||
|
|
||||||
|
func sort_alphabetical(a, b) -> bool:
|
||||||
|
if a.script_path > b.script_path:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
func sort_backwards(a, b) -> bool:
|
||||||
|
if a.script_path < b.script_path:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
297
Godot/addons/Todo_Manager/Dock.gd
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
#signal tree_built # used for debugging
|
||||||
|
enum { CASE_INSENSITIVE, CASE_SENSITIVE }
|
||||||
|
|
||||||
|
const Project := preload("res://addons/Todo_Manager/Project.gd")
|
||||||
|
const Current := preload("res://addons/Todo_Manager/Current.gd")
|
||||||
|
|
||||||
|
const Todo := preload("res://addons/Todo_Manager/todo_class.gd")
|
||||||
|
const TodoItem := preload("res://addons/Todo_Manager/todoItem_class.gd")
|
||||||
|
const ColourPicker := preload("res://addons/Todo_Manager/UI/ColourPicker.tscn")
|
||||||
|
const Pattern := preload("res://addons/Todo_Manager/UI/Pattern.tscn")
|
||||||
|
const DEFAULT_PATTERNS := [["\\bTODO\\b", Color("96f1ad"), CASE_INSENSITIVE], ["\\bHACK\\b", Color("d5bc70"), CASE_INSENSITIVE], ["\\bFIXME\\b", Color("d57070"), CASE_INSENSITIVE]]
|
||||||
|
const DEFAULT_SCRIPT_COLOUR := Color("ccced3")
|
||||||
|
const DEFAULT_SCRIPT_NAME := false
|
||||||
|
const DEFAULT_SORT := true
|
||||||
|
|
||||||
|
var plugin : EditorPlugin
|
||||||
|
|
||||||
|
var todo_items : Array
|
||||||
|
|
||||||
|
var script_colour := Color("ccced3")
|
||||||
|
var ignore_paths : Array[String] = []
|
||||||
|
var full_path := false
|
||||||
|
var auto_refresh := true
|
||||||
|
var builtin_enabled := false
|
||||||
|
var _sort_alphabetical := true
|
||||||
|
|
||||||
|
var patterns := [["\\bTODO\\b", Color("96f1ad"), CASE_INSENSITIVE], ["\\bHACK\\b", Color("d5bc70"), CASE_INSENSITIVE], ["\\bFIXME\\b", Color("d57070"), CASE_INSENSITIVE]]
|
||||||
|
|
||||||
|
|
||||||
|
@onready var tabs := $VBoxContainer/TabContainer as TabContainer
|
||||||
|
@onready var project := $VBoxContainer/TabContainer/Project as Project
|
||||||
|
@onready var current := $VBoxContainer/TabContainer/Current as Current
|
||||||
|
@onready var project_tree := $VBoxContainer/TabContainer/Project/Tree as Tree
|
||||||
|
@onready var current_tree := $VBoxContainer/TabContainer/Current/Tree as Tree
|
||||||
|
@onready var settings_panel := $VBoxContainer/TabContainer/Settings as Panel
|
||||||
|
@onready var colours_container := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer3/Colours as VBoxContainer
|
||||||
|
@onready var pattern_container := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns as VBoxContainer
|
||||||
|
@onready var ignore_textbox := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths/TextEdit as LineEdit
|
||||||
|
@onready var auto_refresh_button := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/RefreshCheckButton as CheckButton
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
load_config()
|
||||||
|
populate_settings()
|
||||||
|
|
||||||
|
|
||||||
|
func build_tree() -> void:
|
||||||
|
if tabs:
|
||||||
|
match tabs.current_tab:
|
||||||
|
0:
|
||||||
|
project.build_tree(todo_items, ignore_paths, patterns, plugin.cased_patterns, _sort_alphabetical, full_path)
|
||||||
|
create_config_file()
|
||||||
|
1:
|
||||||
|
current.build_tree(get_active_script(), patterns, plugin.cased_patterns)
|
||||||
|
create_config_file()
|
||||||
|
2:
|
||||||
|
pass
|
||||||
|
_:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_script() -> TodoItem:
|
||||||
|
var current_script : Script = plugin.get_editor_interface().get_script_editor().get_current_script()
|
||||||
|
if current_script:
|
||||||
|
var script_path = current_script.resource_path
|
||||||
|
for todo_item in todo_items:
|
||||||
|
if todo_item.script_path == script_path:
|
||||||
|
return todo_item
|
||||||
|
|
||||||
|
# nothing found
|
||||||
|
var todo_item := TodoItem.new(script_path, [])
|
||||||
|
return todo_item
|
||||||
|
else:
|
||||||
|
# not a script
|
||||||
|
var todo_item := TodoItem.new("res://Documentation", [])
|
||||||
|
return todo_item
|
||||||
|
|
||||||
|
|
||||||
|
func go_to_script(script_path: String, line_number : int = 0) -> void:
|
||||||
|
if plugin.get_editor_interface().get_editor_settings().get_setting("text_editor/external/use_external_editor"):
|
||||||
|
var exec_path = plugin.get_editor_interface().get_editor_settings().get_setting("text_editor/external/exec_path")
|
||||||
|
var args := get_exec_flags(exec_path, script_path, line_number)
|
||||||
|
OS.execute(exec_path, args)
|
||||||
|
else:
|
||||||
|
var script := load(script_path)
|
||||||
|
plugin.get_editor_interface().edit_resource(script)
|
||||||
|
plugin.get_editor_interface().get_script_editor().goto_line(line_number - 1)
|
||||||
|
|
||||||
|
func get_exec_flags(editor_path : String, script_path : String, line_number : int) -> PackedStringArray:
|
||||||
|
var args : PackedStringArray
|
||||||
|
var script_global_path = ProjectSettings.globalize_path(script_path)
|
||||||
|
|
||||||
|
if editor_path.ends_with("code.cmd") or editor_path.ends_with("code"): ## VS Code
|
||||||
|
args.append(ProjectSettings.globalize_path("res://"))
|
||||||
|
args.append("--goto")
|
||||||
|
args.append(script_global_path + ":" + str(line_number))
|
||||||
|
|
||||||
|
elif editor_path.ends_with("rider64.exe") or editor_path.ends_with("rider"): ## Rider
|
||||||
|
args.append("--line")
|
||||||
|
args.append(str(line_number))
|
||||||
|
args.append(script_global_path)
|
||||||
|
|
||||||
|
else: ## Atom / Sublime
|
||||||
|
args.append(script_global_path + ":" + str(line_number))
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
func sort_alphabetical(a, b) -> bool:
|
||||||
|
if a.script_path > b.script_path:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
func sort_backwards(a, b) -> bool:
|
||||||
|
if a.script_path < b.script_path:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func populate_settings() -> void:
|
||||||
|
for i in patterns.size():
|
||||||
|
## Create Colour Pickers
|
||||||
|
var colour_picker: Variant = ColourPicker.instantiate()
|
||||||
|
colour_picker.colour = patterns[i][1]
|
||||||
|
colour_picker.title = patterns[i][0]
|
||||||
|
colour_picker.index = i
|
||||||
|
colours_container.add_child(colour_picker)
|
||||||
|
colour_picker.colour_picker.color_changed.connect(change_colour.bind(i))
|
||||||
|
|
||||||
|
## Create Patterns
|
||||||
|
var pattern_edit: Variant = Pattern.instantiate()
|
||||||
|
pattern_edit.text = patterns[i][0]
|
||||||
|
pattern_edit.index = i
|
||||||
|
pattern_container.add_child(pattern_edit)
|
||||||
|
pattern_edit.line_edit.text_changed.connect(change_pattern.bind(i,
|
||||||
|
colour_picker))
|
||||||
|
pattern_edit.remove_button.pressed.connect(remove_pattern.bind(i,
|
||||||
|
pattern_edit, colour_picker))
|
||||||
|
pattern_edit.case_checkbox.button_pressed = patterns[i][2]
|
||||||
|
pattern_edit.case_checkbox.toggled.connect(case_sensitive_pattern.bind(i))
|
||||||
|
|
||||||
|
var pattern_button := $VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns/AddPatternButton
|
||||||
|
$VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns.move_child(pattern_button, 0)
|
||||||
|
|
||||||
|
# path filtering
|
||||||
|
var ignore_paths_field := ignore_textbox
|
||||||
|
if not ignore_paths_field.is_connected("text_changed", _on_ignore_paths_changed):
|
||||||
|
ignore_paths_field.connect("text_changed", _on_ignore_paths_changed)
|
||||||
|
var ignore_paths_text := ""
|
||||||
|
for path in ignore_paths:
|
||||||
|
ignore_paths_text += path + ", "
|
||||||
|
ignore_paths_text = ignore_paths_text.trim_suffix(", ")
|
||||||
|
ignore_paths_field.text = ignore_paths_text
|
||||||
|
|
||||||
|
auto_refresh_button.button_pressed = auto_refresh
|
||||||
|
|
||||||
|
|
||||||
|
func rebuild_settings() -> void:
|
||||||
|
for node in colours_container.get_children():
|
||||||
|
node.queue_free()
|
||||||
|
for node in pattern_container.get_children():
|
||||||
|
if node is Button:
|
||||||
|
continue
|
||||||
|
node.queue_free()
|
||||||
|
populate_settings()
|
||||||
|
|
||||||
|
|
||||||
|
#### CONFIG FILE ####
|
||||||
|
func create_config_file() -> void:
|
||||||
|
var config = ConfigFile.new()
|
||||||
|
config.set_value("scripts", "full_path", full_path)
|
||||||
|
config.set_value("scripts", "sort_alphabetical", _sort_alphabetical)
|
||||||
|
config.set_value("scripts", "script_colour", script_colour)
|
||||||
|
config.set_value("scripts", "ignore_paths", ignore_paths)
|
||||||
|
|
||||||
|
config.set_value("patterns", "patterns", patterns)
|
||||||
|
|
||||||
|
config.set_value("config", "auto_refresh", auto_refresh)
|
||||||
|
config.set_value("config", "builtin_enabled", builtin_enabled)
|
||||||
|
|
||||||
|
var err = config.save("res://addons/Todo_Manager/todo.cfg")
|
||||||
|
|
||||||
|
|
||||||
|
func load_config() -> void:
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
if config.load("res://addons/Todo_Manager/todo.cfg") == OK:
|
||||||
|
full_path = config.get_value("scripts", "full_path", DEFAULT_SCRIPT_NAME)
|
||||||
|
_sort_alphabetical = config.get_value("scripts", "sort_alphabetical", DEFAULT_SORT)
|
||||||
|
script_colour = config.get_value("scripts", "script_colour", DEFAULT_SCRIPT_COLOUR)
|
||||||
|
ignore_paths = config.get_value("scripts", "ignore_paths", [] as Array[String])
|
||||||
|
patterns = config.get_value("patterns", "patterns", DEFAULT_PATTERNS)
|
||||||
|
auto_refresh = config.get_value("config", "auto_refresh", true)
|
||||||
|
builtin_enabled = config.get_value("config", "builtin_enabled", false)
|
||||||
|
else:
|
||||||
|
create_config_file()
|
||||||
|
|
||||||
|
|
||||||
|
#### Events ####
|
||||||
|
func _on_SettingsButton_toggled(button_pressed: bool) -> void:
|
||||||
|
settings_panel.visible = button_pressed
|
||||||
|
if button_pressed == false:
|
||||||
|
create_config_file()
|
||||||
|
# plugin.find_tokens_from_path(plugin.script_cache)
|
||||||
|
if auto_refresh:
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func _on_Tree_item_activated() -> void:
|
||||||
|
var item : TreeItem
|
||||||
|
match tabs.current_tab:
|
||||||
|
0:
|
||||||
|
item = project_tree.get_selected()
|
||||||
|
1:
|
||||||
|
item = current_tree.get_selected()
|
||||||
|
if item.get_metadata(0) is Todo:
|
||||||
|
var todo : Todo = item.get_metadata(0)
|
||||||
|
call_deferred("go_to_script", todo.script_path, todo.line_number)
|
||||||
|
else:
|
||||||
|
var todo_item = item.get_metadata(0)
|
||||||
|
call_deferred("go_to_script", todo_item.script_path)
|
||||||
|
|
||||||
|
func _on_FullPathCheckBox_toggled(button_pressed: bool) -> void:
|
||||||
|
full_path = button_pressed
|
||||||
|
|
||||||
|
func _on_ScriptColourPickerButton_color_changed(color: Color) -> void:
|
||||||
|
script_colour = color
|
||||||
|
|
||||||
|
func _on_RescanButton_pressed() -> void:
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func change_colour(colour: Color, index: int) -> void:
|
||||||
|
patterns[index][1] = colour
|
||||||
|
|
||||||
|
func change_pattern(value: String, index: int, this_colour: Node) -> void:
|
||||||
|
patterns[index][0] = value
|
||||||
|
this_colour.title = value
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func remove_pattern(index: int, this: Node, this_colour: Node) -> void:
|
||||||
|
patterns.remove_at(index)
|
||||||
|
this.queue_free()
|
||||||
|
this_colour.queue_free()
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func case_sensitive_pattern(active: bool, index: int) -> void:
|
||||||
|
if active:
|
||||||
|
patterns[index][2] = CASE_SENSITIVE
|
||||||
|
else:
|
||||||
|
patterns[index][2] = CASE_INSENSITIVE
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func _on_DefaultButton_pressed() -> void:
|
||||||
|
patterns = DEFAULT_PATTERNS.duplicate(true)
|
||||||
|
_sort_alphabetical = DEFAULT_SORT
|
||||||
|
script_colour = DEFAULT_SCRIPT_COLOUR
|
||||||
|
full_path = DEFAULT_SCRIPT_NAME
|
||||||
|
rebuild_settings()
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func _on_AlphSortCheckBox_toggled(button_pressed: bool) -> void:
|
||||||
|
_sort_alphabetical = button_pressed
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func _on_AddPatternButton_pressed() -> void:
|
||||||
|
patterns.append(["\\bplaceholder\\b", Color.WHITE, CASE_INSENSITIVE])
|
||||||
|
rebuild_settings()
|
||||||
|
|
||||||
|
func _on_RefreshCheckButton_toggled(button_pressed: bool) -> void:
|
||||||
|
auto_refresh = button_pressed
|
||||||
|
|
||||||
|
func _on_Timer_timeout() -> void:
|
||||||
|
plugin.refresh_lock = false
|
||||||
|
|
||||||
|
func _on_ignore_paths_changed(new_text: String) -> void:
|
||||||
|
var text = ignore_textbox.text
|
||||||
|
var split: Array = text.split(',')
|
||||||
|
ignore_paths.clear()
|
||||||
|
for elem in split:
|
||||||
|
if elem == " " || elem == "":
|
||||||
|
continue
|
||||||
|
ignore_paths.push_front(elem.lstrip(' ').rstrip(' '))
|
||||||
|
# validate so no empty string slips through (all paths ignored)
|
||||||
|
var i := 0
|
||||||
|
for path in ignore_paths:
|
||||||
|
if (path == "" || path == " "):
|
||||||
|
ignore_paths.remove_at(i)
|
||||||
|
i += 1
|
||||||
|
plugin.rescan_files(true)
|
||||||
|
|
||||||
|
func _on_TabContainer_tab_changed(tab: int) -> void:
|
||||||
|
build_tree()
|
||||||
|
|
||||||
|
func _on_BuiltInCheckButton_toggled(button_pressed: bool) -> void:
|
||||||
|
builtin_enabled = button_pressed
|
||||||
|
plugin.rescan_files(true)
|
21
Godot/addons/Todo_Manager/Pattern.gd
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
@tool
|
||||||
|
extends HBoxContainer
|
||||||
|
|
||||||
|
|
||||||
|
var text : String : set = set_text
|
||||||
|
var disabled : bool
|
||||||
|
var index : int
|
||||||
|
|
||||||
|
@onready var line_edit := $LineEdit as LineEdit
|
||||||
|
@onready var remove_button := $RemoveButton as Button
|
||||||
|
@onready var case_checkbox := %CaseSensativeCheckbox as CheckBox
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
line_edit.text = text
|
||||||
|
remove_button.disabled = disabled
|
||||||
|
|
||||||
|
|
||||||
|
func set_text(value: String) -> void:
|
||||||
|
text = value
|
||||||
|
if line_edit:
|
||||||
|
line_edit.text = value
|
73
Godot/addons/Todo_Manager/Project.gd
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
@tool
|
||||||
|
extends Panel
|
||||||
|
|
||||||
|
signal tree_built # used for debugging
|
||||||
|
|
||||||
|
const Todo := preload("res://addons/Todo_Manager/todo_class.gd")
|
||||||
|
|
||||||
|
var _sort_alphabetical := true
|
||||||
|
var _full_path := false
|
||||||
|
|
||||||
|
@onready var tree := $Tree as Tree
|
||||||
|
|
||||||
|
func build_tree(todo_items : Array, ignore_paths : Array, patterns : Array, cased_patterns: Array[String], sort_alphabetical : bool, full_path : bool) -> void:
|
||||||
|
_full_path = full_path
|
||||||
|
tree.clear()
|
||||||
|
if sort_alphabetical:
|
||||||
|
todo_items.sort_custom(Callable(self, "sort_alphabetical"))
|
||||||
|
else:
|
||||||
|
todo_items.sort_custom(Callable(self, "sort_backwards"))
|
||||||
|
var root := tree.create_item()
|
||||||
|
root.set_text(0, "Scripts")
|
||||||
|
for todo_item in todo_items:
|
||||||
|
var ignore := false
|
||||||
|
for ignore_path in ignore_paths:
|
||||||
|
var script_path : String = todo_item.script_path
|
||||||
|
if script_path.begins_with(ignore_path) or script_path.begins_with("res://" + ignore_path) or script_path.begins_with("res:///" + ignore_path):
|
||||||
|
ignore = true
|
||||||
|
break
|
||||||
|
if ignore:
|
||||||
|
continue
|
||||||
|
var script := tree.create_item(root)
|
||||||
|
if full_path:
|
||||||
|
script.set_text(0, todo_item.script_path + " -------")
|
||||||
|
else:
|
||||||
|
script.set_text(0, todo_item.get_short_path() + " -------")
|
||||||
|
script.set_metadata(0, todo_item)
|
||||||
|
for todo in todo_item.todos:
|
||||||
|
var item := tree.create_item(script)
|
||||||
|
var content_header : String = todo.content
|
||||||
|
if "\n" in todo.content:
|
||||||
|
content_header = content_header.split("\n")[0] + "..."
|
||||||
|
item.set_text(0, "(%0) - %1".format([todo.line_number, content_header], "%_"))
|
||||||
|
item.set_tooltip_text(0, todo.content)
|
||||||
|
item.set_metadata(0, todo)
|
||||||
|
for i in range(0, len(cased_patterns)):
|
||||||
|
if cased_patterns[i] == todo.pattern:
|
||||||
|
item.set_custom_color(0, patterns[i][1])
|
||||||
|
emit_signal("tree_built")
|
||||||
|
|
||||||
|
|
||||||
|
func sort_alphabetical(a, b) -> bool:
|
||||||
|
if _full_path:
|
||||||
|
if a.script_path < b.script_path:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
else:
|
||||||
|
if a.get_short_path() < b.get_short_path():
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
func sort_backwards(a, b) -> bool:
|
||||||
|
if _full_path:
|
||||||
|
if a.script_path > b.script_path:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
else:
|
||||||
|
if a.get_short_path() > b.get_short_path():
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return false
|
60
Godot/addons/Todo_Manager/README.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
|
||||||
|
### Localised READMEs
|
||||||
|
- [简体中文](READMECN.md) (Simplified Chinese)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Manager
|
||||||
|
|
||||||
|
![example_image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/example1.png)
|
||||||
|
|
||||||
|
## Simple and flexible
|
||||||
|
|
||||||
|
- Supports GDScript, C# and GDNative
|
||||||
|
- Seamlessly integrated into the Godot dock
|
||||||
|
- Lenient syntax. Write TODOs that suit your style
|
||||||
|
- Quickly jump to lines and launch external editors
|
||||||
|
|
||||||
|
## Customizable
|
||||||
|
|
||||||
|
![settings_example](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/example2.png)
|
||||||
|
|
||||||
|
- Add your own RegEx patterns
|
||||||
|
- Set colours to your liking
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Method 1 (Godot Asset Library)
|
||||||
|
|
||||||
|
The most simple way to get started using TODO Manager is to use Godot's inbuilt Asset Library to install the plugin into your project.
|
||||||
|
|
||||||
|
#### Step 1
|
||||||
|
|
||||||
|
Find TODO Manager in the Godot Asset Library.
|
||||||
|
![AssetLib image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct1.png)
|
||||||
|
|
||||||
|
#### Step 2
|
||||||
|
|
||||||
|
Install the package. You may want to untick the /doc folder at this point as it is not necessary for the functions of the plugin.
|
||||||
|
![Filestrcture image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct3.png)
|
||||||
|
|
||||||
|
#### Step 4
|
||||||
|
|
||||||
|
Enable the plugin in the project settings.
|
||||||
|
![Project image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct4.png)
|
||||||
|
|
||||||
|
### Method 2 (GitHub)
|
||||||
|
|
||||||
|
#### Step 1
|
||||||
|
|
||||||
|
Click Download ZIP from the 'Code' dropdown.
|
||||||
|
![GitHub image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct5.png)
|
||||||
|
|
||||||
|
#### Step 2
|
||||||
|
|
||||||
|
- Unzip the file and add it into your project folder. Make sure 'addons' is a subdirectory of res://
|
||||||
|
- DO NOT change the name of the 'addons' or 'Todo_Manager' folders as this will break the saving and loading of your settings.
|
||||||
|
|
||||||
|
#### Step 3
|
||||||
|
|
||||||
|
Enable the plugin in the project settings.
|
||||||
|
![Project image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct4.png)
|
56
Godot/addons/Todo_Manager/READMECN.md
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# TODO Manager
|
||||||
|
|
||||||
|
![example_image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/example1.png)
|
||||||
|
|
||||||
|
## 简单而灵活
|
||||||
|
|
||||||
|
- 支持 GDScript,C# 和 GDNative。
|
||||||
|
- 无缝集成到 Godot dock 栏。
|
||||||
|
- 宽松的语法,用适合你自己的风格写TODOs。
|
||||||
|
- 快速跳转到某一行并启用外部编辑器。
|
||||||
|
|
||||||
|
## 可定制
|
||||||
|
|
||||||
|
![settings_example](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/example2.png)
|
||||||
|
|
||||||
|
- 添加你自己的正则表达式。
|
||||||
|
- 设置你喜欢的颜色。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
### 方法一 (Godot Asset Library)
|
||||||
|
|
||||||
|
最简单的使用 TODO Manager 的方法,使用 Godot 内置的资源商店(Asset Library)来安装这个插件到你的项目。
|
||||||
|
|
||||||
|
#### 第一步
|
||||||
|
|
||||||
|
在资源商店搜索 TODO Manager。
|
||||||
|
![AssetLib image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct1.png)
|
||||||
|
|
||||||
|
#### 第二步
|
||||||
|
|
||||||
|
安装下载的插件,你可能需要取消勾选 /doc 文件夹,因为插件的功能不需要。
|
||||||
|
![Filestrcture image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct3.png)
|
||||||
|
|
||||||
|
#### 第三步
|
||||||
|
|
||||||
|
在项目设置里启用插件。
|
||||||
|
![Project image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct4.png)
|
||||||
|
|
||||||
|
### 方法二 (GitHub)
|
||||||
|
|
||||||
|
#### 第一步
|
||||||
|
|
||||||
|
点击 Download ZIP。
|
||||||
|
![GitHub image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct5.png)
|
||||||
|
|
||||||
|
#### 第二步
|
||||||
|
|
||||||
|
- 解压文件并且放到你的项目文件夹。确保 “addons” 是 res:// 的子文件夹。
|
||||||
|
- DO NOT change the name of the 'addons' or 'Todo_Manager' folders as this will break the saving and loading of your settings.
|
||||||
|
- 不要更改 “addons” 或 “Todo_Manager” 文件夹的名称,因为这会打破预设的保存和加载。
|
||||||
|
|
||||||
|
#### 第三步
|
||||||
|
|
||||||
|
在项目设置里启用这个插件。
|
||||||
|
![Project image](https://github.com/OrigamiDev-Pete/TODO_Manager/blob/main/addons/Todo_Manager/doc/images/Instruct4.png)
|
21
Godot/addons/Todo_Manager/UI/ColourPicker.tscn
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bie1xn8v1kd66"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/Todo_Manager/ColourPicker.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="TODOColour" type="HBoxContainer"]
|
||||||
|
offset_right = 105.0
|
||||||
|
offset_bottom = 31.0
|
||||||
|
script = ExtResource("1")
|
||||||
|
metadata/_edit_use_custom_anchors = false
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="."]
|
||||||
|
offset_top = 4.0
|
||||||
|
offset_right = 1.0
|
||||||
|
offset_bottom = 27.0
|
||||||
|
|
||||||
|
[node name="TODOColourPickerButton" type="ColorPickerButton" parent="."]
|
||||||
|
custom_minimum_size = Vector2(40, 0)
|
||||||
|
offset_left = 65.0
|
||||||
|
offset_right = 105.0
|
||||||
|
offset_bottom = 31.0
|
||||||
|
size_flags_horizontal = 10
|
315
Godot/addons/Todo_Manager/UI/Dock.tscn
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
[gd_scene load_steps=6 format=3 uid="uid://b6k0dtftankcx"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/Todo_Manager/Dock.gd" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://addons/Todo_Manager/Project.gd" id="2"]
|
||||||
|
[ext_resource type="Script" path="res://addons/Todo_Manager/Current.gd" id="3"]
|
||||||
|
|
||||||
|
[sub_resource type="ButtonGroup" id="ButtonGroup_kqxcu"]
|
||||||
|
|
||||||
|
[sub_resource type="ButtonGroup" id="ButtonGroup_kltg3"]
|
||||||
|
|
||||||
|
[node name="Dock" type="Control"]
|
||||||
|
custom_minimum_size = Vector2(0, 200)
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = 4.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="Header" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HeaderLeft" type="HBoxContainer" parent="VBoxContainer/Header"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Title" type="Label" parent="VBoxContainer/Header/HeaderLeft"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Todo Dock:"
|
||||||
|
|
||||||
|
[node name="HeaderRight" type="HBoxContainer" parent="VBoxContainer/Header"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
alignment = 2
|
||||||
|
|
||||||
|
[node name="SettingsButton" type="Button" parent="VBoxContainer/Header/HeaderRight"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
toggle_mode = true
|
||||||
|
text = "Settings"
|
||||||
|
|
||||||
|
[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Project" type="Panel" parent="VBoxContainer/TabContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("2")
|
||||||
|
|
||||||
|
[node name="Tree" type="Tree" parent="VBoxContainer/TabContainer/Project"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
hide_root = true
|
||||||
|
|
||||||
|
[node name="Current" type="Panel" parent="VBoxContainer/TabContainer"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("3")
|
||||||
|
|
||||||
|
[node name="Tree" type="Tree" parent="VBoxContainer/TabContainer/Current"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
hide_folding = true
|
||||||
|
hide_root = true
|
||||||
|
|
||||||
|
[node name="Settings" type="Panel" parent="VBoxContainer/TabContainer"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/TabContainer/Settings"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Scripts" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Scripts"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Scripts:"
|
||||||
|
|
||||||
|
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Scripts"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 5
|
||||||
|
|
||||||
|
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Scripts" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ScriptName" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Script Name:"
|
||||||
|
|
||||||
|
[node name="FullPathCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName"]
|
||||||
|
layout_mode = 2
|
||||||
|
button_group = SubResource("ButtonGroup_kqxcu")
|
||||||
|
text = "Full path"
|
||||||
|
|
||||||
|
[node name="ShortNameCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName"]
|
||||||
|
layout_mode = 2
|
||||||
|
button_pressed = true
|
||||||
|
button_group = SubResource("ButtonGroup_kqxcu")
|
||||||
|
text = "Short name"
|
||||||
|
|
||||||
|
[node name="ScriptSort" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Sort Order:"
|
||||||
|
|
||||||
|
[node name="AlphSortCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort"]
|
||||||
|
layout_mode = 2
|
||||||
|
button_pressed = true
|
||||||
|
button_group = SubResource("ButtonGroup_kltg3")
|
||||||
|
text = "Alphabetical"
|
||||||
|
|
||||||
|
[node name="RAlphSortCheckBox" type="CheckBox" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort"]
|
||||||
|
layout_mode = 2
|
||||||
|
button_group = SubResource("ButtonGroup_kltg3")
|
||||||
|
text = "Reverse Alphabetical"
|
||||||
|
|
||||||
|
[node name="ScriptColour" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptColour"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Script Colour:"
|
||||||
|
|
||||||
|
[node name="ScriptColourPickerButton" type="ColorPickerButton" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptColour"]
|
||||||
|
custom_minimum_size = Vector2(40, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0.8, 0.807843, 0.827451, 1)
|
||||||
|
|
||||||
|
[node name="IgnorePaths" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Ignore Paths:"
|
||||||
|
|
||||||
|
[node name="TextEdit" type="LineEdit" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths"]
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
expand_to_text_length = true
|
||||||
|
|
||||||
|
[node name="Label3" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/IgnorePaths"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "(Separated by commas)"
|
||||||
|
|
||||||
|
[node name="TODOColours" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/TODOColours"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "TODO Colours:"
|
||||||
|
|
||||||
|
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/TODOColours"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer3"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Colours" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer3"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Patterns" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Patterns"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Patterns:"
|
||||||
|
|
||||||
|
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Patterns"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer4" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Patterns" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="AddPatternButton" type="Button" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Add"
|
||||||
|
|
||||||
|
[node name="Config" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Config"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Config:"
|
||||||
|
|
||||||
|
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/Config"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer5" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Patterns" type="VBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RefreshCheckButton" type="CheckButton" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
button_pressed = true
|
||||||
|
text = "Auto Refresh"
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="BuiltInCheckButton" type="CheckButton" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Scan Built-in Scripts"
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="DefaultButton" type="Button" parent="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Reset to default"
|
||||||
|
|
||||||
|
[node name="Timer" type="Timer" parent="."]
|
||||||
|
one_shot = true
|
||||||
|
|
||||||
|
[node name="RescanButton" type="Button" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 1
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -102.0
|
||||||
|
offset_top = 3.0
|
||||||
|
offset_bottom = 34.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
text = "Rescan Files"
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[connection signal="toggled" from="VBoxContainer/Header/HeaderRight/SettingsButton" to="." method="_on_SettingsButton_toggled"]
|
||||||
|
[connection signal="tab_changed" from="VBoxContainer/TabContainer" to="." method="_on_TabContainer_tab_changed"]
|
||||||
|
[connection signal="item_activated" from="VBoxContainer/TabContainer/Project/Tree" to="." method="_on_Tree_item_activated"]
|
||||||
|
[connection signal="item_activated" from="VBoxContainer/TabContainer/Current/Tree" to="." method="_on_Tree_item_activated"]
|
||||||
|
[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptName/FullPathCheckBox" to="." method="_on_FullPathCheckBox_toggled"]
|
||||||
|
[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptSort/AlphSortCheckBox" to="." method="_on_AlphSortCheckBox_toggled"]
|
||||||
|
[connection signal="color_changed" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/VBoxContainer/HBoxContainer2/Scripts/ScriptColour/ScriptColourPickerButton" to="." method="_on_ScriptColourPickerButton_color_changed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer4/Patterns/AddPatternButton" to="." method="_on_AddPatternButton_pressed"]
|
||||||
|
[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/RefreshCheckButton" to="." method="_on_RefreshCheckButton_toggled"]
|
||||||
|
[connection signal="toggled" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/HBoxContainer/BuiltInCheckButton" to="." method="_on_BuiltInCheckButton_toggled"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/TabContainer/Settings/ScrollContainer/MarginContainer/VBoxContainer/HBoxContainer5/Patterns/DefaultButton" to="." method="_on_DefaultButton_pressed"]
|
||||||
|
[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]
|
||||||
|
[connection signal="pressed" from="RescanButton" to="." method="_on_RescanButton_pressed"]
|
26
Godot/addons/Todo_Manager/UI/Pattern.tscn
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bx11sel2q5wli"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/Todo_Manager/Pattern.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Pattern" type="HBoxContainer"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="LineEdit" type="LineEdit" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
expand_to_text_length = true
|
||||||
|
|
||||||
|
[node name="RemoveButton" type="Button" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "-"
|
||||||
|
|
||||||
|
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||||
|
custom_minimum_size = Vector2(20, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
|
||||||
|
[node name="CaseSensativeCheckbox" type="CheckBox" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Case Sensitive"
|
7
Godot/addons/Todo_Manager/doc/example.gd
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# TODO: this is a TODO
|
||||||
|
# HACK: this is a HACK
|
||||||
|
# FIXME: this is a FIXME
|
||||||
|
# TODO this works too
|
||||||
|
#Hack any format will do
|
BIN
Godot/addons/Todo_Manager/doc/images/Instruct1.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/Instruct2.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/Instruct3.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/Instruct4.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/Instruct5.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/TODO_Manager_Logo.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/TodoExternal.gif
Normal file
After Width: | Height: | Size: 243 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/example1.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
Godot/addons/Todo_Manager/doc/images/example2.png
Normal file
After Width: | Height: | Size: 30 KiB |
7
Godot/addons/Todo_Manager/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="Todo Manager"
|
||||||
|
description="Dock for housing TODO messages."
|
||||||
|
author="Peter de Vroom"
|
||||||
|
version="2.3.1"
|
||||||
|
script="plugin.gd"
|
286
Godot/addons/Todo_Manager/plugin.gd
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
const DockScene := preload("res://addons/Todo_Manager/UI/Dock.tscn")
|
||||||
|
const Dock := preload("res://addons/Todo_Manager/Dock.gd")
|
||||||
|
const Todo := preload("res://addons/Todo_Manager/todo_class.gd")
|
||||||
|
const TodoItem := preload("res://addons/Todo_Manager/todoItem_class.gd")
|
||||||
|
|
||||||
|
var _dockUI : Dock
|
||||||
|
|
||||||
|
class TodoCacheValue:
|
||||||
|
var todos: Array
|
||||||
|
var last_modified_time: int
|
||||||
|
|
||||||
|
func _init(todos: Array, last_modified_time: int):
|
||||||
|
self.todos = todos
|
||||||
|
self.last_modified_time = last_modified_time
|
||||||
|
|
||||||
|
var todo_cache : Dictionary # { key: script_path, value: TodoCacheValue }
|
||||||
|
var remove_queue : Array
|
||||||
|
var combined_pattern : String
|
||||||
|
var cased_patterns : Array[String]
|
||||||
|
|
||||||
|
var refresh_lock := false # makes sure _on_filesystem_changed only triggers once
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
_dockUI = DockScene.instantiate() as Control
|
||||||
|
add_control_to_bottom_panel(_dockUI, "TODO")
|
||||||
|
get_editor_interface().get_resource_filesystem().connect("filesystem_changed",
|
||||||
|
_on_filesystem_changed)
|
||||||
|
get_editor_interface().get_file_system_dock().connect("file_removed", queue_remove)
|
||||||
|
get_editor_interface().get_script_editor().connect("editor_script_changed",
|
||||||
|
_on_active_script_changed)
|
||||||
|
_dockUI.plugin = self
|
||||||
|
|
||||||
|
combined_pattern = combine_patterns(_dockUI.patterns)
|
||||||
|
find_tokens_from_path(find_scripts())
|
||||||
|
_dockUI.build_tree()
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
_dockUI.create_config_file()
|
||||||
|
remove_control_from_bottom_panel(_dockUI)
|
||||||
|
_dockUI.free()
|
||||||
|
|
||||||
|
|
||||||
|
func queue_remove(file: String):
|
||||||
|
for i in _dockUI.todo_items.size() - 1:
|
||||||
|
if _dockUI.todo_items[i].script_path == file:
|
||||||
|
_dockUI.todo_items.remove_at(i)
|
||||||
|
|
||||||
|
|
||||||
|
func find_tokens_from_path(scripts: Array[String]) -> void:
|
||||||
|
for script_path in scripts:
|
||||||
|
var file := FileAccess.open(script_path, FileAccess.READ)
|
||||||
|
var contents := file.get_as_text()
|
||||||
|
if script_path.ends_with(".tscn"):
|
||||||
|
handle_built_in_scripts(contents, script_path)
|
||||||
|
else:
|
||||||
|
find_tokens(contents, script_path)
|
||||||
|
|
||||||
|
|
||||||
|
func handle_built_in_scripts(contents: String, resource_path: String):
|
||||||
|
var s := contents.split("sub_resource type=\"GDScript\"")
|
||||||
|
if s.size() <= 1:
|
||||||
|
return
|
||||||
|
for i in range(1, s.size()):
|
||||||
|
var script_components := s[i].split("script/source")
|
||||||
|
var script_name = script_components[0].substr(5, 14)
|
||||||
|
find_tokens(script_components[1], resource_path + "::" + script_name)
|
||||||
|
|
||||||
|
|
||||||
|
func find_tokens(text: String, script_path: String) -> void:
|
||||||
|
var cached_todos = get_cached_todos(script_path)
|
||||||
|
if cached_todos.size() != 0:
|
||||||
|
# var i := 0
|
||||||
|
# for todo_item in _dockUI.todo_items:
|
||||||
|
# if todo_item.script_path == script_path:
|
||||||
|
# _dockUI.todo_items.remove_at(i)
|
||||||
|
# i += 1
|
||||||
|
var todo_item := TodoItem.new(script_path, cached_todos)
|
||||||
|
_dockUI.todo_items.append(todo_item)
|
||||||
|
else:
|
||||||
|
var regex = RegEx.new()
|
||||||
|
# if regex.compile("#\\s*\\bTODO\\b.*|#\\s*\\bHACK\\b.*") == OK:
|
||||||
|
if regex.compile(combined_pattern) == OK:
|
||||||
|
var result : Array[RegExMatch] = regex.search_all(text)
|
||||||
|
if result.is_empty():
|
||||||
|
for i in _dockUI.todo_items.size():
|
||||||
|
if _dockUI.todo_items[i].script_path == script_path:
|
||||||
|
_dockUI.todo_items.remove_at(i)
|
||||||
|
return # No tokens found
|
||||||
|
var match_found : bool
|
||||||
|
var i := 0
|
||||||
|
for todo_item in _dockUI.todo_items:
|
||||||
|
if todo_item.script_path == script_path:
|
||||||
|
match_found = true
|
||||||
|
var updated_todo_item := update_todo_item(todo_item, result, text, script_path)
|
||||||
|
_dockUI.todo_items.remove_at(i)
|
||||||
|
_dockUI.todo_items.insert(i, updated_todo_item)
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
if !match_found:
|
||||||
|
_dockUI.todo_items.append(create_todo_item(result, text, script_path))
|
||||||
|
|
||||||
|
|
||||||
|
func create_todo_item(regex_results: Array[RegExMatch], text: String, script_path: String) -> TodoItem:
|
||||||
|
var todo_item = TodoItem.new(script_path, [])
|
||||||
|
todo_item.script_path = script_path
|
||||||
|
var last_line_number := 0
|
||||||
|
var lines := text.split("\n")
|
||||||
|
for r in regex_results:
|
||||||
|
var new_todo : Todo = create_todo(r.get_string(), script_path)
|
||||||
|
new_todo.line_number = get_line_number(r.get_string(), text, last_line_number)
|
||||||
|
# GD Multiline comment
|
||||||
|
var trailing_line := new_todo.line_number
|
||||||
|
var should_break = false
|
||||||
|
while trailing_line < lines.size() and lines[trailing_line].dedent().begins_with("#"):
|
||||||
|
for other_r in regex_results:
|
||||||
|
if lines[trailing_line] in other_r.get_string():
|
||||||
|
should_break = true
|
||||||
|
break
|
||||||
|
if should_break:
|
||||||
|
break
|
||||||
|
|
||||||
|
new_todo.content += "\n" + lines[trailing_line]
|
||||||
|
trailing_line += 1
|
||||||
|
|
||||||
|
last_line_number = new_todo.line_number
|
||||||
|
todo_item.todos.append(new_todo)
|
||||||
|
cache_todos(todo_item.todos, script_path)
|
||||||
|
return todo_item
|
||||||
|
|
||||||
|
|
||||||
|
func update_todo_item(todo_item: TodoItem, regex_results: Array[RegExMatch], text: String, script_path: String) -> TodoItem:
|
||||||
|
todo_item.todos.clear()
|
||||||
|
var lines := text.split("\n")
|
||||||
|
for r in regex_results:
|
||||||
|
var new_todo : Todo = create_todo(r.get_string(), script_path)
|
||||||
|
new_todo.line_number = get_line_number(r.get_string(), text)
|
||||||
|
# GD Multiline comment
|
||||||
|
var trailing_line := new_todo.line_number
|
||||||
|
var should_break = false
|
||||||
|
while trailing_line < lines.size() and lines[trailing_line].dedent().begins_with("#"):
|
||||||
|
for other_r in regex_results:
|
||||||
|
if lines[trailing_line] in other_r.get_string():
|
||||||
|
should_break = true
|
||||||
|
break
|
||||||
|
if should_break:
|
||||||
|
break
|
||||||
|
|
||||||
|
new_todo.content += "\n" + lines[trailing_line]
|
||||||
|
trailing_line += 1
|
||||||
|
todo_item.todos.append(new_todo)
|
||||||
|
return todo_item
|
||||||
|
|
||||||
|
|
||||||
|
func get_line_number(what: String, from: String, start := 0) -> int:
|
||||||
|
what = what.split('\n')[0] # Match first line of multiline C# comments
|
||||||
|
var temp_array := from.split('\n')
|
||||||
|
var lines := Array(temp_array)
|
||||||
|
var line_number# = lines.find(what) + 1
|
||||||
|
for i in range(start, lines.size()):
|
||||||
|
if what in lines[i]:
|
||||||
|
line_number = i + 1 # +1 to account of 0-based array vs 1-based line numbers
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
line_number = 0 # This is an error
|
||||||
|
return line_number
|
||||||
|
|
||||||
|
|
||||||
|
func _on_filesystem_changed() -> void:
|
||||||
|
if !refresh_lock:
|
||||||
|
if _dockUI.auto_refresh:
|
||||||
|
refresh_lock = true
|
||||||
|
_dockUI.get_node("Timer").start()
|
||||||
|
rescan_files(false)
|
||||||
|
|
||||||
|
|
||||||
|
func find_scripts() -> Array[String]:
|
||||||
|
var scripts : Array[String]
|
||||||
|
var directory_queue : Array[String]
|
||||||
|
var dir := DirAccess.open("res://")
|
||||||
|
if dir.get_open_error() == OK:
|
||||||
|
get_dir_contents(dir, scripts, directory_queue)
|
||||||
|
else:
|
||||||
|
printerr("TODO_Manager: There was an error during find_scripts()")
|
||||||
|
|
||||||
|
while not directory_queue.is_empty():
|
||||||
|
if dir.change_dir(directory_queue[0]) == OK:
|
||||||
|
get_dir_contents(dir, scripts, directory_queue)
|
||||||
|
else:
|
||||||
|
printerr("TODO_Manager: There was an error at: " + directory_queue[0])
|
||||||
|
directory_queue.pop_front()
|
||||||
|
|
||||||
|
return scripts
|
||||||
|
|
||||||
|
|
||||||
|
func cache_todos(todos: Array, script_path: String) -> void:
|
||||||
|
var last_modified_time = FileAccess.get_modified_time(script_path)
|
||||||
|
todo_cache[script_path] = TodoCacheValue.new(todos, last_modified_time)
|
||||||
|
|
||||||
|
|
||||||
|
func get_cached_todos(script_path: String) -> Array:
|
||||||
|
if todo_cache.has(script_path) and !script_path.contains("tscn::"):
|
||||||
|
var cached_value: TodoCacheValue = todo_cache[script_path]
|
||||||
|
if cached_value.last_modified_time == FileAccess.get_modified_time(script_path):
|
||||||
|
|
||||||
|
return cached_value.todos
|
||||||
|
return []
|
||||||
|
|
||||||
|
func get_dir_contents(dir: DirAccess, scripts: Array[String], directory_queue: Array[String]) -> void:
|
||||||
|
dir.include_navigational = false
|
||||||
|
dir.include_hidden = false
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var file_name : String = dir.get_next()
|
||||||
|
|
||||||
|
while file_name != "":
|
||||||
|
if dir.current_is_dir():
|
||||||
|
if file_name == ".import" or file_name == ".mono": # Skip .import folder which should never have scripts
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
directory_queue.append(dir.get_current_dir().path_join(file_name))
|
||||||
|
else:
|
||||||
|
if file_name.ends_with(".gd") or file_name.ends_with(".cs") \
|
||||||
|
or file_name.ends_with(".c") or file_name.ends_with(".cpp") or file_name.ends_with(".h") \
|
||||||
|
or ((file_name.ends_with(".tscn") and _dockUI.builtin_enabled)):
|
||||||
|
scripts.append(dir.get_current_dir().path_join(file_name))
|
||||||
|
file_name = dir.get_next()
|
||||||
|
|
||||||
|
|
||||||
|
func rescan_files(clear_cache: bool) -> void:
|
||||||
|
_dockUI.todo_items.clear()
|
||||||
|
if clear_cache:
|
||||||
|
todo_cache.clear()
|
||||||
|
combined_pattern = combine_patterns(_dockUI.patterns)
|
||||||
|
find_tokens_from_path(find_scripts())
|
||||||
|
_dockUI.build_tree()
|
||||||
|
|
||||||
|
|
||||||
|
func combine_patterns(patterns: Array) -> String:
|
||||||
|
# Case Sensitivity
|
||||||
|
cased_patterns = []
|
||||||
|
for pattern in patterns:
|
||||||
|
if pattern[2] == _dockUI.CASE_INSENSITIVE:
|
||||||
|
cased_patterns.append(pattern[0].insert(0, "((?i)") + ")")
|
||||||
|
else:
|
||||||
|
cased_patterns.append("(" + pattern[0] + ")")
|
||||||
|
|
||||||
|
if patterns.size() == 1:
|
||||||
|
return cased_patterns[0]
|
||||||
|
else:
|
||||||
|
var pattern_string := "((\\/\\*)|(#|\\/\\/))\\s*("
|
||||||
|
for i in range(patterns.size()):
|
||||||
|
if i == 0:
|
||||||
|
pattern_string += cased_patterns[i]
|
||||||
|
else:
|
||||||
|
pattern_string += "|" + cased_patterns[i]
|
||||||
|
pattern_string += ")(?(2)[\\s\\S]*?\\*\\/|.*)"
|
||||||
|
return pattern_string
|
||||||
|
|
||||||
|
|
||||||
|
func create_todo(todo_string: String, script_path: String) -> Todo:
|
||||||
|
var todo := Todo.new()
|
||||||
|
var regex = RegEx.new()
|
||||||
|
for pattern in cased_patterns:
|
||||||
|
if regex.compile(pattern) == OK:
|
||||||
|
var result : RegExMatch = regex.search(todo_string)
|
||||||
|
if result:
|
||||||
|
todo.pattern = pattern
|
||||||
|
todo.title = result.strings[0]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
printerr("Error compiling " + pattern)
|
||||||
|
|
||||||
|
todo.content = todo_string
|
||||||
|
todo.script_path = script_path
|
||||||
|
return todo
|
||||||
|
|
||||||
|
|
||||||
|
func _on_active_script_changed(script) -> void:
|
||||||
|
if _dockUI:
|
||||||
|
if _dockUI.tabs.current_tab == 1:
|
||||||
|
_dockUI.build_tree()
|
15
Godot/addons/Todo_Manager/todo.cfg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[scripts]
|
||||||
|
|
||||||
|
full_path=false
|
||||||
|
sort_alphabetical=true
|
||||||
|
script_colour=Color(0.8, 0.807843, 0.827451, 1)
|
||||||
|
ignore_paths=Array[String]([])
|
||||||
|
|
||||||
|
[patterns]
|
||||||
|
|
||||||
|
patterns=[["\\bTODO\\b", Color(0.588235, 0.945098, 0.678431, 1), 0], ["\\bHACK\\b", Color(0.835294, 0.737255, 0.439216, 1), 0], ["\\bFIXME\\b", Color(0.835294, 0.439216, 0.439216, 1), 0]]
|
||||||
|
|
||||||
|
[config]
|
||||||
|
|
||||||
|
auto_refresh=true
|
||||||
|
builtin_enabled=false
|
18
Godot/addons/Todo_Manager/todoItem_class.gd
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@tool
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var script_path : String
|
||||||
|
var todos : Array
|
||||||
|
|
||||||
|
func _init(script_path: String, todos: Array):
|
||||||
|
self.script_path = script_path
|
||||||
|
self.todos = todos
|
||||||
|
|
||||||
|
func get_short_path() -> String:
|
||||||
|
var temp_array := script_path.rsplit('/', false, 1)
|
||||||
|
var short_path : String
|
||||||
|
if not temp_array.size() > 1:
|
||||||
|
short_path = "(!)" + temp_array[0]
|
||||||
|
else:
|
||||||
|
short_path = temp_array[1]
|
||||||
|
return short_path
|
9
Godot/addons/Todo_Manager/todo_class.gd
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
@tool
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
var pattern : String
|
||||||
|
var title : String
|
||||||
|
var content : String
|
||||||
|
var script_path : String
|
||||||
|
var line_number : int
|
119
Godot/addons/godot_rl_agents/controller/ai_controller_2d.gd
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
extends Node2D
|
||||||
|
class_name AIController2D
|
||||||
|
|
||||||
|
enum ControlModes { INHERIT_FROM_SYNC, HUMAN, TRAINING, ONNX_INFERENCE, RECORD_EXPERT_DEMOS }
|
||||||
|
@export var control_mode: ControlModes = ControlModes.INHERIT_FROM_SYNC
|
||||||
|
@export var onnx_model_path := ""
|
||||||
|
@export var reset_after := 1000
|
||||||
|
|
||||||
|
@export_group("Record expert demos mode options")
|
||||||
|
## Path where the demos will be saved. The file can later be used for imitation learning.
|
||||||
|
@export var expert_demo_save_path: String
|
||||||
|
## The action that erases the last recorded episode from the currently recorded data.
|
||||||
|
@export var remove_last_episode_key: InputEvent
|
||||||
|
## Action will be repeated for n frames. Will introduce control lag if larger than 1.
|
||||||
|
## Can be used to ensure that action_repeat on inference and training matches
|
||||||
|
## the recorded demonstrations.
|
||||||
|
@export var action_repeat: int = 1
|
||||||
|
|
||||||
|
@export_group("Multi-policy mode options")
|
||||||
|
## Allows you to set certain agents to use different policies.
|
||||||
|
## Changing has no effect with default SB3 training. Works with Rllib example.
|
||||||
|
## Tutorial: https://github.com/edbeeching/godot_rl_agents/blob/main/docs/TRAINING_MULTIPLE_POLICIES.md
|
||||||
|
@export var policy_name: String = "shared_policy"
|
||||||
|
|
||||||
|
var onnx_model: ONNXModel
|
||||||
|
|
||||||
|
var heuristic := "human"
|
||||||
|
var done := false
|
||||||
|
var reward := 0.0
|
||||||
|
var n_steps := 0
|
||||||
|
var needs_reset := false
|
||||||
|
|
||||||
|
var _player: Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
add_to_group("AGENT")
|
||||||
|
|
||||||
|
|
||||||
|
func init(player: Node2D):
|
||||||
|
_player = player
|
||||||
|
|
||||||
|
|
||||||
|
#-- Methods that need implementing using the "extend script" option in Godot --#
|
||||||
|
func get_obs() -> Dictionary:
|
||||||
|
assert(false, "the get_obs method is not implemented when extending from ai_controller")
|
||||||
|
return {"obs": []}
|
||||||
|
|
||||||
|
|
||||||
|
func get_reward() -> float:
|
||||||
|
assert(false, "the get_reward method is not implemented when extending from ai_controller")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func get_action_space() -> Dictionary:
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
"the get get_action_space method is not implemented when extending from ai_controller"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"example_actions_continous": {"size": 2, "action_type": "continuous"},
|
||||||
|
"example_actions_discrete": {"size": 2, "action_type": "discrete"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func set_action(action) -> void:
|
||||||
|
assert(false, "the set_action method is not implemented when extending from ai_controller")
|
||||||
|
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
|
||||||
|
#-- Methods that sometimes need implementing using the "extend script" option in Godot --#
|
||||||
|
# Only needed if you are recording expert demos with this AIController
|
||||||
|
func get_action() -> Array:
|
||||||
|
assert(false, "the get_action method is not implemented in extended AIController but demo_recorder is used")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
func _physics_process(delta):
|
||||||
|
n_steps += 1
|
||||||
|
if n_steps > reset_after:
|
||||||
|
needs_reset = true
|
||||||
|
|
||||||
|
|
||||||
|
func get_obs_space():
|
||||||
|
# may need overriding if the obs space is complex
|
||||||
|
var obs = get_obs()
|
||||||
|
return {
|
||||||
|
"obs": {"size": [len(obs["obs"])], "space": "box"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func reset():
|
||||||
|
n_steps = 0
|
||||||
|
needs_reset = false
|
||||||
|
|
||||||
|
|
||||||
|
func reset_if_done():
|
||||||
|
if done:
|
||||||
|
reset()
|
||||||
|
|
||||||
|
|
||||||
|
func set_heuristic(h):
|
||||||
|
# sets the heuristic from "human" or "model" nothing to change here
|
||||||
|
heuristic = h
|
||||||
|
|
||||||
|
|
||||||
|
func get_done():
|
||||||
|
return done
|
||||||
|
|
||||||
|
|
||||||
|
func set_done_false():
|
||||||
|
done = false
|
||||||
|
|
||||||
|
|
||||||
|
func zero_reward():
|
||||||
|
reward = 0.0
|
120
Godot/addons/godot_rl_agents/controller/ai_controller_3d.gd
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
extends Node3D
|
||||||
|
class_name AIController3D
|
||||||
|
|
||||||
|
enum ControlModes { INHERIT_FROM_SYNC, HUMAN, TRAINING, ONNX_INFERENCE, RECORD_EXPERT_DEMOS }
|
||||||
|
@export var control_mode: ControlModes = ControlModes.INHERIT_FROM_SYNC
|
||||||
|
@export var onnx_model_path := ""
|
||||||
|
@export var reset_after := 1000
|
||||||
|
|
||||||
|
@export_group("Record expert demos mode options")
|
||||||
|
## Path where the demos will be saved. The file can later be used for imitation learning.
|
||||||
|
@export var expert_demo_save_path: String
|
||||||
|
## The action that erases the last recorded episode from the currently recorded data.
|
||||||
|
@export var remove_last_episode_key: InputEvent
|
||||||
|
## Action will be repeated for n frames. Will introduce control lag if larger than 1.
|
||||||
|
## Can be used to ensure that action_repeat on inference and training matches
|
||||||
|
## the recorded demonstrations.
|
||||||
|
@export var action_repeat: int = 1
|
||||||
|
|
||||||
|
@export_group("Multi-policy mode options")
|
||||||
|
## Allows you to set certain agents to use different policies.
|
||||||
|
## Changing has no effect with default SB3 training. Works with Rllib example.
|
||||||
|
## Tutorial: https://github.com/edbeeching/godot_rl_agents/blob/main/docs/TRAINING_MULTIPLE_POLICIES.md
|
||||||
|
@export var policy_name: String = "shared_policy"
|
||||||
|
|
||||||
|
var onnx_model: ONNXModel
|
||||||
|
|
||||||
|
var heuristic := "human"
|
||||||
|
var done := false
|
||||||
|
var reward := 0.0
|
||||||
|
var n_steps := 0
|
||||||
|
var needs_reset := false
|
||||||
|
|
||||||
|
var _player: Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
add_to_group("AGENT")
|
||||||
|
|
||||||
|
|
||||||
|
func init(player: Node3D):
|
||||||
|
_player = player
|
||||||
|
|
||||||
|
|
||||||
|
#-- Methods that need implementing using the "extend script" option in Godot --#
|
||||||
|
func get_obs() -> Dictionary:
|
||||||
|
assert(false, "the get_obs method is not implemented when extending from ai_controller")
|
||||||
|
return {"obs": []}
|
||||||
|
|
||||||
|
|
||||||
|
func get_reward() -> float:
|
||||||
|
assert(false, "the get_reward method is not implemented when extending from ai_controller")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func get_action_space() -> Dictionary:
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
"the get_action_space method is not implemented when extending from ai_controller"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"example_actions_continous": {"size": 2, "action_type": "continuous"},
|
||||||
|
"example_actions_discrete": {"size": 2, "action_type": "discrete"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func set_action(action) -> void:
|
||||||
|
assert(false, "the set_action method is not implemented when extending from ai_controller")
|
||||||
|
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
|
||||||
|
#-- Methods that sometimes need implementing using the "extend script" option in Godot --#
|
||||||
|
# Only needed if you are recording expert demos with this AIController
|
||||||
|
func get_action() -> Array:
|
||||||
|
assert(false, "the get_action method is not implemented in extended AIController but demo_recorder is used")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta):
|
||||||
|
n_steps += 1
|
||||||
|
if n_steps > reset_after:
|
||||||
|
needs_reset = true
|
||||||
|
|
||||||
|
|
||||||
|
func get_obs_space():
|
||||||
|
# may need overriding if the obs space is complex
|
||||||
|
var obs = get_obs()
|
||||||
|
return {
|
||||||
|
"obs": {"size": [len(obs["obs"])], "space": "box"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func reset():
|
||||||
|
n_steps = 0
|
||||||
|
needs_reset = false
|
||||||
|
|
||||||
|
|
||||||
|
func reset_if_done():
|
||||||
|
if done:
|
||||||
|
reset()
|
||||||
|
|
||||||
|
|
||||||
|
func set_heuristic(h):
|
||||||
|
# sets the heuristic from "human" or "model" nothing to change here
|
||||||
|
heuristic = h
|
||||||
|
|
||||||
|
|
||||||
|
func get_done():
|
||||||
|
return done
|
||||||
|
|
||||||
|
|
||||||
|
func set_done_false():
|
||||||
|
done = false
|
||||||
|
|
||||||
|
|
||||||
|
func zero_reward():
|
||||||
|
reward = 0.0
|
16
Godot/addons/godot_rl_agents/godot_rl_agents.gd
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree():
|
||||||
|
# Initialization of the plugin goes here.
|
||||||
|
# Add the new type with a name, a parent type, a script and an icon.
|
||||||
|
add_custom_type("Sync", "Node", preload("sync.gd"), preload("icon.png"))
|
||||||
|
#add_custom_type("RaycastSensor2D2", "Node", preload("raycast_sensor_2d.gd"), preload("icon.png"))
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
# Clean-up of the plugin goes here.
|
||||||
|
# Always remember to remove it from the engine when deactivated.
|
||||||
|
remove_custom_type("Sync")
|
||||||
|
#remove_custom_type("RaycastSensor2D2")
|
BIN
Godot/addons/godot_rl_agents/icon.png
Normal file
After Width: | Height: | Size: 198 B |
109
Godot/addons/godot_rl_agents/onnx/csharp/ONNXInference.cs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
using Godot;
|
||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace GodotONNX
|
||||||
|
{
|
||||||
|
/// <include file='docs/ONNXInference.xml' path='docs/members[@name="ONNXInference"]/ONNXInference/*'/>
|
||||||
|
public partial class ONNXInference : GodotObject
|
||||||
|
{
|
||||||
|
|
||||||
|
private InferenceSession session;
|
||||||
|
/// <summary>
|
||||||
|
/// Path to the ONNX model. Use Initialize to change it.
|
||||||
|
/// </summary>
|
||||||
|
private string modelPath;
|
||||||
|
private int batchSize;
|
||||||
|
|
||||||
|
private SessionOptions SessionOpt;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// init function
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Path"></param>
|
||||||
|
/// <param name="BatchSize"></param>
|
||||||
|
/// <returns>Returns the output size of the model</returns>
|
||||||
|
public int Initialize(string Path, int BatchSize)
|
||||||
|
{
|
||||||
|
modelPath = Path;
|
||||||
|
batchSize = BatchSize;
|
||||||
|
SessionOpt = SessionConfigurator.MakeConfiguredSessionOptions();
|
||||||
|
session = LoadModel(modelPath);
|
||||||
|
return session.OutputMetadata["output"].Dimensions[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <include file='docs/ONNXInference.xml' path='docs/members[@name="ONNXInference"]/Run/*'/>
|
||||||
|
public Godot.Collections.Dictionary<string, Godot.Collections.Array<float>> RunInference(Godot.Collections.Array<float> obs, int state_ins)
|
||||||
|
{
|
||||||
|
//Current model: Any (Godot Rl Agents)
|
||||||
|
//Expects a tensor of shape [batch_size, input_size] type float named obs and a tensor of shape [batch_size] type float named state_ins
|
||||||
|
|
||||||
|
//Fill the input tensors
|
||||||
|
// create span from inputSize
|
||||||
|
var span = new float[obs.Count]; //There's probably a better way to do this
|
||||||
|
for (int i = 0; i < obs.Count; i++)
|
||||||
|
{
|
||||||
|
span[i] = obs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyCollection<NamedOnnxValue> inputs = new List<NamedOnnxValue>
|
||||||
|
{
|
||||||
|
NamedOnnxValue.CreateFromTensor("obs", new DenseTensor<float>(span, new int[] { batchSize, obs.Count })),
|
||||||
|
NamedOnnxValue.CreateFromTensor("state_ins", new DenseTensor<float>(new float[] { state_ins }, new int[] { batchSize }))
|
||||||
|
};
|
||||||
|
IReadOnlyCollection<string> outputNames = new List<string> { "output", "state_outs" }; //ONNX is sensible to these names, as well as the input names
|
||||||
|
|
||||||
|
IDisposableReadOnlyCollection<DisposableNamedOnnxValue> results;
|
||||||
|
//We do not use "using" here so we get a better exception explaination later
|
||||||
|
try
|
||||||
|
{
|
||||||
|
results = session.Run(inputs, outputNames);
|
||||||
|
}
|
||||||
|
catch (OnnxRuntimeException e)
|
||||||
|
{
|
||||||
|
//This error usually means that the model is not compatible with the input, beacause of the input shape (size)
|
||||||
|
GD.Print("Error at inference: ", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//Can't convert IEnumerable<float> to Variant, so we have to convert it to an array or something
|
||||||
|
Godot.Collections.Dictionary<string, Godot.Collections.Array<float>> output = new Godot.Collections.Dictionary<string, Godot.Collections.Array<float>>();
|
||||||
|
DisposableNamedOnnxValue output1 = results.First();
|
||||||
|
DisposableNamedOnnxValue output2 = results.Last();
|
||||||
|
Godot.Collections.Array<float> output1Array = new Godot.Collections.Array<float>();
|
||||||
|
Godot.Collections.Array<float> output2Array = new Godot.Collections.Array<float>();
|
||||||
|
|
||||||
|
foreach (float f in output1.AsEnumerable<float>())
|
||||||
|
{
|
||||||
|
output1Array.Add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (float f in output2.AsEnumerable<float>())
|
||||||
|
{
|
||||||
|
output2Array.Add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Add(output1.Name, output1Array);
|
||||||
|
output.Add(output2.Name, output2Array);
|
||||||
|
|
||||||
|
//Output is a dictionary of arrays, ex: { "output" : [0.1, 0.2, 0.3, 0.4, ...], "state_outs" : [0.5, ...]}
|
||||||
|
results.Dispose();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
/// <include file='docs/ONNXInference.xml' path='docs/members[@name="ONNXInference"]/Load/*'/>
|
||||||
|
public InferenceSession LoadModel(string Path)
|
||||||
|
{
|
||||||
|
using Godot.FileAccess file = FileAccess.Open(Path, Godot.FileAccess.ModeFlags.Read);
|
||||||
|
byte[] model = file.GetBuffer((int)file.GetLength());
|
||||||
|
//file.Close(); file.Dispose(); //Close the file, then dispose the reference.
|
||||||
|
return new InferenceSession(model, SessionOpt); //Load the model
|
||||||
|
}
|
||||||
|
public void FreeDisposables()
|
||||||
|
{
|
||||||
|
session.Dispose();
|
||||||
|
SessionOpt.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
131
Godot/addons/godot_rl_agents/onnx/csharp/SessionConfigurator.cs
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
using Godot;
|
||||||
|
using Microsoft.ML.OnnxRuntime;
|
||||||
|
|
||||||
|
namespace GodotONNX
|
||||||
|
{
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/SessionConfigurator/*'/>
|
||||||
|
|
||||||
|
public static class SessionConfigurator
|
||||||
|
{
|
||||||
|
public enum ComputeName
|
||||||
|
{
|
||||||
|
CUDA,
|
||||||
|
ROCm,
|
||||||
|
DirectML,
|
||||||
|
CoreML,
|
||||||
|
CPU
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/GetSessionOptions/*'/>
|
||||||
|
public static SessionOptions MakeConfiguredSessionOptions()
|
||||||
|
{
|
||||||
|
SessionOptions sessionOptions = new();
|
||||||
|
SetOptions(sessionOptions);
|
||||||
|
return sessionOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetOptions(SessionOptions sessionOptions)
|
||||||
|
{
|
||||||
|
sessionOptions.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING;
|
||||||
|
ApplySystemSpecificOptions(sessionOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/SystemCheck/*'/>
|
||||||
|
static public void ApplySystemSpecificOptions(SessionOptions sessionOptions)
|
||||||
|
{
|
||||||
|
//Most code for this function is verbose only, the only reason it exists is to track
|
||||||
|
//implementation progress of the different compute APIs.
|
||||||
|
|
||||||
|
//December 2022: CUDA is not working.
|
||||||
|
|
||||||
|
string OSName = OS.GetName(); //Get OS Name
|
||||||
|
|
||||||
|
//ComputeName ComputeAPI = ComputeCheck(); //Get Compute API
|
||||||
|
// //TODO: Get CPU architecture
|
||||||
|
|
||||||
|
//Linux can use OpenVINO (C#) on x64 and ROCm on x86 (GDNative/C++)
|
||||||
|
//Windows can use OpenVINO (C#) on x64
|
||||||
|
//TODO: try TensorRT instead of CUDA
|
||||||
|
//TODO: Use OpenVINO for Intel Graphics
|
||||||
|
|
||||||
|
// Temporarily using CPU on all platforms to avoid errors detected with DML
|
||||||
|
ComputeName ComputeAPI = ComputeName.CPU;
|
||||||
|
|
||||||
|
//match OS and Compute API
|
||||||
|
GD.Print($"OS: {OSName} Compute API: {ComputeAPI}");
|
||||||
|
|
||||||
|
// CPU is set by default without appending necessary
|
||||||
|
// sessionOptions.AppendExecutionProvider_CPU(0);
|
||||||
|
|
||||||
|
/*
|
||||||
|
switch (OSName)
|
||||||
|
{
|
||||||
|
case "Windows": //Can use CUDA, DirectML
|
||||||
|
if (ComputeAPI is ComputeName.CUDA)
|
||||||
|
{
|
||||||
|
//CUDA
|
||||||
|
//sessionOptions.AppendExecutionProvider_CUDA(0);
|
||||||
|
//sessionOptions.AppendExecutionProvider_DML(0);
|
||||||
|
}
|
||||||
|
else if (ComputeAPI is ComputeName.DirectML)
|
||||||
|
{
|
||||||
|
//DirectML
|
||||||
|
//sessionOptions.AppendExecutionProvider_DML(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "X11": //Can use CUDA, ROCm
|
||||||
|
if (ComputeAPI is ComputeName.CUDA)
|
||||||
|
{
|
||||||
|
//CUDA
|
||||||
|
//sessionOptions.AppendExecutionProvider_CUDA(0);
|
||||||
|
}
|
||||||
|
if (ComputeAPI is ComputeName.ROCm)
|
||||||
|
{
|
||||||
|
//ROCm, only works on x86
|
||||||
|
//Research indicates that this has to be compiled as a GDNative plugin
|
||||||
|
//GD.Print("ROCm not supported yet, using CPU.");
|
||||||
|
//sessionOptions.AppendExecutionProvider_CPU(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "macOS": //Can use CoreML
|
||||||
|
if (ComputeAPI is ComputeName.CoreML)
|
||||||
|
{ //CoreML
|
||||||
|
//TODO: Needs testing
|
||||||
|
//sessionOptions.AppendExecutionProvider_CoreML(0);
|
||||||
|
//CoreML on ARM64, out of the box, on x64 needs .tar file from GitHub
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
GD.Print("OS not Supported.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <include file='docs/SessionConfigurator.xml' path='docs/members[@name="SessionConfigurator"]/ComputeCheck/*'/>
|
||||||
|
public static ComputeName ComputeCheck()
|
||||||
|
{
|
||||||
|
string adapterName = Godot.RenderingServer.GetVideoAdapterName();
|
||||||
|
//string adapterVendor = Godot.RenderingServer.GetVideoAdapterVendor();
|
||||||
|
adapterName = adapterName.ToUpper(new System.Globalization.CultureInfo(""));
|
||||||
|
//TODO: GPU vendors for MacOS, what do they even use these days?
|
||||||
|
|
||||||
|
if (adapterName.Contains("INTEL"))
|
||||||
|
{
|
||||||
|
return ComputeName.DirectML;
|
||||||
|
}
|
||||||
|
if (adapterName.Contains("AMD") || adapterName.Contains("RADEON"))
|
||||||
|
{
|
||||||
|
return ComputeName.DirectML;
|
||||||
|
}
|
||||||
|
if (adapterName.Contains("NVIDIA"))
|
||||||
|
{
|
||||||
|
return ComputeName.CUDA;
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print("Graphics Card not recognized."); //Should use CPU
|
||||||
|
return ComputeName.CPU;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<docs>
|
||||||
|
<members name="ONNXInference">
|
||||||
|
<ONNXInference>
|
||||||
|
<summary>
|
||||||
|
The main <c>ONNXInference</c> Class that handles the inference process.
|
||||||
|
</summary>
|
||||||
|
</ONNXInference>
|
||||||
|
<Initialize>
|
||||||
|
<summary>
|
||||||
|
Starts the inference process.
|
||||||
|
</summary>
|
||||||
|
<param name="Path">Path to the ONNX model, expects a path inside resources.</param>
|
||||||
|
<param name="BatchSize">How many observations will the model recieve.</param>
|
||||||
|
</Initialize>
|
||||||
|
<Run>
|
||||||
|
<summary>
|
||||||
|
Runs the given input through the model and returns the output.
|
||||||
|
</summary>
|
||||||
|
<param name="obs">Dictionary containing all observations.</param>
|
||||||
|
<param name="state_ins">How many different agents are creating these observations.</param>
|
||||||
|
<returns>A Dictionary of arrays, containing instructions based on the observations.</returns>
|
||||||
|
</Run>
|
||||||
|
<Load>
|
||||||
|
<summary>
|
||||||
|
Loads the given model into the inference process, using the best Execution provider available.
|
||||||
|
</summary>
|
||||||
|
<param name="Path">Path to the ONNX model, expects a path inside resources.</param>
|
||||||
|
<returns>InferenceSession ready to run.</returns>
|
||||||
|
</Load>
|
||||||
|
</members>
|
||||||
|
</docs>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<docs>
|
||||||
|
<members name="SessionConfigurator">
|
||||||
|
<SessionConfigurator>
|
||||||
|
<summary>
|
||||||
|
The main <c>SessionConfigurator</c> Class that handles the execution options and providers for the inference process.
|
||||||
|
</summary>
|
||||||
|
</SessionConfigurator>
|
||||||
|
<GetSessionOptions>
|
||||||
|
<summary>
|
||||||
|
Creates a SessionOptions with all available execution providers.
|
||||||
|
</summary>
|
||||||
|
<returns>SessionOptions with all available execution providers.</returns>
|
||||||
|
</GetSessionOptions>
|
||||||
|
<SystemCheck>
|
||||||
|
<summary>
|
||||||
|
Appends any execution provider available in the current system.
|
||||||
|
</summary>
|
||||||
|
<remarks>
|
||||||
|
This function is mainly verbose for tracking implementation progress of different compute APIs.
|
||||||
|
</remarks>
|
||||||
|
</SystemCheck>
|
||||||
|
<ComputeCheck>
|
||||||
|
<summary>
|
||||||
|
Checks for available GPUs.
|
||||||
|
</summary>
|
||||||
|
<returns>An integer identifier for each compute platform.</returns>
|
||||||
|
</ComputeCheck>
|
||||||
|
</members>
|
||||||
|
</docs>
|
51
Godot/addons/godot_rl_agents/onnx/wrapper/ONNX_wrapper.gd
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
extends Resource
|
||||||
|
class_name ONNXModel
|
||||||
|
var inferencer_script = load("res://addons/godot_rl_agents/onnx/csharp/ONNXInference.cs")
|
||||||
|
|
||||||
|
var inferencer = null
|
||||||
|
|
||||||
|
## How many action values the model outputs
|
||||||
|
var action_output_size: int
|
||||||
|
|
||||||
|
## Used to differentiate models
|
||||||
|
## that only output continuous action mean (e.g. sb3, cleanrl export)
|
||||||
|
## versus models that output mean and logstd (e.g. rllib export)
|
||||||
|
var action_means_only: bool
|
||||||
|
|
||||||
|
## Whether action_means_value has been set already for this model
|
||||||
|
var action_means_only_set: bool
|
||||||
|
|
||||||
|
# Must provide the path to the model and the batch size
|
||||||
|
func _init(model_path, batch_size):
|
||||||
|
inferencer = inferencer_script.new()
|
||||||
|
action_output_size = inferencer.Initialize(model_path, batch_size)
|
||||||
|
|
||||||
|
# This function is the one that will be called from the game,
|
||||||
|
# requires the observation as an array and the state_ins as an int
|
||||||
|
# returns an Array containing the action the model takes.
|
||||||
|
func run_inference(obs: Array, state_ins: int) -> Dictionary:
|
||||||
|
if inferencer == null:
|
||||||
|
printerr("Inferencer not initialized")
|
||||||
|
return {}
|
||||||
|
return inferencer.RunInference(obs, state_ins)
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what):
|
||||||
|
if what == NOTIFICATION_PREDELETE:
|
||||||
|
inferencer.FreeDisposables()
|
||||||
|
inferencer.free()
|
||||||
|
|
||||||
|
# Check whether agent uses a continuous actions model with only action means or not
|
||||||
|
func set_action_means_only(agent_action_space):
|
||||||
|
action_means_only_set = true
|
||||||
|
var continuous_only: bool = true
|
||||||
|
var continuous_actions: int
|
||||||
|
for action in agent_action_space:
|
||||||
|
if not agent_action_space[action]["action_type"] == "continuous":
|
||||||
|
continuous_only = false
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continuous_actions += agent_action_space[action]["size"]
|
||||||
|
if continuous_only:
|
||||||
|
if continuous_actions == action_output_size:
|
||||||
|
action_means_only = true
|
7
Godot/addons/godot_rl_agents/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="GodotRLAgents"
|
||||||
|
description="Custom nodes for the godot rl agents toolkit "
|
||||||
|
author="Edward Beeching"
|
||||||
|
version="0.1"
|
||||||
|
script="godot_rl_agents.gd"
|
|
@ -0,0 +1,48 @@
|
||||||
|
[gd_scene load_steps=5 format=3 uid="uid://ddeq7mn1ealyc"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"]
|
||||||
|
|
||||||
|
[sub_resource type="GDScript" id="2"]
|
||||||
|
script/source = "extends Node2D
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
print(\"step start\")
|
||||||
|
|
||||||
|
"
|
||||||
|
|
||||||
|
[sub_resource type="GDScript" id="1"]
|
||||||
|
script/source = "extends RayCast2D
|
||||||
|
|
||||||
|
var steps = 1
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
print(\"processing raycast\")
|
||||||
|
steps += 1
|
||||||
|
if steps % 2:
|
||||||
|
force_raycast_update()
|
||||||
|
|
||||||
|
print(is_colliding())
|
||||||
|
"
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="3"]
|
||||||
|
|
||||||
|
[node name="ExampleRaycastSensor2D" type="Node2D"]
|
||||||
|
script = SubResource("2")
|
||||||
|
|
||||||
|
[node name="ExampleAgent" type="Node2D" parent="."]
|
||||||
|
position = Vector2(573, 314)
|
||||||
|
rotation = 0.286234
|
||||||
|
|
||||||
|
[node name="RaycastSensor2D" type="Node2D" parent="ExampleAgent"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="TestRayCast2D" type="RayCast2D" parent="."]
|
||||||
|
script = SubResource("1")
|
||||||
|
|
||||||
|
[node name="StaticBody2D" type="StaticBody2D" parent="."]
|
||||||
|
position = Vector2(1, 52)
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D"]
|
||||||
|
shape = SubResource("3")
|
235
Godot/addons/godot_rl_agents/sensors/sensors_2d/GridSensor2D.gd
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
@tool
|
||||||
|
extends ISensor2D
|
||||||
|
class_name GridSensor2D
|
||||||
|
|
||||||
|
@export var debug_view := false:
|
||||||
|
get:
|
||||||
|
return debug_view
|
||||||
|
set(value):
|
||||||
|
debug_view = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_flags_2d_physics var detection_mask := 0:
|
||||||
|
get:
|
||||||
|
return detection_mask
|
||||||
|
set(value):
|
||||||
|
detection_mask = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_areas := false:
|
||||||
|
get:
|
||||||
|
return collide_with_areas
|
||||||
|
set(value):
|
||||||
|
collide_with_areas = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_bodies := true:
|
||||||
|
get:
|
||||||
|
return collide_with_bodies
|
||||||
|
set(value):
|
||||||
|
collide_with_bodies = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(1, 200, 0.1) var cell_width := 20.0:
|
||||||
|
get:
|
||||||
|
return cell_width
|
||||||
|
set(value):
|
||||||
|
cell_width = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(1, 200, 0.1) var cell_height := 20.0:
|
||||||
|
get:
|
||||||
|
return cell_height
|
||||||
|
set(value):
|
||||||
|
cell_height = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(1, 21, 2, "or_greater") var grid_size_x := 3:
|
||||||
|
get:
|
||||||
|
return grid_size_x
|
||||||
|
set(value):
|
||||||
|
grid_size_x = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(1, 21, 2, "or_greater") var grid_size_y := 3:
|
||||||
|
get:
|
||||||
|
return grid_size_y
|
||||||
|
set(value):
|
||||||
|
grid_size_y = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
var _obs_buffer: PackedFloat64Array
|
||||||
|
var _rectangle_shape: RectangleShape2D
|
||||||
|
var _collision_mapping: Dictionary
|
||||||
|
var _n_layers_per_cell: int
|
||||||
|
|
||||||
|
var _highlighted_cell_color: Color
|
||||||
|
var _standard_cell_color: Color
|
||||||
|
|
||||||
|
|
||||||
|
func get_observation():
|
||||||
|
return _obs_buffer
|
||||||
|
|
||||||
|
|
||||||
|
func _update():
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
if is_node_ready():
|
||||||
|
_spawn_nodes()
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_set_colors()
|
||||||
|
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
if get_child_count() == 0:
|
||||||
|
_spawn_nodes()
|
||||||
|
else:
|
||||||
|
_spawn_nodes()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_colors() -> void:
|
||||||
|
_standard_cell_color = Color(100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0)
|
||||||
|
_highlighted_cell_color = Color(255.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_collision_mapping() -> Dictionary:
|
||||||
|
# defines which layer is mapped to which cell obs index
|
||||||
|
var total_bits = 0
|
||||||
|
var collision_mapping = {}
|
||||||
|
for i in 32:
|
||||||
|
var bit_mask = 2 ** i
|
||||||
|
if (detection_mask & bit_mask) > 0:
|
||||||
|
collision_mapping[i] = total_bits
|
||||||
|
total_bits += 1
|
||||||
|
|
||||||
|
return collision_mapping
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_nodes():
|
||||||
|
for cell in get_children():
|
||||||
|
cell.name = "_%s" % cell.name # Otherwise naming below will fail
|
||||||
|
cell.queue_free()
|
||||||
|
|
||||||
|
_collision_mapping = _get_collision_mapping()
|
||||||
|
#prints("collision_mapping", _collision_mapping, len(_collision_mapping))
|
||||||
|
# allocate memory for the observations
|
||||||
|
_n_layers_per_cell = len(_collision_mapping)
|
||||||
|
_obs_buffer = PackedFloat64Array()
|
||||||
|
_obs_buffer.resize(grid_size_x * grid_size_y * _n_layers_per_cell)
|
||||||
|
_obs_buffer.fill(0)
|
||||||
|
#prints(len(_obs_buffer), _obs_buffer )
|
||||||
|
|
||||||
|
_rectangle_shape = RectangleShape2D.new()
|
||||||
|
_rectangle_shape.set_size(Vector2(cell_width, cell_height))
|
||||||
|
|
||||||
|
var shift := Vector2(
|
||||||
|
-(grid_size_x / 2) * cell_width,
|
||||||
|
-(grid_size_y / 2) * cell_height,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in grid_size_x:
|
||||||
|
for j in grid_size_y:
|
||||||
|
var cell_position = Vector2(i * cell_width, j * cell_height) + shift
|
||||||
|
_create_cell(i, j, cell_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_cell(i: int, j: int, position: Vector2):
|
||||||
|
var cell := Area2D.new()
|
||||||
|
cell.position = position
|
||||||
|
cell.name = "GridCell %s %s" % [i, j]
|
||||||
|
cell.modulate = _standard_cell_color
|
||||||
|
|
||||||
|
if collide_with_areas:
|
||||||
|
cell.area_entered.connect(_on_cell_area_entered.bind(i, j))
|
||||||
|
cell.area_exited.connect(_on_cell_area_exited.bind(i, j))
|
||||||
|
|
||||||
|
if collide_with_bodies:
|
||||||
|
cell.body_entered.connect(_on_cell_body_entered.bind(i, j))
|
||||||
|
cell.body_exited.connect(_on_cell_body_exited.bind(i, j))
|
||||||
|
|
||||||
|
cell.collision_layer = 0
|
||||||
|
cell.collision_mask = detection_mask
|
||||||
|
cell.monitorable = true
|
||||||
|
add_child(cell)
|
||||||
|
cell.set_owner(get_tree().edited_scene_root)
|
||||||
|
|
||||||
|
var col_shape := CollisionShape2D.new()
|
||||||
|
col_shape.shape = _rectangle_shape
|
||||||
|
col_shape.name = "CollisionShape2D"
|
||||||
|
cell.add_child(col_shape)
|
||||||
|
col_shape.set_owner(get_tree().edited_scene_root)
|
||||||
|
|
||||||
|
if debug_view:
|
||||||
|
var quad = MeshInstance2D.new()
|
||||||
|
quad.name = "MeshInstance2D"
|
||||||
|
var quad_mesh = QuadMesh.new()
|
||||||
|
|
||||||
|
quad_mesh.set_size(Vector2(cell_width, cell_height))
|
||||||
|
|
||||||
|
quad.mesh = quad_mesh
|
||||||
|
cell.add_child(quad)
|
||||||
|
quad.set_owner(get_tree().edited_scene_root)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_obs(cell_i: int, cell_j: int, collision_layer: int, entered: bool):
|
||||||
|
for key in _collision_mapping:
|
||||||
|
var bit_mask = 2 ** key
|
||||||
|
if (collision_layer & bit_mask) > 0:
|
||||||
|
var collison_map_index = _collision_mapping[key]
|
||||||
|
|
||||||
|
var obs_index = (
|
||||||
|
(cell_i * grid_size_x * _n_layers_per_cell)
|
||||||
|
+ (cell_j * _n_layers_per_cell)
|
||||||
|
+ collison_map_index
|
||||||
|
)
|
||||||
|
#prints(obs_index, cell_i, cell_j)
|
||||||
|
if entered:
|
||||||
|
_obs_buffer[obs_index] += 1
|
||||||
|
else:
|
||||||
|
_obs_buffer[obs_index] -= 1
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_cell(cell_i: int, cell_j: int):
|
||||||
|
var cell = get_node_or_null("GridCell %s %s" % [cell_i, cell_j])
|
||||||
|
|
||||||
|
if cell == null:
|
||||||
|
print("cell not found, returning")
|
||||||
|
|
||||||
|
var n_hits = 0
|
||||||
|
var start_index = (cell_i * grid_size_x * _n_layers_per_cell) + (cell_j * _n_layers_per_cell)
|
||||||
|
for i in _n_layers_per_cell:
|
||||||
|
n_hits += _obs_buffer[start_index + i]
|
||||||
|
|
||||||
|
if n_hits > 0:
|
||||||
|
cell.modulate = _highlighted_cell_color
|
||||||
|
else:
|
||||||
|
cell.modulate = _standard_cell_color
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_area_entered(area: Area2D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_area_entered", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, area.collision_layer, true)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
||||||
|
#print(_obs_buffer)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_area_exited(area: Area2D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_area_exited", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, area.collision_layer, false)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_body_entered(body: Node2D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_body_entered", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, body.collision_layer, true)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_body_exited(body: Node2D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_body_exited", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, body.collision_layer, false)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
25
Godot/addons/godot_rl_agents/sensors/sensors_2d/ISensor2D.gd
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
extends Node2D
|
||||||
|
class_name ISensor2D
|
||||||
|
|
||||||
|
var _obs: Array = []
|
||||||
|
var _active := false
|
||||||
|
|
||||||
|
|
||||||
|
func get_observation():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func activate():
|
||||||
|
_active = true
|
||||||
|
|
||||||
|
|
||||||
|
func deactivate():
|
||||||
|
_active = false
|
||||||
|
|
||||||
|
|
||||||
|
func _update_observation():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func reset():
|
||||||
|
pass
|
|
@ -0,0 +1,118 @@
|
||||||
|
@tool
|
||||||
|
extends ISensor2D
|
||||||
|
class_name RaycastSensor2D
|
||||||
|
|
||||||
|
@export_flags_2d_physics var collision_mask := 1:
|
||||||
|
get:
|
||||||
|
return collision_mask
|
||||||
|
set(value):
|
||||||
|
collision_mask = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_areas := false:
|
||||||
|
get:
|
||||||
|
return collide_with_areas
|
||||||
|
set(value):
|
||||||
|
collide_with_areas = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_bodies := true:
|
||||||
|
get:
|
||||||
|
return collide_with_bodies
|
||||||
|
set(value):
|
||||||
|
collide_with_bodies = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var n_rays := 16.0:
|
||||||
|
get:
|
||||||
|
return n_rays
|
||||||
|
set(value):
|
||||||
|
n_rays = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(5, 3000, 5.0) var ray_length := 200:
|
||||||
|
get:
|
||||||
|
return ray_length
|
||||||
|
set(value):
|
||||||
|
ray_length = value
|
||||||
|
_update()
|
||||||
|
@export_range(5, 360, 5.0) var cone_width := 360.0:
|
||||||
|
get:
|
||||||
|
return cone_width
|
||||||
|
set(value):
|
||||||
|
cone_width = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var debug_draw := true:
|
||||||
|
get:
|
||||||
|
return debug_draw
|
||||||
|
set(value):
|
||||||
|
debug_draw = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
var _angles = []
|
||||||
|
var rays := []
|
||||||
|
|
||||||
|
|
||||||
|
func _update():
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
if debug_draw:
|
||||||
|
_spawn_nodes()
|
||||||
|
else:
|
||||||
|
for ray in get_children():
|
||||||
|
if ray is RayCast2D:
|
||||||
|
remove_child(ray)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_spawn_nodes()
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_nodes():
|
||||||
|
for ray in rays:
|
||||||
|
ray.queue_free()
|
||||||
|
rays = []
|
||||||
|
|
||||||
|
_angles = []
|
||||||
|
var step = cone_width / (n_rays)
|
||||||
|
var start = step / 2 - cone_width / 2
|
||||||
|
|
||||||
|
for i in n_rays:
|
||||||
|
var angle = start + i * step
|
||||||
|
var ray = RayCast2D.new()
|
||||||
|
ray.set_target_position(
|
||||||
|
Vector2(ray_length * cos(deg_to_rad(angle)), ray_length * sin(deg_to_rad(angle)))
|
||||||
|
)
|
||||||
|
ray.set_name("node_" + str(i))
|
||||||
|
ray.enabled = false
|
||||||
|
ray.collide_with_areas = collide_with_areas
|
||||||
|
ray.collide_with_bodies = collide_with_bodies
|
||||||
|
ray.collision_mask = collision_mask
|
||||||
|
add_child(ray)
|
||||||
|
rays.append(ray)
|
||||||
|
|
||||||
|
_angles.append(start + i * step)
|
||||||
|
|
||||||
|
|
||||||
|
func get_observation() -> Array:
|
||||||
|
return self.calculate_raycasts()
|
||||||
|
|
||||||
|
|
||||||
|
func calculate_raycasts() -> Array:
|
||||||
|
var result = []
|
||||||
|
for ray in rays:
|
||||||
|
ray.enabled = true
|
||||||
|
ray.force_raycast_update()
|
||||||
|
var distance = _get_raycast_distance(ray)
|
||||||
|
result.append(distance)
|
||||||
|
ray.enabled = false
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _get_raycast_distance(ray: RayCast2D) -> float:
|
||||||
|
if !ray.is_colliding():
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
var distance = (global_position - ray.get_collision_point()).length()
|
||||||
|
distance = clamp(distance, 0.0, ray_length)
|
||||||
|
return (ray_length - distance) / ray_length
|
|
@ -0,0 +1,7 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://drvfihk5esgmv"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_2d/RaycastSensor2D.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="RaycastSensor2D" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
n_rays = 17.0
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene format=3 uid="uid://biu787qh4woik"]
|
||||||
|
|
||||||
|
[node name="ExampleRaycastSensor3D" type="Node3D"]
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.804183, 0, 2.70146)
|
258
Godot/addons/godot_rl_agents/sensors/sensors_3d/GridSensor3D.gd
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
@tool
|
||||||
|
extends ISensor3D
|
||||||
|
class_name GridSensor3D
|
||||||
|
|
||||||
|
@export var debug_view := false:
|
||||||
|
get:
|
||||||
|
return debug_view
|
||||||
|
set(value):
|
||||||
|
debug_view = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_flags_3d_physics var detection_mask := 0:
|
||||||
|
get:
|
||||||
|
return detection_mask
|
||||||
|
set(value):
|
||||||
|
detection_mask = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_areas := false:
|
||||||
|
get:
|
||||||
|
return collide_with_areas
|
||||||
|
set(value):
|
||||||
|
collide_with_areas = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_bodies := false:
|
||||||
|
# NOTE! The sensor will not detect StaticBody3D, add an area to static bodies to detect them
|
||||||
|
get:
|
||||||
|
return collide_with_bodies
|
||||||
|
set(value):
|
||||||
|
collide_with_bodies = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(0.1, 2, 0.1) var cell_width := 1.0:
|
||||||
|
get:
|
||||||
|
return cell_width
|
||||||
|
set(value):
|
||||||
|
cell_width = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(0.1, 2, 0.1) var cell_height := 1.0:
|
||||||
|
get:
|
||||||
|
return cell_height
|
||||||
|
set(value):
|
||||||
|
cell_height = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(1, 21, 2, "or_greater") var grid_size_x := 3:
|
||||||
|
get:
|
||||||
|
return grid_size_x
|
||||||
|
set(value):
|
||||||
|
grid_size_x = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export_range(1, 21, 2, "or_greater") var grid_size_z := 3:
|
||||||
|
get:
|
||||||
|
return grid_size_z
|
||||||
|
set(value):
|
||||||
|
grid_size_z = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
var _obs_buffer: PackedFloat64Array
|
||||||
|
var _box_shape: BoxShape3D
|
||||||
|
var _collision_mapping: Dictionary
|
||||||
|
var _n_layers_per_cell: int
|
||||||
|
|
||||||
|
var _highlighted_box_material: StandardMaterial3D
|
||||||
|
var _standard_box_material: StandardMaterial3D
|
||||||
|
|
||||||
|
|
||||||
|
func get_observation():
|
||||||
|
return _obs_buffer
|
||||||
|
|
||||||
|
|
||||||
|
func reset():
|
||||||
|
_obs_buffer.fill(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _update():
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
if is_node_ready():
|
||||||
|
_spawn_nodes()
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_make_materials()
|
||||||
|
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
if get_child_count() == 0:
|
||||||
|
_spawn_nodes()
|
||||||
|
else:
|
||||||
|
_spawn_nodes()
|
||||||
|
|
||||||
|
|
||||||
|
func _make_materials() -> void:
|
||||||
|
if _highlighted_box_material != null and _standard_box_material != null:
|
||||||
|
return
|
||||||
|
|
||||||
|
_standard_box_material = StandardMaterial3D.new()
|
||||||
|
_standard_box_material.set_transparency(1) # ALPHA
|
||||||
|
_standard_box_material.albedo_color = Color(
|
||||||
|
100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0
|
||||||
|
)
|
||||||
|
|
||||||
|
_highlighted_box_material = StandardMaterial3D.new()
|
||||||
|
_highlighted_box_material.set_transparency(1) # ALPHA
|
||||||
|
_highlighted_box_material.albedo_color = Color(
|
||||||
|
255.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0, 100.0 / 255.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_collision_mapping() -> Dictionary:
|
||||||
|
# defines which layer is mapped to which cell obs index
|
||||||
|
var total_bits = 0
|
||||||
|
var collision_mapping = {}
|
||||||
|
for i in 32:
|
||||||
|
var bit_mask = 2 ** i
|
||||||
|
if (detection_mask & bit_mask) > 0:
|
||||||
|
collision_mapping[i] = total_bits
|
||||||
|
total_bits += 1
|
||||||
|
|
||||||
|
return collision_mapping
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_nodes():
|
||||||
|
for cell in get_children():
|
||||||
|
cell.name = "_%s" % cell.name # Otherwise naming below will fail
|
||||||
|
cell.queue_free()
|
||||||
|
|
||||||
|
_collision_mapping = _get_collision_mapping()
|
||||||
|
#prints("collision_mapping", _collision_mapping, len(_collision_mapping))
|
||||||
|
# allocate memory for the observations
|
||||||
|
_n_layers_per_cell = len(_collision_mapping)
|
||||||
|
_obs_buffer = PackedFloat64Array()
|
||||||
|
_obs_buffer.resize(grid_size_x * grid_size_z * _n_layers_per_cell)
|
||||||
|
_obs_buffer.fill(0)
|
||||||
|
#prints(len(_obs_buffer), _obs_buffer )
|
||||||
|
|
||||||
|
_box_shape = BoxShape3D.new()
|
||||||
|
_box_shape.set_size(Vector3(cell_width, cell_height, cell_width))
|
||||||
|
|
||||||
|
var shift := Vector3(
|
||||||
|
-(grid_size_x / 2) * cell_width,
|
||||||
|
0,
|
||||||
|
-(grid_size_z / 2) * cell_width,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in grid_size_x:
|
||||||
|
for j in grid_size_z:
|
||||||
|
var cell_position = Vector3(i * cell_width, 0.0, j * cell_width) + shift
|
||||||
|
_create_cell(i, j, cell_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_cell(i: int, j: int, position: Vector3):
|
||||||
|
var cell := Area3D.new()
|
||||||
|
cell.position = position
|
||||||
|
cell.name = "GridCell %s %s" % [i, j]
|
||||||
|
|
||||||
|
if collide_with_areas:
|
||||||
|
cell.area_entered.connect(_on_cell_area_entered.bind(i, j))
|
||||||
|
cell.area_exited.connect(_on_cell_area_exited.bind(i, j))
|
||||||
|
|
||||||
|
if collide_with_bodies:
|
||||||
|
cell.body_entered.connect(_on_cell_body_entered.bind(i, j))
|
||||||
|
cell.body_exited.connect(_on_cell_body_exited.bind(i, j))
|
||||||
|
|
||||||
|
# cell.body_shape_entered.connect(_on_cell_body_shape_entered.bind(i, j))
|
||||||
|
# cell.body_shape_exited.connect(_on_cell_body_shape_exited.bind(i, j))
|
||||||
|
|
||||||
|
cell.collision_layer = 0
|
||||||
|
cell.collision_mask = detection_mask
|
||||||
|
cell.monitorable = true
|
||||||
|
cell.input_ray_pickable = false
|
||||||
|
add_child(cell)
|
||||||
|
cell.set_owner(get_tree().edited_scene_root)
|
||||||
|
|
||||||
|
var col_shape := CollisionShape3D.new()
|
||||||
|
col_shape.shape = _box_shape
|
||||||
|
col_shape.name = "CollisionShape3D"
|
||||||
|
cell.add_child(col_shape)
|
||||||
|
col_shape.set_owner(get_tree().edited_scene_root)
|
||||||
|
|
||||||
|
if debug_view:
|
||||||
|
var box = MeshInstance3D.new()
|
||||||
|
box.name = "MeshInstance3D"
|
||||||
|
var box_mesh = BoxMesh.new()
|
||||||
|
|
||||||
|
box_mesh.set_size(Vector3(cell_width, cell_height, cell_width))
|
||||||
|
box_mesh.material = _standard_box_material
|
||||||
|
|
||||||
|
box.mesh = box_mesh
|
||||||
|
cell.add_child(box)
|
||||||
|
box.set_owner(get_tree().edited_scene_root)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_obs(cell_i: int, cell_j: int, collision_layer: int, entered: bool):
|
||||||
|
for key in _collision_mapping:
|
||||||
|
var bit_mask = 2 ** key
|
||||||
|
if (collision_layer & bit_mask) > 0:
|
||||||
|
var collison_map_index = _collision_mapping[key]
|
||||||
|
|
||||||
|
var obs_index = (
|
||||||
|
(cell_i * grid_size_x * _n_layers_per_cell)
|
||||||
|
+ (cell_j * _n_layers_per_cell)
|
||||||
|
+ collison_map_index
|
||||||
|
)
|
||||||
|
#prints(obs_index, cell_i, cell_j)
|
||||||
|
if entered:
|
||||||
|
_obs_buffer[obs_index] += 1
|
||||||
|
else:
|
||||||
|
_obs_buffer[obs_index] -= 1
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_cell(cell_i: int, cell_j: int):
|
||||||
|
var cell = get_node_or_null("GridCell %s %s" % [cell_i, cell_j])
|
||||||
|
|
||||||
|
if cell == null:
|
||||||
|
print("cell not found, returning")
|
||||||
|
|
||||||
|
var n_hits = 0
|
||||||
|
var start_index = (cell_i * grid_size_x * _n_layers_per_cell) + (cell_j * _n_layers_per_cell)
|
||||||
|
for i in _n_layers_per_cell:
|
||||||
|
n_hits += _obs_buffer[start_index + i]
|
||||||
|
|
||||||
|
var cell_mesh = cell.get_node_or_null("MeshInstance3D")
|
||||||
|
if n_hits > 0:
|
||||||
|
cell_mesh.mesh.material = _highlighted_box_material
|
||||||
|
else:
|
||||||
|
cell_mesh.mesh.material = _standard_box_material
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_area_entered(area: Area3D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_area_entered", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, area.collision_layer, true)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
||||||
|
#print(_obs_buffer)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_area_exited(area: Area3D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_area_exited", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, area.collision_layer, false)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_body_entered(body: Node3D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_body_entered", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, body.collision_layer, true)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_body_exited(body: Node3D, cell_i: int, cell_j: int):
|
||||||
|
#prints("_on_cell_body_exited", cell_i, cell_j)
|
||||||
|
_update_obs(cell_i, cell_j, body.collision_layer, false)
|
||||||
|
if debug_view:
|
||||||
|
_toggle_cell(cell_i, cell_j)
|
25
Godot/addons/godot_rl_agents/sensors/sensors_3d/ISensor3D.gd
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
extends Node3D
|
||||||
|
class_name ISensor3D
|
||||||
|
|
||||||
|
var _obs: Array = []
|
||||||
|
var _active := false
|
||||||
|
|
||||||
|
|
||||||
|
func get_observation():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func activate():
|
||||||
|
_active = true
|
||||||
|
|
||||||
|
|
||||||
|
func deactivate():
|
||||||
|
_active = false
|
||||||
|
|
||||||
|
|
||||||
|
func _update_observation():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func reset():
|
||||||
|
pass
|
|
@ -0,0 +1,21 @@
|
||||||
|
extends Node3D
|
||||||
|
class_name RGBCameraSensor3D
|
||||||
|
var camera_pixels = null
|
||||||
|
|
||||||
|
@onready var camera_texture := $Control/TextureRect/CameraTexture as Sprite2D
|
||||||
|
@onready var sub_viewport := $SubViewport as SubViewport
|
||||||
|
|
||||||
|
|
||||||
|
func get_camera_pixel_encoding():
|
||||||
|
return camera_texture.get_texture().get_image().get_data().hex_encode()
|
||||||
|
|
||||||
|
|
||||||
|
func get_camera_shape() -> Array:
|
||||||
|
assert(
|
||||||
|
sub_viewport.size.x >= 36 and sub_viewport.size.y >= 36,
|
||||||
|
"SubViewport size must be 36x36 or larger."
|
||||||
|
)
|
||||||
|
if sub_viewport.transparent_bg:
|
||||||
|
return [4, sub_viewport.size.y, sub_viewport.size.x]
|
||||||
|
else:
|
||||||
|
return [3, sub_viewport.size.y, sub_viewport.size.x]
|
|
@ -0,0 +1,41 @@
|
||||||
|
[gd_scene load_steps=3 format=3 uid="uid://baaywi3arsl2m"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_3d/RGBCameraSensor3D.gd" id="1"]
|
||||||
|
|
||||||
|
[sub_resource type="ViewportTexture" id="1"]
|
||||||
|
viewport_path = NodePath("SubViewport")
|
||||||
|
|
||||||
|
[node name="RGBCameraSensor3D" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="RemoteTransform3D" type="RemoteTransform3D" parent="."]
|
||||||
|
remote_path = NodePath("../SubViewport/Camera3D")
|
||||||
|
|
||||||
|
[node name="SubViewport" type="SubViewport" parent="."]
|
||||||
|
size = Vector2i(32, 32)
|
||||||
|
render_target_update_mode = 3
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="SubViewport"]
|
||||||
|
near = 0.5
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="."]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="TextureRect" type="ColorRect" parent="Control"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 1096.0
|
||||||
|
offset_top = 534.0
|
||||||
|
offset_right = 1114.0
|
||||||
|
offset_bottom = 552.0
|
||||||
|
scale = Vector2(10, 10)
|
||||||
|
color = Color(0.00784314, 0.00784314, 0.00784314, 1)
|
||||||
|
|
||||||
|
[node name="CameraTexture" type="Sprite2D" parent="Control/TextureRect"]
|
||||||
|
texture = SubResource("1")
|
||||||
|
offset = Vector2(9, 9)
|
||||||
|
flip_v = true
|
|
@ -0,0 +1,185 @@
|
||||||
|
@tool
|
||||||
|
extends ISensor3D
|
||||||
|
class_name RayCastSensor3D
|
||||||
|
@export_flags_3d_physics var collision_mask = 1:
|
||||||
|
get:
|
||||||
|
return collision_mask
|
||||||
|
set(value):
|
||||||
|
collision_mask = value
|
||||||
|
_update()
|
||||||
|
@export_flags_3d_physics var boolean_class_mask = 1:
|
||||||
|
get:
|
||||||
|
return boolean_class_mask
|
||||||
|
set(value):
|
||||||
|
boolean_class_mask = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var n_rays_width := 6.0:
|
||||||
|
get:
|
||||||
|
return n_rays_width
|
||||||
|
set(value):
|
||||||
|
n_rays_width = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var n_rays_height := 6.0:
|
||||||
|
get:
|
||||||
|
return n_rays_height
|
||||||
|
set(value):
|
||||||
|
n_rays_height = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var ray_length := 10.0:
|
||||||
|
get:
|
||||||
|
return ray_length
|
||||||
|
set(value):
|
||||||
|
ray_length = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var cone_width := 60.0:
|
||||||
|
get:
|
||||||
|
return cone_width
|
||||||
|
set(value):
|
||||||
|
cone_width = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var cone_height := 60.0:
|
||||||
|
get:
|
||||||
|
return cone_height
|
||||||
|
set(value):
|
||||||
|
cone_height = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_areas := false:
|
||||||
|
get:
|
||||||
|
return collide_with_areas
|
||||||
|
set(value):
|
||||||
|
collide_with_areas = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var collide_with_bodies := true:
|
||||||
|
get:
|
||||||
|
return collide_with_bodies
|
||||||
|
set(value):
|
||||||
|
collide_with_bodies = value
|
||||||
|
_update()
|
||||||
|
|
||||||
|
@export var class_sensor := false
|
||||||
|
|
||||||
|
var rays := []
|
||||||
|
var geo = null
|
||||||
|
|
||||||
|
|
||||||
|
func _update():
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
if is_node_ready():
|
||||||
|
_spawn_nodes()
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
if get_child_count() == 0:
|
||||||
|
_spawn_nodes()
|
||||||
|
else:
|
||||||
|
_spawn_nodes()
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_nodes():
|
||||||
|
print("spawning nodes")
|
||||||
|
for ray in get_children():
|
||||||
|
ray.queue_free()
|
||||||
|
if geo:
|
||||||
|
geo.clear()
|
||||||
|
#$Lines.remove_points()
|
||||||
|
rays = []
|
||||||
|
|
||||||
|
var horizontal_step = cone_width / (n_rays_width)
|
||||||
|
var vertical_step = cone_height / (n_rays_height)
|
||||||
|
|
||||||
|
var horizontal_start = horizontal_step / 2 - cone_width / 2
|
||||||
|
var vertical_start = vertical_step / 2 - cone_height / 2
|
||||||
|
|
||||||
|
var points = []
|
||||||
|
|
||||||
|
for i in n_rays_width:
|
||||||
|
for j in n_rays_height:
|
||||||
|
var angle_w = horizontal_start + i * horizontal_step
|
||||||
|
var angle_h = vertical_start + j * vertical_step
|
||||||
|
#angle_h = 0.0
|
||||||
|
var ray = RayCast3D.new()
|
||||||
|
var cast_to = to_spherical_coords(ray_length, angle_w, angle_h)
|
||||||
|
ray.set_target_position(cast_to)
|
||||||
|
|
||||||
|
points.append(cast_to)
|
||||||
|
|
||||||
|
ray.set_name("node_" + str(i) + " " + str(j))
|
||||||
|
ray.enabled = true
|
||||||
|
ray.collide_with_bodies = collide_with_bodies
|
||||||
|
ray.collide_with_areas = collide_with_areas
|
||||||
|
ray.collision_mask = collision_mask
|
||||||
|
add_child(ray)
|
||||||
|
ray.set_owner(get_tree().edited_scene_root)
|
||||||
|
rays.append(ray)
|
||||||
|
ray.force_raycast_update()
|
||||||
|
|
||||||
|
|
||||||
|
# if Engine.editor_hint:
|
||||||
|
# _create_debug_lines(points)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_debug_lines(points):
|
||||||
|
if not geo:
|
||||||
|
geo = ImmediateMesh.new()
|
||||||
|
add_child(geo)
|
||||||
|
|
||||||
|
geo.clear()
|
||||||
|
geo.begin(Mesh.PRIMITIVE_LINES)
|
||||||
|
for point in points:
|
||||||
|
geo.set_color(Color.AQUA)
|
||||||
|
geo.add_vertex(Vector3.ZERO)
|
||||||
|
geo.add_vertex(point)
|
||||||
|
geo.end()
|
||||||
|
|
||||||
|
|
||||||
|
func display():
|
||||||
|
if geo:
|
||||||
|
geo.display()
|
||||||
|
|
||||||
|
|
||||||
|
func to_spherical_coords(r, inc, azimuth) -> Vector3:
|
||||||
|
return Vector3(
|
||||||
|
r * sin(deg_to_rad(inc)) * cos(deg_to_rad(azimuth)),
|
||||||
|
r * sin(deg_to_rad(azimuth)),
|
||||||
|
r * cos(deg_to_rad(inc)) * cos(deg_to_rad(azimuth))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_observation() -> Array:
|
||||||
|
return self.calculate_raycasts()
|
||||||
|
|
||||||
|
|
||||||
|
func calculate_raycasts() -> Array:
|
||||||
|
var result = []
|
||||||
|
for ray in rays:
|
||||||
|
ray.set_enabled(true)
|
||||||
|
ray.force_raycast_update()
|
||||||
|
var distance = _get_raycast_distance(ray)
|
||||||
|
|
||||||
|
result.append(distance)
|
||||||
|
if class_sensor:
|
||||||
|
var hit_class: float = 0
|
||||||
|
if ray.get_collider():
|
||||||
|
var hit_collision_layer = ray.get_collider().collision_layer
|
||||||
|
hit_collision_layer = hit_collision_layer & collision_mask
|
||||||
|
hit_class = (hit_collision_layer & boolean_class_mask) > 0
|
||||||
|
result.append(float(hit_class))
|
||||||
|
ray.set_enabled(false)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _get_raycast_distance(ray: RayCast3D) -> float:
|
||||||
|
if !ray.is_colliding():
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
var distance = (global_transform.origin - ray.get_collision_point()).length()
|
||||||
|
distance = clamp(distance, 0.0, ray_length)
|
||||||
|
return (ray_length - distance) / ray_length
|
|
@ -0,0 +1,27 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://b803cbh1fmy66"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/godot_rl_agents/sensors/sensors_3d/RaycastSensor3D.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="RaycastSensor3D" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
n_rays_width = 4.0
|
||||||
|
n_rays_height = 2.0
|
||||||
|
ray_length = 11.0
|
||||||
|
|
||||||
|
[node name="node_1 0" type="RayCast3D" parent="."]
|
||||||
|
target_position = Vector3(-1.38686, -2.84701, 10.5343)
|
||||||
|
|
||||||
|
[node name="node_1 1" type="RayCast3D" parent="."]
|
||||||
|
target_position = Vector3(-1.38686, 2.84701, 10.5343)
|
||||||
|
|
||||||
|
[node name="node_2 0" type="RayCast3D" parent="."]
|
||||||
|
target_position = Vector3(1.38686, -2.84701, 10.5343)
|
||||||
|
|
||||||
|
[node name="node_2 1" type="RayCast3D" parent="."]
|
||||||
|
target_position = Vector3(1.38686, 2.84701, 10.5343)
|
||||||
|
|
||||||
|
[node name="node_3 0" type="RayCast3D" parent="."]
|
||||||
|
target_position = Vector3(4.06608, -2.84701, 9.81639)
|
||||||
|
|
||||||
|
[node name="node_3 1" type="RayCast3D" parent="."]
|
||||||
|
target_position = Vector3(4.06608, 2.84701, 9.81639)
|
579
Godot/addons/godot_rl_agents/sync.gd
Normal file
|
@ -0,0 +1,579 @@
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# --fixed-fps 2000 --disable-render-loop
|
||||||
|
|
||||||
|
enum ControlModes { HUMAN, TRAINING, ONNX_INFERENCE }
|
||||||
|
@export var control_mode: ControlModes = ControlModes.TRAINING
|
||||||
|
@export_range(1, 10, 1, "or_greater") var action_repeat := 8
|
||||||
|
@export_range(0, 10, 0.1, "or_greater") var speed_up := 1.0
|
||||||
|
@export var onnx_model_path := ""
|
||||||
|
|
||||||
|
# Onnx model stored for each requested path
|
||||||
|
var onnx_models: Dictionary
|
||||||
|
|
||||||
|
@onready var start_time = Time.get_ticks_msec()
|
||||||
|
|
||||||
|
const MAJOR_VERSION := "0"
|
||||||
|
const MINOR_VERSION := "7"
|
||||||
|
const DEFAULT_PORT := "11008"
|
||||||
|
const DEFAULT_SEED := "1"
|
||||||
|
var stream: StreamPeerTCP = null
|
||||||
|
var connected = false
|
||||||
|
var message_center
|
||||||
|
var should_connect = true
|
||||||
|
|
||||||
|
var all_agents: Array
|
||||||
|
var agents_training: Array
|
||||||
|
## Policy name of each agent, for use with multi-policy multi-agent RL cases
|
||||||
|
var agents_training_policy_names: Array[String] = ["shared_policy"]
|
||||||
|
var agents_inference: Array
|
||||||
|
var agents_heuristic: Array
|
||||||
|
|
||||||
|
## For recording expert demos
|
||||||
|
var agent_demo_record: Node
|
||||||
|
## File path for writing recorded trajectories
|
||||||
|
var expert_demo_save_path: String
|
||||||
|
## Stores recorded trajectories
|
||||||
|
var demo_trajectories: Array
|
||||||
|
## A trajectory includes obs: Array, acts: Array, terminal (set in Python env instead)
|
||||||
|
var current_demo_trajectory: Array
|
||||||
|
|
||||||
|
var need_to_send_obs = false
|
||||||
|
var args = null
|
||||||
|
var initialized = false
|
||||||
|
var just_reset = false
|
||||||
|
var onnx_model = null
|
||||||
|
var n_action_steps = 0
|
||||||
|
|
||||||
|
var _action_space_training: Array[Dictionary] = []
|
||||||
|
var _action_space_inference: Array[Dictionary] = []
|
||||||
|
var _obs_space_training: Array[Dictionary] = []
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
func _ready():
|
||||||
|
await get_tree().root.ready
|
||||||
|
get_tree().set_pause(true)
|
||||||
|
_initialize()
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
get_tree().set_pause(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize():
|
||||||
|
_get_agents()
|
||||||
|
args = _get_args()
|
||||||
|
Engine.physics_ticks_per_second = _get_speedup() * 60 # Replace with function body.
|
||||||
|
Engine.time_scale = _get_speedup() * 1.0
|
||||||
|
prints(
|
||||||
|
"physics ticks",
|
||||||
|
Engine.physics_ticks_per_second,
|
||||||
|
Engine.time_scale,
|
||||||
|
_get_speedup(),
|
||||||
|
speed_up
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_heuristic("human", all_agents)
|
||||||
|
|
||||||
|
_initialize_training_agents()
|
||||||
|
_initialize_inference_agents()
|
||||||
|
_initialize_demo_recording()
|
||||||
|
|
||||||
|
_set_seed()
|
||||||
|
_set_action_repeat()
|
||||||
|
initialized = true
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_training_agents():
|
||||||
|
if agents_training.size() > 0:
|
||||||
|
_obs_space_training.resize(agents_training.size())
|
||||||
|
_action_space_training.resize(agents_training.size())
|
||||||
|
for agent_idx in range(0, agents_training.size()):
|
||||||
|
_obs_space_training[agent_idx] = agents_training[agent_idx].get_obs_space()
|
||||||
|
_action_space_training[agent_idx] = agents_training[agent_idx].get_action_space()
|
||||||
|
connected = connect_to_server()
|
||||||
|
if connected:
|
||||||
|
_set_heuristic("model", agents_training)
|
||||||
|
_handshake()
|
||||||
|
_send_env_info()
|
||||||
|
else:
|
||||||
|
push_warning(
|
||||||
|
"Couldn't connect to Python server, using human controls instead. ",
|
||||||
|
"Did you start the training server using e.g. `gdrl` from the console?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_inference_agents():
|
||||||
|
if agents_inference.size() > 0:
|
||||||
|
if control_mode == ControlModes.ONNX_INFERENCE:
|
||||||
|
assert(
|
||||||
|
FileAccess.file_exists(onnx_model_path),
|
||||||
|
"Onnx Model Path set on Sync node does not exist: %s" % onnx_model_path
|
||||||
|
)
|
||||||
|
onnx_models[onnx_model_path] = ONNXModel.new(onnx_model_path, 1)
|
||||||
|
|
||||||
|
for agent in agents_inference:
|
||||||
|
var action_space = agent.get_action_space()
|
||||||
|
_action_space_inference.append(action_space)
|
||||||
|
|
||||||
|
var agent_onnx_model: ONNXModel
|
||||||
|
if agent.onnx_model_path.is_empty():
|
||||||
|
assert(
|
||||||
|
onnx_models.has(onnx_model_path),
|
||||||
|
(
|
||||||
|
"Node %s has no onnx model path set " % agent.get_path()
|
||||||
|
+ "and sync node's control mode is not set to OnnxInference. "
|
||||||
|
+ "Either add the path to the AIController, "
|
||||||
|
+ "or if you want to use the path set on sync node instead, "
|
||||||
|
+ "set control mode to OnnxInference."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
prints(
|
||||||
|
"Info: AIController %s" % agent.get_path(),
|
||||||
|
"has no onnx model path set.",
|
||||||
|
"Using path set on the sync node instead."
|
||||||
|
)
|
||||||
|
agent_onnx_model = onnx_models[onnx_model_path]
|
||||||
|
else:
|
||||||
|
if not onnx_models.has(agent.onnx_model_path):
|
||||||
|
assert(
|
||||||
|
FileAccess.file_exists(agent.onnx_model_path),
|
||||||
|
(
|
||||||
|
"Onnx Model Path set on %s node does not exist: %s"
|
||||||
|
% [agent.get_path(), agent.onnx_model_path]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onnx_models[agent.onnx_model_path] = ONNXModel.new(agent.onnx_model_path, 1)
|
||||||
|
agent_onnx_model = onnx_models[agent.onnx_model_path]
|
||||||
|
|
||||||
|
agent.onnx_model = agent_onnx_model
|
||||||
|
if not agent_onnx_model.action_means_only_set:
|
||||||
|
agent_onnx_model.set_action_means_only(action_space)
|
||||||
|
|
||||||
|
_set_heuristic("model", agents_inference)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_demo_recording():
|
||||||
|
if agent_demo_record:
|
||||||
|
expert_demo_save_path = agent_demo_record.expert_demo_save_path
|
||||||
|
assert(
|
||||||
|
not expert_demo_save_path.is_empty(),
|
||||||
|
"Expert demo save path set in %s is empty." % agent_demo_record.get_path()
|
||||||
|
)
|
||||||
|
|
||||||
|
InputMap.add_action("RemoveLastDemoEpisode")
|
||||||
|
InputMap.action_add_event(
|
||||||
|
"RemoveLastDemoEpisode", agent_demo_record.remove_last_episode_key
|
||||||
|
)
|
||||||
|
current_demo_trajectory.resize(2)
|
||||||
|
current_demo_trajectory[0] = []
|
||||||
|
current_demo_trajectory[1] = []
|
||||||
|
agent_demo_record.heuristic = "demo_record"
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(_delta):
|
||||||
|
# two modes, human control, agent control
|
||||||
|
# pause tree, send obs, get actions, set actions, unpause tree
|
||||||
|
|
||||||
|
_demo_record_process()
|
||||||
|
|
||||||
|
if n_action_steps % action_repeat != 0:
|
||||||
|
n_action_steps += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
n_action_steps += 1
|
||||||
|
|
||||||
|
_training_process()
|
||||||
|
_inference_process()
|
||||||
|
_heuristic_process()
|
||||||
|
|
||||||
|
|
||||||
|
func _training_process():
|
||||||
|
if connected:
|
||||||
|
get_tree().set_pause(true)
|
||||||
|
|
||||||
|
if just_reset:
|
||||||
|
just_reset = false
|
||||||
|
var obs = _get_obs_from_agents(agents_training)
|
||||||
|
|
||||||
|
var reply = {"type": "reset", "obs": obs}
|
||||||
|
_send_dict_as_json_message(reply)
|
||||||
|
# this should go straight to getting the action and setting it checked the agent, no need to perform one phyics tick
|
||||||
|
get_tree().set_pause(false)
|
||||||
|
return
|
||||||
|
|
||||||
|
if need_to_send_obs:
|
||||||
|
need_to_send_obs = false
|
||||||
|
var reward = _get_reward_from_agents()
|
||||||
|
var done = _get_done_from_agents()
|
||||||
|
#_reset_agents_if_done() # this ensures the new observation is from the next env instance : NEEDS REFACTOR
|
||||||
|
|
||||||
|
var obs = _get_obs_from_agents(agents_training)
|
||||||
|
|
||||||
|
var reply = {"type": "step", "obs": obs, "reward": reward, "done": done}
|
||||||
|
_send_dict_as_json_message(reply)
|
||||||
|
|
||||||
|
var handled = handle_message()
|
||||||
|
|
||||||
|
|
||||||
|
func _inference_process():
|
||||||
|
if agents_inference.size() > 0:
|
||||||
|
var obs: Array = _get_obs_from_agents(agents_inference)
|
||||||
|
var actions = []
|
||||||
|
|
||||||
|
for agent_id in range(0, agents_inference.size()):
|
||||||
|
var model: ONNXModel = agents_inference[agent_id].onnx_model
|
||||||
|
var action = model.run_inference(
|
||||||
|
obs[agent_id]["obs"], 1.0
|
||||||
|
)
|
||||||
|
var action_dict = _extract_action_dict(
|
||||||
|
action["output"], _action_space_inference[agent_id], model.action_means_only
|
||||||
|
)
|
||||||
|
actions.append(action_dict)
|
||||||
|
|
||||||
|
_set_agent_actions(actions, agents_inference)
|
||||||
|
_reset_agents_if_done(agents_inference)
|
||||||
|
get_tree().set_pause(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _demo_record_process():
|
||||||
|
if not agent_demo_record:
|
||||||
|
return
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("RemoveLastDemoEpisode"):
|
||||||
|
print("[Sync script][Demo recorder] Removing last recorded episode.")
|
||||||
|
demo_trajectories.remove_at(demo_trajectories.size() - 1)
|
||||||
|
print("Remaining episode count: %d" % demo_trajectories.size())
|
||||||
|
|
||||||
|
if n_action_steps % agent_demo_record.action_repeat != 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var obs_dict: Dictionary = agent_demo_record.get_obs()
|
||||||
|
|
||||||
|
# Get the current obs from the agent
|
||||||
|
assert(
|
||||||
|
obs_dict.has("obs"),
|
||||||
|
"Demo recorder needs an 'obs' key in get_obs() returned dictionary to record obs from."
|
||||||
|
)
|
||||||
|
current_demo_trajectory[0].append(obs_dict.obs)
|
||||||
|
|
||||||
|
# Get the action applied for the current obs from the agent
|
||||||
|
agent_demo_record.set_action()
|
||||||
|
var acts = agent_demo_record.get_action()
|
||||||
|
|
||||||
|
var terminal = agent_demo_record.get_done()
|
||||||
|
# Record actions only for non-terminal states
|
||||||
|
if terminal:
|
||||||
|
agent_demo_record.set_done_false()
|
||||||
|
else:
|
||||||
|
current_demo_trajectory[1].append(acts)
|
||||||
|
|
||||||
|
if terminal:
|
||||||
|
#current_demo_trajectory[2].append(true)
|
||||||
|
demo_trajectories.append(current_demo_trajectory.duplicate(true))
|
||||||
|
print("[Sync script][Demo recorder] Recorded episode count: %d" % demo_trajectories.size())
|
||||||
|
current_demo_trajectory[0].clear()
|
||||||
|
current_demo_trajectory[1].clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _heuristic_process():
|
||||||
|
for agent in agents_heuristic:
|
||||||
|
_reset_agents_if_done(agents_heuristic)
|
||||||
|
|
||||||
|
|
||||||
|
func _extract_action_dict(action_array: Array, action_space: Dictionary, action_means_only: bool):
|
||||||
|
var index = 0
|
||||||
|
var result = {}
|
||||||
|
for key in action_space.keys():
|
||||||
|
var size = action_space[key]["size"]
|
||||||
|
var action_type = action_space[key]["action_type"]
|
||||||
|
if action_type == "discrete":
|
||||||
|
var largest_logit: float # Value of the largest logit for this action in the actions array
|
||||||
|
var largest_logit_idx: int # Index of the largest logit for this action in the actions array
|
||||||
|
for logit_idx in range(0, size):
|
||||||
|
var logit_value = action_array[index + logit_idx]
|
||||||
|
if logit_value > largest_logit:
|
||||||
|
largest_logit = logit_value
|
||||||
|
largest_logit_idx = logit_idx
|
||||||
|
result[key] = largest_logit_idx # Index of the largest logit is the discrete action value
|
||||||
|
index += size
|
||||||
|
elif action_type == "continuous":
|
||||||
|
# For continous actions, we only take the action mean values
|
||||||
|
result[key] = clamp_array(action_array.slice(index, index + size), -1.0, 1.0)
|
||||||
|
if action_means_only:
|
||||||
|
index += size # model only outputs action means, so we move index by size
|
||||||
|
else:
|
||||||
|
index += size * 2 # model outputs logstd after action mean, we skip the logstd part
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert(false, 'Only "discrete" and "continuous" action types supported. Found: %s action type set.' % action_type)
|
||||||
|
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## For AIControllers that inherit mode from sync, sets the correct mode.
|
||||||
|
func _set_agent_mode(agent: Node):
|
||||||
|
var agent_inherits_mode: bool = agent.control_mode == agent.ControlModes.INHERIT_FROM_SYNC
|
||||||
|
|
||||||
|
if agent_inherits_mode:
|
||||||
|
match control_mode:
|
||||||
|
ControlModes.HUMAN:
|
||||||
|
agent.control_mode = agent.ControlModes.HUMAN
|
||||||
|
ControlModes.TRAINING:
|
||||||
|
agent.control_mode = agent.ControlModes.TRAINING
|
||||||
|
ControlModes.ONNX_INFERENCE:
|
||||||
|
agent.control_mode = agent.ControlModes.ONNX_INFERENCE
|
||||||
|
|
||||||
|
|
||||||
|
func _get_agents():
|
||||||
|
all_agents = get_tree().get_nodes_in_group("AGENT")
|
||||||
|
for agent in all_agents:
|
||||||
|
_set_agent_mode(agent)
|
||||||
|
|
||||||
|
if agent.control_mode == agent.ControlModes.TRAINING:
|
||||||
|
agents_training.append(agent)
|
||||||
|
elif agent.control_mode == agent.ControlModes.ONNX_INFERENCE:
|
||||||
|
agents_inference.append(agent)
|
||||||
|
elif agent.control_mode == agent.ControlModes.HUMAN:
|
||||||
|
agents_heuristic.append(agent)
|
||||||
|
elif agent.control_mode == agent.ControlModes.RECORD_EXPERT_DEMOS:
|
||||||
|
assert(
|
||||||
|
not agent_demo_record,
|
||||||
|
"Currently only a single AIController can be used for recording expert demos."
|
||||||
|
)
|
||||||
|
agent_demo_record = agent
|
||||||
|
|
||||||
|
var training_agent_count = agents_training.size()
|
||||||
|
agents_training_policy_names.resize(training_agent_count)
|
||||||
|
for i in range(0, training_agent_count):
|
||||||
|
agents_training_policy_names[i] = agents_training[i].policy_name
|
||||||
|
|
||||||
|
|
||||||
|
func _set_heuristic(heuristic, agents: Array):
|
||||||
|
for agent in agents:
|
||||||
|
agent.set_heuristic(heuristic)
|
||||||
|
|
||||||
|
|
||||||
|
func _handshake():
|
||||||
|
print("performing handshake")
|
||||||
|
|
||||||
|
var json_dict = _get_dict_json_message()
|
||||||
|
assert(json_dict["type"] == "handshake")
|
||||||
|
var major_version = json_dict["major_version"]
|
||||||
|
var minor_version = json_dict["minor_version"]
|
||||||
|
if major_version != MAJOR_VERSION:
|
||||||
|
print("WARNING: major verison mismatch ", major_version, " ", MAJOR_VERSION)
|
||||||
|
if minor_version != MINOR_VERSION:
|
||||||
|
print("WARNING: minor verison mismatch ", minor_version, " ", MINOR_VERSION)
|
||||||
|
|
||||||
|
print("handshake complete")
|
||||||
|
|
||||||
|
|
||||||
|
func _get_dict_json_message():
|
||||||
|
# returns a dictionary from of the most recent message
|
||||||
|
# this is not waiting
|
||||||
|
while stream.get_available_bytes() == 0:
|
||||||
|
stream.poll()
|
||||||
|
if stream.get_status() != 2:
|
||||||
|
print("server disconnected status, closing")
|
||||||
|
get_tree().quit()
|
||||||
|
return null
|
||||||
|
|
||||||
|
OS.delay_usec(10)
|
||||||
|
|
||||||
|
var message = stream.get_string()
|
||||||
|
var json_data = JSON.parse_string(message)
|
||||||
|
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
|
||||||
|
func _send_dict_as_json_message(dict):
|
||||||
|
stream.put_string(JSON.stringify(dict, "", false))
|
||||||
|
|
||||||
|
|
||||||
|
func _send_env_info():
|
||||||
|
var json_dict = _get_dict_json_message()
|
||||||
|
assert(json_dict["type"] == "env_info")
|
||||||
|
|
||||||
|
var message = {
|
||||||
|
"type": "env_info",
|
||||||
|
"observation_space": _obs_space_training,
|
||||||
|
"action_space": _action_space_training,
|
||||||
|
"n_agents": len(agents_training),
|
||||||
|
"agent_policy_names": agents_training_policy_names
|
||||||
|
}
|
||||||
|
_send_dict_as_json_message(message)
|
||||||
|
|
||||||
|
|
||||||
|
func connect_to_server():
|
||||||
|
print("Waiting for one second to allow server to start")
|
||||||
|
OS.delay_msec(1000)
|
||||||
|
print("trying to connect to server")
|
||||||
|
stream = StreamPeerTCP.new()
|
||||||
|
|
||||||
|
# "localhost" was not working on windows VM, had to use the IP
|
||||||
|
var ip = "127.0.0.1"
|
||||||
|
var port = _get_port()
|
||||||
|
var connect = stream.connect_to_host(ip, port)
|
||||||
|
stream.set_no_delay(true) # TODO check if this improves performance or not
|
||||||
|
stream.poll()
|
||||||
|
# Fetch the status until it is either connected (2) or failed to connect (3)
|
||||||
|
while stream.get_status() < 2:
|
||||||
|
stream.poll()
|
||||||
|
return stream.get_status() == 2
|
||||||
|
|
||||||
|
|
||||||
|
func _get_args():
|
||||||
|
print("getting command line arguments")
|
||||||
|
var arguments = {}
|
||||||
|
for argument in OS.get_cmdline_args():
|
||||||
|
print(argument)
|
||||||
|
if argument.find("=") > -1:
|
||||||
|
var key_value = argument.split("=")
|
||||||
|
arguments[key_value[0].lstrip("--")] = key_value[1]
|
||||||
|
else:
|
||||||
|
# Options without an argument will be present in the dictionary,
|
||||||
|
# with the value set to an empty string.
|
||||||
|
arguments[argument.lstrip("--")] = ""
|
||||||
|
|
||||||
|
return arguments
|
||||||
|
|
||||||
|
|
||||||
|
func _get_speedup():
|
||||||
|
print(args)
|
||||||
|
return args.get("speedup", str(speed_up)).to_float()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_port():
|
||||||
|
return args.get("port", DEFAULT_PORT).to_int()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_seed():
|
||||||
|
var _seed = args.get("env_seed", DEFAULT_SEED).to_int()
|
||||||
|
seed(_seed)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_action_repeat():
|
||||||
|
action_repeat = args.get("action_repeat", str(action_repeat)).to_int()
|
||||||
|
|
||||||
|
|
||||||
|
func disconnect_from_server():
|
||||||
|
stream.disconnect_from_host()
|
||||||
|
|
||||||
|
|
||||||
|
func handle_message() -> bool:
|
||||||
|
# get json message: reset, step, close
|
||||||
|
var message = _get_dict_json_message()
|
||||||
|
if message["type"] == "close":
|
||||||
|
print("received close message, closing game")
|
||||||
|
get_tree().quit()
|
||||||
|
get_tree().set_pause(false)
|
||||||
|
return true
|
||||||
|
|
||||||
|
if message["type"] == "reset":
|
||||||
|
print("resetting all agents")
|
||||||
|
_reset_agents()
|
||||||
|
just_reset = true
|
||||||
|
get_tree().set_pause(false)
|
||||||
|
#print("resetting forcing draw")
|
||||||
|
# RenderingServer.force_draw()
|
||||||
|
# var obs = _get_obs_from_agents()
|
||||||
|
# print("obs ", obs)
|
||||||
|
# var reply = {
|
||||||
|
# "type": "reset",
|
||||||
|
# "obs": obs
|
||||||
|
# }
|
||||||
|
# _send_dict_as_json_message(reply)
|
||||||
|
return true
|
||||||
|
|
||||||
|
if message["type"] == "call":
|
||||||
|
var method = message["method"]
|
||||||
|
var returns = _call_method_on_agents(method)
|
||||||
|
var reply = {"type": "call", "returns": returns}
|
||||||
|
print("calling method from Python")
|
||||||
|
_send_dict_as_json_message(reply)
|
||||||
|
return handle_message()
|
||||||
|
|
||||||
|
if message["type"] == "action":
|
||||||
|
var action = message["action"]
|
||||||
|
_set_agent_actions(action, agents_training)
|
||||||
|
need_to_send_obs = true
|
||||||
|
get_tree().set_pause(false)
|
||||||
|
return true
|
||||||
|
|
||||||
|
print("message was not handled")
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _call_method_on_agents(method):
|
||||||
|
var returns = []
|
||||||
|
for agent in all_agents:
|
||||||
|
returns.append(agent.call(method))
|
||||||
|
|
||||||
|
return returns
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_agents_if_done(agents = all_agents):
|
||||||
|
for agent in agents:
|
||||||
|
if agent.get_done():
|
||||||
|
agent.set_done_false()
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_agents(agents = all_agents):
|
||||||
|
for agent in agents:
|
||||||
|
agent.needs_reset = true
|
||||||
|
#agent.reset()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_obs_from_agents(agents: Array = all_agents):
|
||||||
|
var obs = []
|
||||||
|
for agent in agents:
|
||||||
|
obs.append(agent.get_obs())
|
||||||
|
return obs
|
||||||
|
|
||||||
|
|
||||||
|
func _get_reward_from_agents(agents: Array = agents_training):
|
||||||
|
var rewards = []
|
||||||
|
for agent in agents:
|
||||||
|
rewards.append(agent.get_reward())
|
||||||
|
agent.zero_reward()
|
||||||
|
return rewards
|
||||||
|
|
||||||
|
|
||||||
|
func _get_done_from_agents(agents: Array = agents_training):
|
||||||
|
var dones = []
|
||||||
|
for agent in agents:
|
||||||
|
var done = agent.get_done()
|
||||||
|
if done:
|
||||||
|
agent.set_done_false()
|
||||||
|
dones.append(done)
|
||||||
|
return dones
|
||||||
|
|
||||||
|
|
||||||
|
func _set_agent_actions(actions, agents: Array = all_agents):
|
||||||
|
for i in range(len(actions)):
|
||||||
|
agents[i].set_action(actions[i])
|
||||||
|
|
||||||
|
|
||||||
|
func clamp_array(arr: Array, min: float, max: float):
|
||||||
|
var output: Array = []
|
||||||
|
for a in arr:
|
||||||
|
output.append(clamp(a, min, max))
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
## Save recorded export demos on window exit (Close game window instead of "Stop" button in Godot Editor)
|
||||||
|
func _notification(what):
|
||||||
|
if demo_trajectories.size() == 0 or expert_demo_save_path.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
if what == NOTIFICATION_PREDELETE:
|
||||||
|
var json_string = JSON.stringify(demo_trajectories, "", false)
|
||||||
|
var file = FileAccess.open(expert_demo_save_path, FileAccess.WRITE)
|
||||||
|
|
||||||
|
if not file:
|
||||||
|
var error: Error = FileAccess.get_open_error()
|
||||||
|
assert(not error, "There was an error opening the file: %d" % error)
|
||||||
|
|
||||||
|
file.store_line(json_string)
|
||||||
|
var error = file.get_error()
|
||||||
|
assert(not error, "There was an error after trying to write to the file: %d" % error)
|
109
Godot/args.py
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='Pneuma',
|
||||||
|
allow_abbrev=False,
|
||||||
|
description='A Reinforcement Learning platform made with Godot',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--env_path",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="The Godot binary to use, do not include for in editor training",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exper_dir",
|
||||||
|
default="logs/sb3",
|
||||||
|
type=str,
|
||||||
|
help="The name of the experiment directory, in which the tensorboard logs and checkpoints (if enabled) are "
|
||||||
|
"getting stored.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exper_name",
|
||||||
|
default="experiment",
|
||||||
|
type=str,
|
||||||
|
help="The name of the experiment, which will be displayed in tensorboard and "
|
||||||
|
"for checkpoint directory and name (if enabled).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--seed",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="seed of the experiment"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--resume_model_path",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="The path to a model file previously saved using --save_model_path or a checkpoint saved using "
|
||||||
|
"--save_checkpoints_frequency. Use this to resume training or infer from a saved model.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--save_model_path",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="The path to use for saving the trained sb3 model after training is complete. Saved model can be used later "
|
||||||
|
"to resume training. Extension will be set to .zip",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--save_checkpoint_frequency",
|
||||||
|
default=None,
|
||||||
|
type=int,
|
||||||
|
help=(
|
||||||
|
"If set, will save checkpoints every 'frequency' environment steps. "
|
||||||
|
"Requires a unique --experiment_name or --experiment_dir for each run. "
|
||||||
|
"Does not need --save_model_path to be set. "
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--onnx_export_path",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="If included, will export onnx file after training to the path specified.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timesteps",
|
||||||
|
default=1_000_000,
|
||||||
|
type=int,
|
||||||
|
help="The number of environment steps to train for, default is 1_000_000. If resuming from a saved model, "
|
||||||
|
"it will continue training for this amount of steps from the saved state without counting previously trained "
|
||||||
|
"steps",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--inference",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Instead of training, it will run inference on a loaded model for --timesteps steps. "
|
||||||
|
"Requires --resume_model_path to be set.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--linear_lr_schedule",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Use a linear LR schedule for training. If set, learning rate will decrease until it reaches 0 at "
|
||||||
|
"--timesteps"
|
||||||
|
"value. Note: On resuming training, the schedule will reset. If disabled, constant LR will be used.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--viz",
|
||||||
|
action="store_true",
|
||||||
|
help="If set, the simulation will be displayed in a window during training. Otherwise "
|
||||||
|
"training will run without rendering the simulation. This setting does not apply to in-editor training.",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--speedup",
|
||||||
|
default=1,
|
||||||
|
type=int,
|
||||||
|
help="Whether to speed up the physics in the env"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--n_parallel",
|
||||||
|
default=1,
|
||||||
|
type=int,
|
||||||
|
help="How many instances of the environment executable to " "launch - requires --env_path to be set if > 1.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_known_args()
|
BIN
Godot/assets/audio/Fire.wav
Normal file
BIN
Godot/assets/audio/attack/claw.wav
Normal file
BIN
Godot/assets/audio/attack/fireball.wav
Normal file
BIN
Godot/assets/audio/attack/slash.wav
Normal file
BIN
Godot/assets/audio/death.wav
Normal file
BIN
Godot/assets/audio/heal.wav
Normal file
BIN
Godot/assets/audio/hit.wav
Normal file
BIN
Godot/assets/audio/main.ogg
Normal file
BIN
Godot/assets/audio/sword.wav
Normal file
BIN
Godot/assets/graphics/font/joystix.ttf
Normal file
BIN
Godot/assets/graphics/grass/grass_1.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
Godot/assets/graphics/grass/grass_2.png
Normal file
After Width: | Height: | Size: 500 B |
BIN
Godot/assets/graphics/grass/grass_3.png
Normal file
After Width: | Height: | Size: 575 B |
BIN
Godot/assets/graphics/monsters/bamboo/attack/0.png
Normal file
After Width: | Height: | Size: 487 B |
BIN
Godot/assets/graphics/monsters/bamboo/idle/0.png
Normal file
After Width: | Height: | Size: 487 B |
BIN
Godot/assets/graphics/monsters/bamboo/idle/1.png
Normal file
After Width: | Height: | Size: 517 B |
BIN
Godot/assets/graphics/monsters/bamboo/idle/2.png
Normal file
After Width: | Height: | Size: 487 B |
BIN
Godot/assets/graphics/monsters/bamboo/idle/3.png
Normal file
After Width: | Height: | Size: 504 B |
BIN
Godot/assets/graphics/monsters/bamboo/move/0.png
Normal file
After Width: | Height: | Size: 487 B |
BIN
Godot/assets/graphics/monsters/bamboo/move/1.png
Normal file
After Width: | Height: | Size: 517 B |
BIN
Godot/assets/graphics/monsters/bamboo/move/2.png
Normal file
After Width: | Height: | Size: 487 B |
BIN
Godot/assets/graphics/monsters/bamboo/move/3.png
Normal file
After Width: | Height: | Size: 504 B |
BIN
Godot/assets/graphics/monsters/raccoon/attack/0.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/attack/1.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/attack/2.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/attack/3.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/idle/0.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/idle/1.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/idle/2.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/idle/3.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/idle/4.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/idle/5.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/move/0.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/move/1.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/move/2.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/move/3.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Godot/assets/graphics/monsters/raccoon/move/4.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Godot/assets/graphics/monsters/spirit/attack/0.png
Normal file
After Width: | Height: | Size: 426 B |
BIN
Godot/assets/graphics/monsters/spirit/idle/0.png
Normal file
After Width: | Height: | Size: 420 B |
BIN
Godot/assets/graphics/monsters/spirit/idle/1.png
Normal file
After Width: | Height: | Size: 417 B |
BIN
Godot/assets/graphics/monsters/spirit/idle/2.png
Normal file
After Width: | Height: | Size: 406 B |
BIN
Godot/assets/graphics/monsters/spirit/idle/3.png
Normal file
After Width: | Height: | Size: 418 B |
BIN
Godot/assets/graphics/monsters/spirit/move/0.png
Normal file
After Width: | Height: | Size: 426 B |
BIN
Godot/assets/graphics/monsters/spirit/move/1.png
Normal file
After Width: | Height: | Size: 425 B |
BIN
Godot/assets/graphics/monsters/spirit/move/2.png
Normal file
After Width: | Height: | Size: 414 B |
BIN
Godot/assets/graphics/monsters/spirit/move/3.png
Normal file
After Width: | Height: | Size: 413 B |
BIN
Godot/assets/graphics/monsters/squid/attack/0 - Copy (2).png
Normal file
After Width: | Height: | Size: 463 B |
BIN
Godot/assets/graphics/monsters/squid/attack/0 - Copy (3).png
Normal file
After Width: | Height: | Size: 463 B |
BIN
Godot/assets/graphics/monsters/squid/attack/0 - Copy.png
Normal file
After Width: | Height: | Size: 463 B |