├── ChangeLog ├── addons └── GodotAsyncLoader │ ├── GodotAsyncLoader.gd │ ├── LICENSE │ ├── Singletons │ ├── SceneAdder.gd │ ├── SceneLoader.gd │ └── SceneSwitcher.gd │ └── plugin.cfg └── tools ├── make_release.sh └── plugin.template.cfg /ChangeLog: -------------------------------------------------------------------------------- 1 | 08/25/2022 Release 0.5.0 2 | * Fixed Bug #4: Extract into addon and put on Godot Asset Library 3 | 4 | 06/30/2022 Release 0.4 5 | * Fixed Bug #2: Make SceneAdder groups customizable 6 | * Fixed Bug #5: Trying to load non existent scene silently fails 7 | -------------------------------------------------------------------------------- /addons/GodotAsyncLoader/GodotAsyncLoader.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2022 Matthew Brennan Jones 2 | # This file is licensed under the MIT License 3 | # https://github.com/ImmersiveRPG/GodotAsyncLoader 4 | 5 | tool 6 | extends EditorPlugin 7 | 8 | # Get the name and paths of all the autoloads 9 | const autoloads := [ 10 | {"name": "SceneLoader", "path": "res://addons/GodotAsyncLoader/Singletons/SceneLoader.gd"}, 11 | {"name": "SceneAdder", "path": "res://addons/GodotAsyncLoader/Singletons/SceneAdder.gd"}, 12 | {"name": "SceneSwitcher", "path": "res://addons/GodotAsyncLoader/Singletons/SceneSwitcher.gd"}, 13 | ] 14 | 15 | func _enter_tree() -> void: 16 | # Install all the autoloads 17 | for entry in autoloads: 18 | print("Adding Autoload: %s" % [entry.name]) 19 | if not ProjectSettings.has_setting("autoload/%s" % [entry.name]): 20 | self.add_autoload_singleton(entry.name, entry.path) 21 | 22 | print("Installed plugin Godot Async Loader") 23 | 24 | func _exit_tree() -> void: 25 | # Uninstall all the autoloads 26 | var reverse_autoloads := autoloads.duplicate() 27 | reverse_autoloads.invert() 28 | for entry in reverse_autoloads: 29 | print("Removing Autoload: %s" % [entry.name]) 30 | if ProjectSettings.has_setting("autoload/%s" % [entry.name]): 31 | self.remove_autoload_singleton(entry.name) 32 | 33 | print("Uninstalled plugin Godot Async Loader") 34 | -------------------------------------------------------------------------------- /addons/GodotAsyncLoader/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Matthew Brennan Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/GodotAsyncLoader/Singletons/SceneAdder.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2022 Matthew Brennan Jones 2 | # This file is licensed under the MIT License 3 | # https://github.com/ImmersiveRPG/GodotAsyncLoader 4 | 5 | extends Node 6 | 7 | var _sleep_msec := 10 8 | var _is_running := false 9 | var _thread : Thread 10 | var _to_add := [] 11 | var _to_add_mutex := Mutex.new() 12 | var _to_adds := {} 13 | 14 | var GROUPS := [] 15 | 16 | func set_groups(groups : Array) -> void: 17 | GROUPS = groups 18 | 19 | for group in GROUPS: 20 | _to_adds[group] = [] 21 | 22 | func _enter_tree() -> void: 23 | _thread = Thread.new() 24 | var err = _thread.start(self, "_run_thread", 0, Thread.PRIORITY_LOW) 25 | assert(err == OK) 26 | 27 | func _exit_tree() -> void: 28 | if _is_running: 29 | _is_running = false 30 | 31 | if _thread: 32 | _thread.wait_to_finish() 33 | _thread = null 34 | 35 | func add_scene(on_done_cb : FuncRef, target : Node, path : String, pos : Vector3, is_pos_global : bool, cb : FuncRef, instance : Node, data : Dictionary, has_priority : bool) -> void: 36 | var entry := { 37 | "target" : target, 38 | "on_done_cb" : on_done_cb, 39 | "path" : path, 40 | "pos" : pos, 41 | "is_pos_global" : is_pos_global, 42 | "cb" : cb, 43 | "instance" : instance, 44 | "data" : data, 45 | "has_priority" : has_priority, 46 | } 47 | 48 | _to_add_mutex.lock() 49 | 50 | if has_priority: 51 | _to_add.push_front(entry) 52 | else: 53 | _to_add.push_back(entry) 54 | 55 | _to_add_mutex.unlock() 56 | 57 | func _can_add(group : String) -> bool: 58 | var i := GROUPS.find(group) 59 | 60 | match i: 61 | # Return false if unknown group 62 | -1: 63 | return false 64 | # Return true if there are any instances of this group to add 65 | 0: 66 | return not _to_adds[group].empty() 67 | # Return true if there are any instances of this group to add 68 | # and the previous group has no more instances to add 69 | _: 70 | var prev_group = GROUPS[i - 1] 71 | return not _to_adds[group].empty() and not _can_add(prev_group) 72 | 73 | return false 74 | 75 | func _run_thread(_arg : int) -> void: 76 | _is_running = true 77 | var is_reset := false 78 | 79 | while _is_running: 80 | is_reset = false 81 | self._check_for_new_scenes() 82 | 83 | for group in GROUPS: 84 | while _is_running and not is_reset and _can_add(group): 85 | is_reset = _add_entry(_to_adds[group], group) 86 | 87 | OS.delay_msec(2) 88 | 89 | func _add_entry(from : Array, group : String) -> bool: 90 | var entry = from.pop_front() 91 | if entry["is_child"]: 92 | _add_entry_child(entry, group) 93 | else: 94 | _add_entry_parent(entry, group) 95 | 96 | OS.delay_msec(_sleep_msec) 97 | return self._check_for_new_scenes() 98 | 99 | func _add_entry_parent(entry, group : String) -> void: 100 | var target = entry["target"] 101 | var on_done_cb = entry["on_done_cb"] 102 | var path = entry["path"] 103 | var pos = entry["pos"] 104 | var is_pos_global = entry["is_pos_global"] 105 | var cb = entry["cb"] 106 | var instance = entry["instance"] 107 | var data = entry["data"] 108 | print("+++ Adding %s \"%s\"" % [group, instance.name]) 109 | #on_done_cb.call_func(target, path, pos, is_pos_global, cb, instance, data) 110 | on_done_cb.call_deferred("call_func", target, path, pos, is_pos_global, cb, instance, data) 111 | 112 | func _add_entry_child(entry, group : String) -> void: 113 | var parent = entry["parent"] 114 | var owner = entry.get("owner", null) 115 | var instance = entry["instance"] 116 | var transform = entry["transform"] 117 | instance.transform = transform 118 | self.call_deferred("_on_add_entry_child_cb", parent, owner, instance, group) 119 | 120 | func _on_add_entry_child_cb(parent : Node, owner : Node, instance : Node, group : String) -> void: 121 | var start := OS.get_ticks_msec() 122 | parent.add_child(instance) 123 | if owner: 124 | instance.set_owner(owner) 125 | var time := OS.get_ticks_msec() - start 126 | print("+++ Adding %s \"%s\" %s ms" % [group, instance.name, time]) 127 | 128 | func _get_destination_queue_for_instance(instance : Node, has_priority : bool, default_queue = null): 129 | if has_priority: 130 | return _to_adds[GROUPS[0]] 131 | 132 | for group in instance.get_groups(): 133 | var i := GROUPS.find(group) 134 | if i != -1: 135 | return _to_adds[GROUPS[i]] 136 | 137 | return default_queue 138 | 139 | func _check_for_new_scenes() -> bool: 140 | _to_add_mutex.lock() 141 | var to_add := _to_add.duplicate() 142 | _to_add.clear() 143 | _to_add_mutex.unlock() 144 | 145 | var has_new_scenes := false 146 | for entry in to_add: 147 | var target = entry["target"] 148 | var has_priority = entry["has_priority"] 149 | #OS.delay_msec(1) 150 | var instance = entry["instance"] 151 | 152 | # Get the queue for this instance type 153 | var to = _get_destination_queue_for_instance(instance, has_priority, _to_adds[GROUPS[0]]) 154 | 155 | # Add the scene 156 | var entry_copy = entry.duplicate() 157 | #entry_copy["target"] = target 158 | entry_copy["is_child"] = false 159 | to.append(entry_copy) 160 | has_new_scenes = true 161 | 162 | # Remove all the scene's children to add later 163 | for child in _recursively_get_all_children_of_type(instance, Node): 164 | to = _get_destination_queue_for_instance(child, false, null) 165 | if to != null: 166 | var parent = child.get_parent() 167 | var owner = instance 168 | if parent != null: 169 | to.append({ "is_child" : true, "instance" : child, "parent" : parent, "owner" : owner, "transform" : child.transform }) 170 | parent.remove_child(child) 171 | 172 | return has_new_scenes 173 | 174 | func _recursively_get_all_children_of_type(target : Node, target_type) -> Array: 175 | var matches := [] 176 | var to_search := [target] 177 | while not to_search.empty(): 178 | var entry = to_search.pop_front() 179 | 180 | for child in entry.get_children(): 181 | to_search.append(child) 182 | 183 | if entry is target_type: 184 | matches.append(entry) 185 | 186 | return matches 187 | -------------------------------------------------------------------------------- /addons/GodotAsyncLoader/Singletons/SceneLoader.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2022 Matthew Brennan Jones 2 | # This file is licensed under the MIT License 3 | # https://github.com/ImmersiveRPG/GodotAsyncLoader 4 | 5 | extends Node 6 | 7 | var _is_logging_loads := false 8 | var _is_running := false 9 | var _thread : Thread 10 | var _scenes := {} 11 | var _scenes_mutex := Mutex.new() 12 | var _to_load := {} 13 | var _to_load_mutex := Mutex.new() 14 | 15 | func _enter_tree() -> void: 16 | _thread = Thread.new() 17 | var err = _thread.start(self, "_run_thread", 0, Thread.PRIORITY_LOW) 18 | assert(err == OK) 19 | 20 | func _exit_tree() -> void: 21 | if _is_running: 22 | _is_running = false 23 | 24 | if _thread: 25 | _thread.wait_to_finish() 26 | _thread = null 27 | 28 | func load_scene_async_with_cb(target : Node, path : String, pos : Vector3, is_pos_global : bool, cb : FuncRef, data : Dictionary, has_priority := false) -> void: 29 | var entry := { 30 | "path" : path, 31 | "cb" : cb, 32 | "pos" : pos, 33 | "is_pos_global" : is_pos_global, 34 | "data" : data, 35 | "has_priority" : has_priority, 36 | } 37 | 38 | _to_load_mutex.lock() 39 | if not _to_load.has(target): 40 | _to_load[target] = [] 41 | 42 | if has_priority: 43 | _to_load[target].push_front(entry) 44 | else: 45 | _to_load[target].push_back(entry) 46 | _to_load_mutex.unlock() 47 | #print(_to_load) 48 | 49 | func load_scene_async(target : Node, path : String, pos : Vector3, is_pos_global : bool) -> void: 50 | self.load_scene_async_with_cb(target, path, pos, is_pos_global, null, {}) 51 | 52 | func load_scene_sync(target : Node, path : String) -> Node: 53 | var data := {} 54 | 55 | # Load the scene 56 | var start := OS.get_ticks_msec() 57 | var scene = _get_cached_scene(path) 58 | if scene == null: return null 59 | if SceneLoader._is_logging_loads: data["load"] = OS.get_ticks_msec() - start 60 | 61 | # Instance the scene 62 | start = OS.get_ticks_msec() 63 | var instance = scene.instance() 64 | if SceneLoader._is_logging_loads: data["instance"] = OS.get_ticks_msec() - start 65 | 66 | # Add the scene to the target 67 | start = OS.get_ticks_msec() 68 | if target: 69 | target.add_child(instance) 70 | if SceneLoader._is_logging_loads: data["add"] = OS.get_ticks_msec() - start 71 | 72 | if SceneLoader._is_logging_loads: 73 | print("!!!!!! SYNC scene %s\n load %s ms in MAIN!!!!!!!!!!!!\n instance %s ms in MAIN!!!!!!!!!!!!\n add %s ms in MAIN!!!!!!!!!!!!" % [path, data["load"], data["instance"], data["add"]]) 74 | 75 | return instance 76 | 77 | func _run_thread(_arg : int) -> void: 78 | _is_running = true 79 | 80 | while _is_running: 81 | _to_load_mutex.lock() 82 | var to_load := _to_load.duplicate() 83 | _to_load = {} 84 | _to_load_mutex.unlock() 85 | 86 | for target in to_load: 87 | for entry in to_load[target]: 88 | var path = entry["path"] 89 | var cb = entry["cb"] 90 | var pos = entry["pos"] 91 | var is_pos_global = entry["is_pos_global"] 92 | var data = entry["data"] 93 | var has_priority = entry["has_priority"] 94 | #print("!!!!!!! path: %s" % path) 95 | 96 | var is_existing = ResourceLoader.exists(path) 97 | #print(path, " ", is_existing) 98 | if not is_existing: 99 | push_error("Scene files does not exist: %s" % [path]) 100 | else: 101 | # Load the scene 102 | var start := OS.get_ticks_msec() 103 | var scene = _get_cached_scene(path) 104 | if SceneLoader._is_logging_loads: data["load"] = OS.get_ticks_msec() - start 105 | 106 | # Instance the scene 107 | start = OS.get_ticks_msec() 108 | var instance = scene.instance() 109 | if SceneLoader._is_logging_loads: data["instance"] = OS.get_ticks_msec() - start 110 | 111 | # Send the instance to the callback in the main thread 112 | SceneAdder.add_scene(funcref(self, "_on_done"), target, path, pos, is_pos_global, cb, instance, data, has_priority) 113 | #self.call_deferred("_on_done", target, path, pos, is_pos_global, cb, instance, data) 114 | #print("??????? instance.global_transform.origin: %s" % instance.global_transform.origin) 115 | 116 | OS.delay_msec(2) 117 | 118 | func _on_done(target : Node, path : String, pos : Vector3, is_pos_global : bool, cb : FuncRef, instance : Node, data : Dictionary) -> void: 119 | var start := OS.get_ticks_msec() 120 | 121 | # Just return if target is invalid 122 | if not is_instance_valid(target): 123 | return 124 | 125 | # Just return if instance is invalid 126 | if not is_instance_valid(instance): 127 | return 128 | 129 | # Just return if the cb is invalid 130 | if cb != null and not cb.is_valid(): 131 | return 132 | 133 | if cb != null: 134 | #cb.call_deferred("call_func", path, instance, pos, is_pos_global, data) 135 | cb.call_func(path, instance, pos, is_pos_global, data) 136 | else: 137 | # Set the instance position 138 | if pos != Vector3.INF and "transform" in instance: 139 | # Convert the position from global to local if needed 140 | if is_pos_global: 141 | pos = pos - target.global_transform.origin 142 | 143 | instance.transform.origin = pos 144 | 145 | # Add the instance to the target 146 | target.add_child(instance) 147 | 148 | if SceneLoader._is_logging_loads: data["add"] = OS.get_ticks_msec() - start 149 | 150 | if SceneLoader._is_logging_loads: 151 | var message := "" 152 | message += "!!!!!! ASYNC scene %s\n" % path 153 | message += " load %s ms in THREAD\n" % data["load"] 154 | message += " instance %s ms in THREAD\n" % data["instance"] 155 | message += " add %s ms in MAIN!!!!!!!!!!!!" % data["add"] 156 | print(message) 157 | 158 | func _get_cached_scene(path : String) -> PackedScene: 159 | # Return null if path does not exist 160 | if not ResourceLoader.exists(path): 161 | push_error("Scene files does not exist: %s" % [path]) 162 | return null 163 | 164 | # Check if the scene is loaded 165 | _scenes_mutex.lock() 166 | var has_scene := _scenes.has(path) 167 | _scenes_mutex.unlock() 168 | 169 | # Load the scene if it isn't loaded 170 | if not has_scene: 171 | var packed_scene = ResourceLoader.load(path) 172 | _scenes_mutex.lock() 173 | _scenes[path] = packed_scene 174 | _scenes_mutex.unlock() 175 | 176 | # Get the scene 177 | _scenes_mutex.lock() 178 | var scene = _scenes[path] 179 | _scenes_mutex.unlock() 180 | 181 | return scene 182 | -------------------------------------------------------------------------------- /addons/GodotAsyncLoader/Singletons/SceneSwitcher.gd: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2022 Matthew Brennan Jones 2 | # This file is licensed under the MIT License 3 | # https://github.com/ImmersiveRPG/GodotAsyncLoader 4 | 5 | extends Node 6 | 7 | 8 | func change_scene(path : String, loading_path := "") -> void: 9 | var data := {} 10 | 11 | # Make sure the scene exists before starting to load 12 | if not ResourceLoader.exists(path): 13 | push_error("Scene files does not exist: %s" % [path]) 14 | return 15 | 16 | # Make sure the loading scene exists before starting to load 17 | if loading_path and not ResourceLoader.exists(loading_path): 18 | push_error("Loading scene files does not exist: %s" % [loading_path]) 19 | return 20 | 21 | # Show the loading screen 22 | var start := OS.get_ticks_msec() 23 | if loading_path: 24 | var err : int = get_tree().change_scene(loading_path) 25 | assert(err == OK) 26 | #if SceneLoader._is_logging_loads: print("!!!!!! MAIN: changed to loading scene for %s ms" % [OS.get_ticks_msec() - start]) 27 | if SceneLoader._is_logging_loads: data["change_scene"] = OS.get_ticks_msec() - start 28 | 29 | # Load the scene 30 | var pos := Vector3.INF 31 | SceneLoader.load_scene_async_with_cb(self, path, pos, true, funcref(self, "_on_scene_loaded"), data) 32 | 33 | func _on_scene_loaded(path : String, node : Node, _pos : Vector3, _is_pos_global : bool, data : Dictionary) -> void: 34 | var tree : SceneTree = self.get_tree() 35 | var new_scene = node 36 | 37 | # Remove the old scene 38 | var start := OS.get_ticks_msec() 39 | var old_scene = tree.current_scene 40 | tree.root.remove_child(old_scene) 41 | old_scene.queue_free() 42 | if SceneLoader._is_logging_loads: data["remove_scene"] = OS.get_ticks_msec() - start 43 | 44 | # Add the new scene 45 | start = OS.get_ticks_msec() 46 | tree.root.add_child(new_scene) 47 | var time := OS.get_ticks_msec() - start 48 | if SceneLoader._is_logging_loads: data["add"] = time 49 | print("+++ Adding %s \"%s\" %s ms" % ["scene", new_scene.name, time]) 50 | 51 | # Change to the new scene 52 | start = OS.get_ticks_msec() 53 | tree.set_current_scene(new_scene) 54 | if SceneLoader._is_logging_loads: data["set_current"] = OS.get_ticks_msec() - start 55 | 56 | if SceneLoader._is_logging_loads: 57 | var message := "" 58 | message += "!!!!!! scene switch %s\n" % path 59 | message += " load %s ms in THREAD\n" % data["load"] 60 | message += " instance %s ms in THREAD\n" % data["instance"] 61 | message += " remove previous %s ms in MAIN!!!!!!!!!!!!\n" % data["remove_scene"] 62 | message += " add %s ms in MAIN!!!!!!!!!!!!\n" % data["add"] 63 | message += " set current %s ms in MAIN!!!!!!!!!!!!\n" % data["set_current"] 64 | print(message) 65 | -------------------------------------------------------------------------------- /addons/GodotAsyncLoader/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Godot Async Loader" 4 | description="A Godot plugin to load, instance, and add scenes asynchronously using a background thread." 5 | author="Matthew Brennan Jones" 6 | version="0.5.0" 7 | script="GodotAsyncLoader.gd" 8 | -------------------------------------------------------------------------------- /tools/make_release.sh: -------------------------------------------------------------------------------- 1 | 2 | # Stop and exit on error 3 | set -e 4 | 5 | VERSION="0.5.0" 6 | 7 | # Change dir to this scripts directory 8 | script_dir=$(dirname $0) 9 | cd $script_dir 10 | 11 | # Generate plugin config 12 | cd .. 13 | sed 's/$VERSION/'$VERSION'/g' tools/plugin.template.cfg > addons/GodotAsyncLoader/plugin.cfg 14 | 15 | # Create release 16 | git commit -a -m "Release $VERSION" 17 | git push 18 | 19 | # Create and push tag 20 | git tag v$VERSION -m "Release $VERSION" 21 | git push --tags 22 | -------------------------------------------------------------------------------- /tools/plugin.template.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Godot Async Loader" 4 | description="A Godot plugin to load, instance, and add scenes asynchronously using a background thread." 5 | author="Matthew Brennan Jones" 6 | version="$VERSION" 7 | script="GodotAsyncLoader.gd" 8 | --------------------------------------------------------------------------------