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 |