├── addons ├── .DS_Store └── nylon │ ├── icon.png │ ├── plugin.cfg │ ├── scripts │ ├── callable.gd │ ├── weak_callable.gd │ ├── delayed_resume.gd │ ├── frame_resume.gd │ ├── timed_resume.gd │ ├── delayed_callable.gd │ ├── frame_callable.gd │ ├── nylon.gd │ ├── timed_iter.gd │ ├── settings.gd │ ├── worker.gd │ ├── batch_iter.gd │ ├── silk.gd │ ├── coroutine.gd │ └── async_resource_loader.gd │ ├── plugin.gd │ └── icon.png.import ├── screenshots ├── settings.png └── settings.png.import ├── test.tscn ├── dockerfiles └── gdtoolkit.Dockerfile ├── default_env.tres ├── .github └── workflows │ └── gdlint.yaml ├── docker-compose.yaml ├── .gitignore ├── LICENSE.md ├── .gdlintrc ├── project.godot ├── test.gd └── README.md /addons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mashumafi/nylon/HEAD/addons/.DS_Store -------------------------------------------------------------------------------- /addons/nylon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mashumafi/nylon/HEAD/addons/nylon/icon.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mashumafi/nylon/HEAD/screenshots/settings.png -------------------------------------------------------------------------------- /test.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://test.gd" type="Script" id=1] 4 | 5 | [node name="Test" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | -------------------------------------------------------------------------------- /addons/nylon/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Nylon" 4 | description="Async scheduling and management of couroutines" 5 | author="mashumafi" 6 | version="0.4.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /dockerfiles/gdtoolkit.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | RUN apt-get update && apt-get install -y python3 python3-pip 4 | RUN pip3 install 'gdtoolkit==3.*' 5 | 6 | WORKDIR /workdir 7 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /.github/workflows/gdlint.yaml: -------------------------------------------------------------------------------- 1 | name: GdLint project 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Docker Compose GdLint 13 | run: docker-compose run gdlint 14 | -------------------------------------------------------------------------------- /addons/nylon/scripts/callable.gd: -------------------------------------------------------------------------------- 1 | # Callable 2 | # Custom FuncRef implementation that incremements reference count for `instance` 3 | 4 | var instance 5 | var funcname: String 6 | 7 | 8 | func _init(instance, funcname: String) -> void: 9 | self.instance = instance 10 | self.funcname = funcname 11 | 12 | 13 | func call_func(args := []): 14 | return self.instance.callv(self.funcname, args) 15 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | gdlint: 6 | build: 7 | context: dockerfiles 8 | dockerfile: gdtoolkit.Dockerfile 9 | volumes: 10 | - .:/workdir 11 | command: gdlint addons/nylon 12 | 13 | gdformat: 14 | build: 15 | context: dockerfiles 16 | dockerfile: gdtoolkit.Dockerfile 17 | volumes: 18 | - .:/workdir 19 | command: gdformat addons/nylon 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/godot 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=godot 3 | 4 | ### Godot ### 5 | # Godot-specific ignores 6 | .import/ 7 | export.cfg 8 | export_presets.cfg 9 | 10 | # Imported translations (automatically generated from CSV files) 11 | *.translation 12 | 13 | # Mono-specific ignores 14 | .mono/ 15 | data_*/ 16 | 17 | # End of https://www.toptal.com/developers/gitignore/api/godot 18 | -------------------------------------------------------------------------------- /addons/nylon/plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | const Settings := preload("scripts/settings.gd") 5 | 6 | 7 | 8 | func _enter_tree(): 9 | Settings.create_project_settings() 10 | if Settings.get_add_singleton(): 11 | add_autoload_singleton("Worker", "res://addons/nylon/scripts/worker.gd") 12 | else: 13 | remove_autoload_singleton("Worker") 14 | 15 | 16 | func _exit_tree(): 17 | remove_autoload_singleton("Worker") 18 | Settings.clear_project_settings() 19 | 20 | 21 | func get_plugin_icon() -> Texture: 22 | return preload("icon.png") 23 | -------------------------------------------------------------------------------- /addons/nylon/scripts/weak_callable.gd: -------------------------------------------------------------------------------- 1 | # WeakCallable 2 | # Safely calls methods of `WeakRef` objects 3 | 4 | class_name WeakCallable 5 | extends Reference 6 | 7 | var instance: WeakRef 8 | var funcname: String 9 | 10 | 11 | # WeakCallable.new(instance: WeakRef, funcname: String) 12 | # instance (WeakRef): The object to call the method of 13 | # funcname (String): The method name to call 14 | func _init(instance: WeakRef, funcname: String): 15 | self.instance = instance 16 | self.funcname = funcname 17 | 18 | 19 | # call_func() 20 | # Calls the function if the instance is valid 21 | func call_func(): 22 | var ref = instance.get_ref() 23 | if ref: 24 | return ref.call(funcname) 25 | 26 | return true # Cancel infinite replay 27 | -------------------------------------------------------------------------------- /addons/nylon/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-73149d3f7632975d7849da51d447bb8d.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/nylon/icon.png" 13 | dest_files=[ "res://.import/icon.png-73149d3f7632975d7849da51d447bb8d.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | process/normal_map_invert_y=false 32 | stream=false 33 | size_limit=0 34 | detect_3d=true 35 | svg/scale=1.0 36 | -------------------------------------------------------------------------------- /screenshots/settings.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/settings.png-3d9f025a45c72dc554ceff0ea7128d70.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://screenshots/settings.png" 13 | dest_files=[ "res://.import/settings.png-3d9f025a45c72dc554ceff0ea7128d70.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | process/normal_map_invert_y=false 32 | stream=false 33 | size_limit=0 34 | detect_3d=true 35 | svg/scale=1.0 36 | -------------------------------------------------------------------------------- /addons/nylon/scripts/delayed_resume.gd: -------------------------------------------------------------------------------- 1 | # DelayedResume 2 | # Adds a delay after yielding from a coroutine 3 | 4 | class_name DelayedResume 5 | extends Reference 6 | 7 | const Callable := preload("callable.gd") 8 | 9 | var callable: Callable 10 | var delay: int 11 | 12 | 13 | # TimedResume.new(instance: Object, funcname: String, delay : int) 14 | # instance (Object): object to call a function 15 | # funcname (String): name of the function to call 16 | # delay (int): Time in milliseconds to wait after yielding 17 | func _init(instance, funcname: String, delay: int): 18 | self.callable = Callable.new(instance, funcname) 19 | self.delay = delay 20 | 21 | 22 | # call_func(): 23 | # Resumes the function after `delay` elapses 24 | func call_func(): 25 | var state = self.callable.call_func() 26 | while state is GDScriptFunctionState: 27 | var time := OS.get_system_time_msecs() + self.delay 28 | while OS.get_system_time_msecs() < time: 29 | yield() 30 | state = state.resume() 31 | return state 32 | -------------------------------------------------------------------------------- /addons/nylon/scripts/frame_resume.gd: -------------------------------------------------------------------------------- 1 | # FrameResume 2 | # Waits the requested number of idle frames after yielding from a coroutine 3 | 4 | class_name FrameResume 5 | extends Reference 6 | 7 | const Callable := preload("callable.gd") 8 | 9 | var callable: Callable 10 | var frames: int 11 | 12 | 13 | # FrameResume.new(instance: Object, funcname: String, frames : int) 14 | # instance (Object): object to call a function 15 | # funcname (String): name of the function to call 16 | # frames (int): Number of idle frames to wait after yielding 17 | func _init(instance, funcname: String, frames: int): 18 | self.callable = Callable.new(instance, funcname) 19 | self.frames = frames 20 | 21 | 22 | # call_func(): 23 | # Resumes the function after `frames` idle frames has pass 24 | func call_func(): 25 | var state = self.callable.call_func() 26 | while state is GDScriptFunctionState: 27 | var target_frames := Engine.get_idle_frames() + self.frames 28 | while Engine.get_idle_frames() < target_frames: 29 | yield() 30 | state = state.resume() 31 | return state 32 | -------------------------------------------------------------------------------- /addons/nylon/scripts/timed_resume.gd: -------------------------------------------------------------------------------- 1 | # TimedResume 2 | # Processes a coroutine until `timeout` 3 | 4 | class_name TimedResume 5 | extends Reference 6 | 7 | const Callable := preload("callable.gd") 8 | 9 | var callable: Callable 10 | var timeout: int 11 | 12 | 13 | # TimedResume.new(instance: Object, funcname: String, timeout : int) 14 | # instance (Object): object to call a function 15 | # funcname (String): name of the function to call 16 | # timeout (int): Time in milliseconds to spend proccesing the coroutine 17 | func _init(instance, funcname: String, timeout: int): 18 | self.callable = Callable.new(instance, funcname) 19 | self.timeout = timeout 20 | 21 | 22 | # call_func(): 23 | # Processes a coroutine until `timeout` 24 | func call_func(): 25 | var time := OS.get_system_time_msecs() + self.timeout 26 | var state = self.callable.call_func() 27 | while state is GDScriptFunctionState: 28 | if OS.get_system_time_msecs() >= time: 29 | yield() 30 | time = OS.get_system_time_msecs() + self.timeout 31 | state = state.resume() 32 | return state 33 | -------------------------------------------------------------------------------- /addons/nylon/scripts/delayed_callable.gd: -------------------------------------------------------------------------------- 1 | # DelayedCallable 2 | # Adds a delay after calling a coroutine 3 | 4 | class_name DelayedCallable 5 | extends Reference 6 | 7 | const Callable := preload("callable.gd") 8 | 9 | var callable: Callable 10 | var delay: int 11 | var last_finished := 0 12 | 13 | 14 | # DelayedCallable.new(instance: Object, funcname: String, delay : int) 15 | # instance (Object): object to call a function 16 | # funcname (String): name of the function to call 17 | # delay (int): Time in milliseconds to wait before a retry 18 | func _init(instance, funcname: String, delay: int): 19 | self.callable = Callable.new(instance, funcname) 20 | self.delay = delay 21 | 22 | 23 | # call_func() 24 | # Calls the function after `delay` elapses 25 | func call_func(): 26 | while OS.get_system_time_msecs() < last_finished: 27 | yield() 28 | 29 | var state = self.callable.call_func() 30 | while state is GDScriptFunctionState: 31 | yield() 32 | state = state.resume() 33 | 34 | last_finished = OS.get_system_time_msecs() + self.delay 35 | return state 36 | -------------------------------------------------------------------------------- /addons/nylon/scripts/frame_callable.gd: -------------------------------------------------------------------------------- 1 | # FrameCallable 2 | # Waits the requested number of idle frames after calling a coroutine 3 | 4 | class_name FrameCallable 5 | extends Reference 6 | 7 | const Callable := preload("callable.gd") 8 | 9 | var callable: Callable 10 | var frames: int 11 | var target_frames := 0 12 | 13 | 14 | # FrameCallable.new(instance: Object, funcname: String, frames : int) 15 | # instance (Object): object to call a function 16 | # funcname (String): name of the function to call 17 | # frames (int): Number of idle frames to wait before a retry 18 | func _init(instance, funcname: String, frames: int): 19 | self.callable = Callable.new(instance, funcname) 20 | self.frames = frames 21 | 22 | 23 | # call_func() 24 | # Calls the function after `frames` idle frames has passed 25 | func call_func(): 26 | while Engine.get_idle_frames() < target_frames: 27 | yield() 28 | 29 | var state = self.callable.call_func() 30 | while state is GDScriptFunctionState: 31 | yield() 32 | state = state.resume() 33 | 34 | target_frames = Engine.get_idle_frames() + self.frames 35 | return state 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2021 Matthew Murphy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /addons/nylon/scripts/nylon.gd: -------------------------------------------------------------------------------- 1 | class_name Nylon 2 | extends Node 3 | 4 | enum WorkerProcessMode { IDLE, PHYSICS } 5 | 6 | const Callable := preload("callable.gd") 7 | const Settings := preload("settings.gd") 8 | 9 | export(WorkerProcessMode) var process_mode := WorkerProcessMode.IDLE setget set_process_mode, get_process_mode 10 | 11 | # How long (milliseconds) to spend processing jobs before stopping 12 | # Jobs are processed using round-robbin and continues each frame 13 | # Use lower numbers if you see frames dropping, 0 will process 1 job per frame 14 | # Can be configured from Project Settings under Nylon section 15 | export(int) var process_timeout = Settings.get_process_timeout() 16 | 17 | 18 | func set_process_mode(new_process_mode: int) -> void: 19 | process_mode = new_process_mode 20 | set_process(WorkerProcessMode.IDLE == process_mode) 21 | set_physics_process(WorkerProcessMode.PHYSICS == process_mode) 22 | 23 | 24 | func get_process_mode() -> int: 25 | return process_mode 26 | 27 | 28 | func _ready() -> void: 29 | set_process_mode(process_mode) 30 | 31 | 32 | func _process(_delta: float) -> void: 33 | _run_coroutines() 34 | 35 | 36 | func _physics_process(_delta: float) -> void: 37 | _run_coroutines() 38 | 39 | 40 | func _run_coroutines() -> void: 41 | pass 42 | -------------------------------------------------------------------------------- /.gdlintrc: -------------------------------------------------------------------------------- 1 | class-definitions-order: 2 | - tools 3 | - classnames 4 | - extends 5 | - signals 6 | - enums 7 | - consts 8 | - exports 9 | - pubvars 10 | - prvvars 11 | - onreadypubvars 12 | - onreadyprvvars 13 | - others 14 | class-load-variable-name: (([A-Z][a-z0-9]*)+|_?[a-z][a-z0-9]*(_[a-z0-9]+)*) 15 | class-name: ([A-Z][a-z0-9]*)+ 16 | class-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* 17 | comparison-with-itself: null 18 | constant-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*' 19 | disable: [] 20 | duplicated-load: null 21 | enum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*' 22 | enum-name: ([A-Z][a-z0-9]*)+ 23 | excluded_directories: !!set 24 | .git: null 25 | expression-not-assigned: null 26 | function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* 27 | function-arguments-number: 10 28 | function-name: (_on_([A-Z][a-z0-9]*)+(_[a-z0-9]+)*|_?[a-z][a-z0-9]*(_[a-z0-9]+)*) 29 | function-preload-variable-name: ([A-Z][a-z0-9]*)+ 30 | function-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' 31 | load-constant-name: (([A-Z][a-z0-9]*)+|[A-Z][A-Z0-9]*(_[A-Z0-9]+)*) 32 | loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* 33 | max-file-lines: 1000 34 | max-line-length: 140 35 | max-public-methods: 35 36 | mixed-tabs-and-spaces: null 37 | no-elif-return: null 38 | no-else-return: null 39 | private-method-call: null 40 | signal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' 41 | sub-class-name: _?([A-Z][a-z0-9]*)+ 42 | tab-characters: 1 43 | trailing-whitespace: null 44 | unnecessary-pass: null 45 | unused-argument: null 46 | -------------------------------------------------------------------------------- /addons/nylon/scripts/timed_iter.gd: -------------------------------------------------------------------------------- 1 | # TimedIter 2 | # Processes an iterator in chunks based on `timeout` 3 | 4 | class_name TimedIter 5 | extends Reference 6 | 7 | const Callable := preload("callable.gd") 8 | 9 | var callable: Callable 10 | var iterator 11 | var timeout: int 12 | 13 | 14 | # TimedIter.new(instance: Object, funcname: String, iterator: Iterator, timeout : int) 15 | # instance (Object): object to call a function 16 | # funcname (String): name of the function to call 17 | # iterator (Iterator): The iterator to call functions on 18 | # timeout (int): Time in milliseconds to spend processing 19 | func _init(instance, funcname: String, iterator, timeout: int): 20 | self.callable = Callable.new(instance, funcname) 21 | self.iterator = iterator 22 | self.timeout = timeout 23 | 24 | 25 | # call_func() 26 | # Call `callable` on each item in `iterator` in chunks based on `timeout` 27 | # Returns the final result 28 | func call_func(): 29 | var end_time := OS.get_system_time_msecs() + timeout 30 | var final_result = null 31 | for item in iterator: 32 | var result = callable.call_func([item]) 33 | while result is GDScriptFunctionState: 34 | result = result.resume() 35 | if OS.get_system_time_msecs() >= end_time: 36 | yield() 37 | end_time = OS.get_system_time_msecs() + timeout 38 | final_result = result 39 | if OS.get_system_time_msecs() >= end_time: 40 | yield() 41 | end_time = OS.get_system_time_msecs() + timeout 42 | return final_result 43 | -------------------------------------------------------------------------------- /addons/nylon/scripts/settings.gd: -------------------------------------------------------------------------------- 1 | extends Resource 2 | 3 | const ADD_SINGLETON := "nylon/add_singleton" 4 | const ADD_SINGLETON_DEFAULT := true 5 | const PROCESS_TIMEOUT := "nylon/process_timeout" 6 | const PROCESS_TIMEOUT_DEFAULT := 3 7 | 8 | 9 | static func enum_to_hint(enumeration: Dictionary) -> String: 10 | return PoolStringArray(enumeration.keys()).join(",") 11 | 12 | 13 | static func create_project_settings() -> void: 14 | create_project_setting(ADD_SINGLETON, ADD_SINGLETON_DEFAULT) 15 | create_project_setting(PROCESS_TIMEOUT, PROCESS_TIMEOUT_DEFAULT) 16 | 17 | 18 | static func clear_project_settings() -> void: 19 | ProjectSettings.clear(ADD_SINGLETON) 20 | ProjectSettings.clear(PROCESS_TIMEOUT) 21 | 22 | 23 | static func create_project_setting( 24 | name: String, default, hint: int = PROPERTY_HINT_NONE, hint_string := "" 25 | ) -> void: 26 | if not ProjectSettings.has_setting(name): 27 | ProjectSettings.set_setting(name, default) 28 | 29 | ProjectSettings.set_initial_value(name, default) 30 | var info = { 31 | "name": name, 32 | "type": typeof(default), 33 | "hint": hint, 34 | "hint_string": hint_string, 35 | } 36 | ProjectSettings.add_property_info(info) 37 | 38 | 39 | static func get_setting(name: String, default): 40 | if ProjectSettings.has_setting(name): 41 | return ProjectSettings.get_setting(name) 42 | return default 43 | 44 | 45 | static func get_add_singleton() -> bool: 46 | return get_setting(ADD_SINGLETON, ADD_SINGLETON_DEFAULT) 47 | 48 | 49 | static func get_process_timeout() -> int: 50 | return get_setting(PROCESS_TIMEOUT, PROCESS_TIMEOUT_DEFAULT) 51 | -------------------------------------------------------------------------------- /addons/nylon/scripts/worker.gd: -------------------------------------------------------------------------------- 1 | extends Nylon 2 | 3 | const Coroutine := preload("coroutine.gd") 4 | 5 | # List of coroutines 6 | var _coroutines := [] 7 | 8 | 9 | # run_async(instance: Object, funcname: String, replay: int | bool) -> Coroutine 10 | # instance (Object): object to call a function 11 | # funcname (String): name of the function to call 12 | # replay (int | bool): How many times to call the function 13 | # Using `true` repeats until cancelled 14 | func run_async(instance, funcname: String, replay = 1) -> Coroutine: 15 | return _append_coroutine(Coroutine.new(Callable.new(instance, funcname), replay)) 16 | 17 | 18 | # _run_coroutines() 19 | # Runs all coroutines and throws away ones that are no longer valid 20 | func _run_coroutines(): 21 | var start := OS.get_system_time_msecs() 22 | var processed_coroutines := [] 23 | while not _coroutines.empty(): 24 | var coroutine = _coroutines.pop_front() 25 | coroutine.resume() 26 | processed_coroutines.append(coroutine) 27 | if OS.get_system_time_msecs() - start > process_timeout: 28 | break 29 | for coroutine in processed_coroutines: 30 | _append_coroutine(coroutine) 31 | 32 | 33 | # _append_coroutine(coroutine: Coroutine) -> Coroutine 34 | # Append a coroutine if it is valid and return it 35 | # coroutine (Coroutine): The coroutine to add, and invalid one results in a no-op 36 | # Returns Coroutine The same coroutine if it is valid, null otherwise 37 | func _append_coroutine(coroutine: Coroutine) -> Coroutine: 38 | if coroutine.is_valid(): 39 | _coroutines.append(coroutine) 40 | return coroutine 41 | 42 | return null 43 | -------------------------------------------------------------------------------- /addons/nylon/scripts/batch_iter.gd: -------------------------------------------------------------------------------- 1 | # BatchIter 2 | # Processes an iterator in chunks based on `batch_size` 3 | 4 | class_name BatchIter 5 | extends Reference 6 | 7 | const Callable := preload("callable.gd") 8 | 9 | var callable: Callable 10 | var iterator 11 | var batch_size: int 12 | 13 | 14 | # BatchIter.new(instance: Object, funcname: String, iterator: Iterator, batch_size : int) 15 | # instance (Object): object to call a function 16 | # funcname (String): name of the function to call 17 | # iterator (Iterator): The iterator to call functions on 18 | # batch_size (int): The max number of items to process per call 19 | func _init(instance, funcname: String, iterator, batch_size: int): 20 | self.callable = Callable.new(instance, funcname) 21 | self.iterator = iterator 22 | self.batch_size = batch_size 23 | 24 | 25 | # call_func() 26 | # Call `callable` on each item in `iterator` in chunks based on `batch_size` 27 | # If `callable` returns a `GDScriptFunctionState` each `resume` will increment count by 1 28 | # If `callable` returns an `int` then increment the count by that amount 29 | # Returns the final result 30 | func call_func(): 31 | var count := 0 32 | var final_result = null 33 | for item in iterator: 34 | var result = callable.call_func([item]) 35 | while result is GDScriptFunctionState: 36 | result = result.resume() 37 | count += 1 38 | if count >= batch_size: 39 | yield() 40 | count = 0 41 | if result is int: 42 | count += result 43 | else: 44 | final_result = result 45 | count += 1 46 | if count >= batch_size: 47 | yield() 48 | count = 0 49 | return final_result 50 | -------------------------------------------------------------------------------- /addons/nylon/scripts/silk.gd: -------------------------------------------------------------------------------- 1 | # Silk 2 | # Builder class for submitting coroutines to a worker 3 | 4 | class_name Silk 5 | extends Reference 6 | 7 | const NylonWorker := preload("worker.gd") 8 | 9 | var instance 10 | var funcname: String 11 | 12 | 13 | # Silk.new(instance: Object, funcname: String) 14 | # instance (Object): The object to call the method of 15 | # func_name (String): The method name to call 16 | func _init(instance, funcname := "call_func"): 17 | if instance is WeakRef: 18 | self.instance = WeakCallable.new(instance, funcname) 19 | self.funcname = "call_func" 20 | else: 21 | self.instance = instance 22 | self.funcname = funcname 23 | 24 | 25 | # batch_iter(iter, batch_size : int) -> Silk 26 | func batch_iter(iter, batch_size: int) -> Silk: 27 | self.instance = BatchIter.new(self.instance, self.funcname, iter, batch_size) 28 | self.funcname = "call_func" 29 | return self 30 | 31 | 32 | # delayed_callable(delay : int) -> Silk 33 | func delayed_callable(delay: int) -> Silk: 34 | self.instance = DelayedCallable.new(self.instance, self.funcname, delay) 35 | self.funcname = "call_func" 36 | return self 37 | 38 | 39 | # delayed_resume(delay : int) -> Silk 40 | func delayed_resume(delay: int) -> Silk: 41 | self.instance = DelayedResume.new(self.instance, self.funcname, delay) 42 | self.funcname = "call_func" 43 | return self 44 | 45 | 46 | # frame_callable(frames : int) -> Silk 47 | func frame_callable(frames: int) -> Silk: 48 | self.instance = FrameCallable.new(self.instance, self.funcname, frames) 49 | self.funcname = "call_func" 50 | return self 51 | 52 | 53 | # frame_resume(frames : int) -> Silk 54 | func frame_resume(frames: int) -> Silk: 55 | self.instance = FrameResume.new(self.instance, self.funcname, frames) 56 | self.funcname = "call_func" 57 | return self 58 | 59 | 60 | # timed_iter(timeout : int) -> Silk 61 | func timed_iter(iter, timeout: int) -> Silk: 62 | self.instance = TimedIter.new(self.instance, self.funcname, iter, timeout) 63 | self.funcname = "call_func" 64 | return self 65 | 66 | 67 | # timed_resume(timeout : int) -> Silk 68 | func timed_resume(timeout: int) -> Silk: 69 | self.instance = TimedResume.new(self.instance, self.funcname, timeout) 70 | self.funcname = "call_func" 71 | return self 72 | 73 | 74 | # submit(worker: NylonWorker, replay: int | bool) -> Coroutine 75 | # replay (int | bool): How many times to call the function 76 | # Using `true` repeats until cancelled 77 | func submit(worker: Nylon, retry = 1) -> Coroutine: 78 | return worker.callv("run_async", build(retry)) 79 | 80 | 81 | func build(retry = 1) -> Array: 82 | return [self.instance, self.funcname, retry] 83 | -------------------------------------------------------------------------------- /addons/nylon/scripts/coroutine.gd: -------------------------------------------------------------------------------- 1 | # Coroutine 2 | # Contains async state of a called function 3 | 4 | class_name Coroutine 5 | extends Reference 6 | 7 | # Emitted on the final execution 8 | # Can be used along with `replay` 9 | # Returns the final result 10 | signal completed(final_result) 11 | # Emitted at the start of each coroutine 12 | signal started 13 | # Emitted at the end of each coroutine once it stops yielding 14 | # Returns the latest result 15 | signal ended(result) 16 | 17 | const Callable := preload("callable.gd") 18 | 19 | var _callable: Callable 20 | var _replay = 1 21 | var _state: GDScriptFunctionState = null 22 | var _result = null 23 | 24 | 25 | # Coroutine.new(callable: Callable, replay : int | bool) 26 | # callable (Callable): The function to call 27 | # replay (int | bool): How many times to call the function 28 | # Using `true` repeats until cancelled 29 | func _init(callable: Callable, replay = 1): 30 | self._callable = callable 31 | self._replay = replay 32 | 33 | 34 | # _update_state(result) 35 | # Update the state and emit `ended` signal when invalid 36 | func _update_state(result): 37 | self._result = result 38 | self._state = result 39 | 40 | if not self._state is GDScriptFunctionState: 41 | self.emit_signal("ended", self._result) 42 | 43 | 44 | # resume() 45 | # Resumes processing the function 46 | # Decrements the `replay` by 1 when using an `int` 47 | # Coroutines that return exactly `true` will be canceled and end execution 48 | # Emits `started`, `ended` and `completed` 49 | func resume() -> void: 50 | var cancelled: bool = (self._result is bool and self._result) or not self._callable 51 | if self._state is GDScriptFunctionState: 52 | self._update_state(self._state.resume()) 53 | elif self._replay is int and 0 < self._replay and not cancelled: 54 | self.emit_signal("started") 55 | self._update_state(self._callable.call_func()) 56 | self._replay -= 1 57 | elif self._replay is bool and self._replay and not cancelled: 58 | self.emit_signal("started") 59 | self._update_state(self._callable.call_func()) 60 | else: 61 | self._callable = null 62 | self.emit_signal("completed", self._result) 63 | 64 | 65 | # cancel(finish_resuming: bool) 66 | # Cancels processing of the coroutine 67 | # finish_resuming (bool): true will allow function to `resume` until completion, false will destroy the function state 68 | func cancel(finish_resuming := false) -> void: 69 | self._callable = null 70 | if not finish_resuming: 71 | _update_state(null) 72 | 73 | 74 | # is_valid() -> bool 75 | # Returns true if calling `resume()` would change the state 76 | func is_valid() -> bool: 77 | return self._callable != null or self._state is GDScriptFunctionState 78 | -------------------------------------------------------------------------------- /addons/nylon/scripts/async_resource_loader.gd: -------------------------------------------------------------------------------- 1 | # AsyncResourceLoader 2 | # Loads resources asynchronously using coroutines 3 | 4 | class_name AsyncResourceLoader 5 | extends Reference 6 | 7 | var resource 8 | var loader_node: Node 9 | 10 | 11 | # AsyncResourceLoader.new(resource: String|ResourceInteractiveLoader|PackedScene|Node, loader_node: Node) 12 | # Loads a resource with special logic for smooth scene loading. 13 | # resource (String|ResourceInteractiveLoader|PackedScene|Node): Resource to load 14 | # loader_node (Node): Node used to load the scene. This will allow nodes to be added one-by-one 15 | func _init(resource, loader_node: Node = null): 16 | self.resource = resource 17 | self.loader_node = loader_node 18 | 19 | 20 | # call_func() 21 | # Loads a resource using yields at different stages so user can control the flow 22 | # Resource can be in any stage such as String for file path, PackedScenes, Node 23 | # There is logic to add nodes 1 at a time for smooth scenes loading 24 | func call_func(): 25 | if resource is String: 26 | resource = ResourceLoader.load_interactive(resource) 27 | 28 | if resource is ResourceInteractiveLoader: 29 | var result := OK 30 | while result == OK: 31 | yield() 32 | result = resource.poll() 33 | resource = resource.get_resource() 34 | yield() 35 | 36 | if resource is PackedScene: 37 | resource = resource.instance() 38 | yield() 39 | 40 | if not loader_node: 41 | return resource 42 | 43 | if resource is Node: 44 | var node = resource 45 | resource = [] 46 | orphan_prefabs(node, resource) 47 | 48 | if resource is Array and loader_node: 49 | for orphan in resource: 50 | yield() 51 | orphan.remove_from_parent() 52 | for orphan in resource: 53 | yield() 54 | loader_node.add_child(orphan.node) 55 | loader_node.remove_child(orphan.node) 56 | orphan.add_to_parent() 57 | return resource[resource.size() - 1].node 58 | 59 | 60 | func orphan_prefabs(node: Node, orphans: Array): 61 | for child in node.get_children(): 62 | orphan_prefabs(child, orphans) 63 | 64 | if node.filename: 65 | var orphan := Orphan.new(node.get_parent(), node.get_position_in_parent(), node) 66 | orphans.append(orphan) 67 | 68 | 69 | class Orphan: 70 | var parent: Node 71 | var index: int 72 | var node: Node 73 | 74 | func _init(p_parent: Node, p_index: int, p_node: Node) -> void: 75 | parent = p_parent 76 | index = p_index 77 | node = p_node 78 | 79 | func remove_from_parent(): 80 | if parent: 81 | parent.remove_child(node) 82 | 83 | func add_to_parent(): 84 | if parent: 85 | parent.add_child(node) 86 | parent.move_child(node, index) 87 | 88 | func _to_string() -> String: 89 | return "{0}[{1}]={2}".format([parent, index, node]) 90 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ { 12 | "base": "Reference", 13 | "class": "AsyncResourceLoader", 14 | "language": "GDScript", 15 | "path": "res://addons/nylon/scripts/async_resource_loader.gd" 16 | }, { 17 | "base": "Reference", 18 | "class": "BatchIter", 19 | "language": "GDScript", 20 | "path": "res://addons/nylon/scripts/batch_iter.gd" 21 | }, { 22 | "base": "Reference", 23 | "class": "Coroutine", 24 | "language": "GDScript", 25 | "path": "res://addons/nylon/scripts/coroutine.gd" 26 | }, { 27 | "base": "Reference", 28 | "class": "DelayedCallable", 29 | "language": "GDScript", 30 | "path": "res://addons/nylon/scripts/delayed_callable.gd" 31 | }, { 32 | "base": "Reference", 33 | "class": "DelayedResume", 34 | "language": "GDScript", 35 | "path": "res://addons/nylon/scripts/delayed_resume.gd" 36 | }, { 37 | "base": "Reference", 38 | "class": "FrameCallable", 39 | "language": "GDScript", 40 | "path": "res://addons/nylon/scripts/frame_callable.gd" 41 | }, { 42 | "base": "Reference", 43 | "class": "FrameResume", 44 | "language": "GDScript", 45 | "path": "res://addons/nylon/scripts/frame_resume.gd" 46 | }, { 47 | "base": "Node", 48 | "class": "Nylon", 49 | "language": "GDScript", 50 | "path": "res://addons/nylon/scripts/nylon.gd" 51 | }, { 52 | "base": "Reference", 53 | "class": "Silk", 54 | "language": "GDScript", 55 | "path": "res://addons/nylon/scripts/silk.gd" 56 | }, { 57 | "base": "Reference", 58 | "class": "TimedIter", 59 | "language": "GDScript", 60 | "path": "res://addons/nylon/scripts/timed_iter.gd" 61 | }, { 62 | "base": "Reference", 63 | "class": "TimedResume", 64 | "language": "GDScript", 65 | "path": "res://addons/nylon/scripts/timed_resume.gd" 66 | }, { 67 | "base": "Reference", 68 | "class": "WeakCallable", 69 | "language": "GDScript", 70 | "path": "res://addons/nylon/scripts/weak_callable.gd" 71 | } ] 72 | _global_script_class_icons={ 73 | "AsyncResourceLoader": "", 74 | "BatchIter": "", 75 | "Coroutine": "", 76 | "DelayedCallable": "", 77 | "DelayedResume": "", 78 | "FrameCallable": "", 79 | "FrameResume": "", 80 | "Nylon": "", 81 | "Silk": "", 82 | "TimedIter": "", 83 | "TimedResume": "", 84 | "WeakCallable": "" 85 | } 86 | 87 | [application] 88 | 89 | config/name="Nylon" 90 | run/main_scene="res://test.tscn" 91 | config/icon="res://addons/nylon/icon.png" 92 | 93 | [autoload] 94 | 95 | Worker="*res://addons/nylon/scripts/worker.gd" 96 | 97 | [editor_plugins] 98 | 99 | enabled=PoolStringArray( "res://addons/nylon/plugin.cfg" ) 100 | 101 | [physics] 102 | 103 | common/enable_pause_aware_picking=true 104 | 105 | [rendering] 106 | 107 | quality/driver/driver_name="GLES2" 108 | vram_compression/import_etc=true 109 | vram_compression/import_etc2=false 110 | environment/default_environment="res://default_env.tres" 111 | -------------------------------------------------------------------------------- /test.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class Test: 4 | func count_to_10(): 5 | for i in range(10): 6 | print(i) 7 | yield() 8 | 9 | return 10 10 | 11 | var j := 20 12 | func repeat_10_times(): 13 | if j < 30: 14 | print(j) 15 | j += 1 16 | return false 17 | 18 | return true 19 | 20 | class Sum: 21 | var count := 0 22 | 23 | func increment(num: int): 24 | count += num 25 | return float(count) 26 | 27 | func just_yield_once(): 28 | yield() 29 | 30 | func print_waiting_500(): 31 | print("Waiting 500 ms") 32 | 33 | class WeakTest: 34 | extends Reference 35 | 36 | var count := 0 37 | 38 | func echo(): 39 | count += 1 40 | print("existing..") 41 | 42 | var finished_work := false 43 | 44 | func _ready() -> void: 45 | var test := Test.new() 46 | 47 | var count_job := Worker.run_async(test, "count_to_10") 48 | yield(count_job, "started") 49 | assert(yield(count_job, "ended") == 10) 50 | assert(yield(count_job, "completed") == 10) 51 | 52 | var repeat_job := Worker.run_async(test, "repeat_10_times", true) 53 | for _i in range(11): 54 | yield(repeat_job, "started") 55 | yield(repeat_job, "ended") 56 | yield(repeat_job, "completed") 57 | 58 | var loader := AsyncResourceLoader.new("addons/nylon/icon.png") 59 | var load_job := Worker.run_async(loader, "call_func") 60 | var res = yield(load_job, "completed") 61 | assert(res is Texture) 62 | print(res) 63 | 64 | var weak := WeakTest.new() 65 | var weak_callable := WeakCallable.new(weakref(weak), "echo") 66 | var weak_job := Worker.run_async(weak_callable, "call_func", true) 67 | yield(get_tree().create_timer(.1), "timeout") 68 | var count := weak.count 69 | weak = null 70 | assert(yield(weak_job, "completed") == true) 71 | assert(count > 0) 72 | 73 | var sum := Sum.new() 74 | var batch_iter := BatchIter.new(sum, "increment", [1, 1, 2, 3, 5], 2) 75 | var batch_job := Worker.run_async(batch_iter, "call_func") 76 | assert(yield(batch_job, "completed") == 12.0) 77 | assert(sum.count == 12) 78 | 79 | var cancel_job := Worker.run_async(batch_iter, "call_func") 80 | yield(cancel_job, "started") 81 | yield(get_tree(), "idle_frame") # Cannot cancel while handling `started` signal 82 | cancel_job.cancel(false) # emits `ended` 83 | assert(yield(cancel_job, "completed") == null) # no result since it was cancelled 84 | assert(sum.count == 14) 85 | 86 | var cancel_job_wait := Worker.run_async(batch_iter, "call_func") 87 | yield(cancel_job_wait, "started") 88 | yield(get_tree(), "idle_frame") # Cannot cancel while handling `started` signal 89 | cancel_job_wait.cancel(true) 90 | assert(yield(cancel_job_wait, "ended") == 26.0) # `completed` won't be called 91 | assert(sum.count == 26) 92 | 93 | var timed_iter := TimedIter.new(sum, "increment", [1, 1, 2, 3, 5], 1) 94 | var timed_iter_job := Worker.run_async(timed_iter, "call_func") 95 | assert(yield(timed_iter_job, "completed") == 38.0) 96 | assert(sum.count == 38) 97 | 98 | print("waiting...") 99 | var timed_weak := WeakCallable.new(weakref(self), "print_wait") 100 | var timed_resume := DelayedResume.new(timed_weak, "call_func", 5) 101 | var timed_start := DelayedCallable.new(timed_resume, "call_func", 50) 102 | var timed_job = Worker.run_async(timed_start, "call_func") 103 | assert(yield(timed_job, "completed") == "result") 104 | 105 | print("waiting for silk...") 106 | var silk_timed_job = Silk.new(self, "print_wait") \ 107 | .delayed_resume(5) \ 108 | .delayed_callable(50) \ 109 | .submit(Worker) 110 | assert(yield(silk_timed_job, "completed") == "result") 111 | 112 | print("Prints twice {") 113 | var timed_new := Silk.new(self, "sleep_ms") \ 114 | .timed_resume(15) \ 115 | .submit(Worker) 116 | yield(timed_new, "completed") 117 | print("} Prints twice") 118 | 119 | print("Starting..") 120 | var delayed_callable := Silk.new(self, "print_waiting_500") \ 121 | .delayed_callable(500) \ 122 | .submit(Worker, 3) 123 | yield(delayed_callable, "completed") 124 | print("..Done") 125 | 126 | print("Waiting 18 frames") 127 | var frames := Silk.new(self, "just_yield_once") \ 128 | .frame_callable(3) \ 129 | .frame_resume(3) \ 130 | .submit(Worker, 3) 131 | yield(frames, "completed") 132 | print("...Done 18 frames") 133 | 134 | finished_work = true 135 | print("Finished") 136 | 137 | var unfinished_job := Worker.run_async(Test.new(), "count_to_10") # Test should live on 138 | assert(get_tree().connect("idle_frame", self, "wait_for_job", [unfinished_job]) == OK) 139 | 140 | func sleep_ms(): 141 | for i in range(5): 142 | OS.delay_usec(1) 143 | yield() 144 | 145 | func wait_for_job(unfinished_job: Coroutine): 146 | assert(yield(unfinished_job, "completed") == 10) 147 | 148 | func print_wait(): 149 | print("waited") 150 | 151 | for k in range(30, 40): 152 | yield() 153 | print(k) 154 | 155 | return "result" 156 | 157 | func _process(_delta: float): 158 | if not finished_work: 159 | print("Processing...") 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nylon 2 | 3 | A gdscript module that runs coroutines asynchronously. 4 | 5 | ## Purpose 6 | 7 | This library gives the feeling of threads without some of the headaches. 8 | 9 | Threading can be used to improve performance or allow running logic seperate from the main loop but in most cases come with their own problems including: 10 | * Lack of [Thread-safe APIs](https://docs.godotengine.org/en/stable/tutorials/threads/thread_safe_apis.html) 11 | * Managing threading primitives [Mutex and Semaphore](https://docs.godotengine.org/en/stable/tutorials/threads/using_multiple_threads.html) 12 | * Not able to use editor break-points for debugging 13 | 14 | Nylon makes use of the `yield` keyword which creates a `GDScriptFunctionState` which can be used to `resume` a function. It calls `resume` on each frame which allows users to compute chunks at a time and not hog processing time. It uses the main thread so you avoid issues above with threads but still requires users to chunk work using `yield` to give back control to the main game loop. 15 | 16 | ## Example 17 | 18 | Say you have the following function which when called users will see a noticeable number of frames dropped. 19 | 20 | ```gdscript 21 | func update_nodes(): 22 | for child in get_children(): 23 | update_child(child) # performance bottle-neck 24 | ``` 25 | 26 | The performance issue could be due to having hundreds of nodes to update. If this update could occur in the background and infrequently it might be worth using Nylon to perform the work. Using Nylon this function could be refactored like so: 27 | 28 | ```gdscript 29 | func update_nodes(): 30 | for child in get_children(): 31 | update_child(child) 32 | yield() 33 | ``` 34 | 35 | It can then be run async by nylon with the following call: 36 | 37 | ```gdscript 38 | func submit_update(): 39 | Worker.run_async(self, "update_nodes") 40 | ``` 41 | 42 | Now Nylon will now update 1 node over the next hundred frames which could improve the user experience. 43 | 44 | ## Settings 45 | 46 | Settings can be found under the `Nylon` section in the project settings. 47 | 48 | ![Settings](screenshots/settings.png) 49 | 50 | ### Add Singleton 51 | 52 | Adds a `Worker` singleton. If you prefer to do this on your own or use local nodes then disable this feature. 53 | 54 | ### Process Timeout 55 | 56 | The amount of time (in milliseconds) spent processing coroutines in a single frame. 57 | Nylon uses a round-robin queue to process tasks. 58 | Setting a value of 0 will process 1 task per frame. 59 | Lower numbers may improve frame rates if the queue grows large. 60 | 61 | ## Usage 62 | 63 | The main entry point for Nylon is the function `Worker.run_async(instance, funcname, retry)`. 64 | It is recommended to add `Worker` as a autoload singleton depending on your workflow. 65 | It takes only the instace/funcname and how many times to run that function. 66 | See [Silk](#Silk) which uses the builder pattern to create complex jobs. 67 | 68 | You can run a function forever by supplying `true` for `retry`. It will run until cancelled which occurs when the coroutine `return true`. 69 | 70 | Coroutines emit the following signals: 71 | * `started` when a function is first called at the beginning of each `retry` 72 | * `ended` when a function does not `yield` 73 | * It returns the latest result of the coroutine 74 | * `completed` when `retry` reaches `0` or the coroutine is cancelled 75 | * It yields the latest or final result of the coroutine depending if it was cancelled 76 | 77 | See [test.gd](https://github.com/mashumafi/nylon/blob/main/test.gd) for more examples. When run you will see the `_process()` function gets called while Nylon is performing other operations. 78 | 79 | You can build complex hierarchy of coroutines, here are a few that come with Nylon: 80 | 81 | ### DelayedCallable 82 | 83 | Adds a delay after calling the coroutine. This delay occurs before each retry. 84 | 85 | ```gdscript 86 | # Update all nodes forever with a 500 millisecond delay between each update 87 | var delayed_callable := DelayedCallable.new(self, "update_nodes", 500) 88 | Worker.run_async(delayed_callable, "call_func", true) # true to repeat forever 89 | ``` 90 | 91 | ### DelayedResume 92 | 93 | Adds a delay after each `yield`. This allows workers to take breaks between chunks. 94 | 95 | ```gdscript 96 | # Update 1 node every 50 milliseconds 97 | var delayed_resume := TimedResume.new(self, "update_nodes", 50) 98 | Worker.run_async(delayed_resume, "call_func") 99 | ``` 100 | 101 | ### FrameCallable 102 | 103 | Waits the requested number of idle frames after calling the coroutine. This delay occurs before each retry. 104 | 105 | ```gdscript 106 | # Update all nodes forever with a 16 idle frames between each update 107 | var frame_callable := FrameCallable.new(self, "update_nodes", 16) 108 | Worker.run_async(frame_callable, "call_func", true) # true to repeat forever 109 | ``` 110 | 111 | ### FrameResume 112 | 113 | Waits the requested number of idle frames after each `yield`. This allows workers to take breaks between chunks. 114 | 115 | ```gdscript 116 | # Update 1 node every 3 frames 117 | var frame_resume := FrameResume.new(self, "update_nodes", 3) 118 | Worker.run_async(frame_resume, "call_func") 119 | ``` 120 | 121 | ### TimedResume 122 | 123 | Processes a coroutine and `yield` control after `timeout`. 124 | 125 | ```gdscript 126 | # Update nodes for 3 milliseconds of each frame. 127 | var timed_resume := TimedResume.new(self, "update_nodes", 3) 128 | Worker.run_async(delayed_retimed_resumesume, "call_func") 129 | ``` 130 | 131 | ### WeakCallable 132 | 133 | Safely call coroutines of a `WeakRef`. 134 | 135 | ```gdscript 136 | # Update nodes until `self` is no longer valid 137 | var weak_callable := WeakCallable.new(weakref(self), "update_nodes") 138 | Worker.run_async(weak_callable, "call_func") 139 | ``` 140 | 141 | ### Iterators 142 | 143 | The Iter classes take an iterator and performs small amounts of work at a time. They take an iterator and a instance/funcname, the function should take 1 argument. With the above example you can call `update_child` directly. 144 | 145 | #### BatchIter 146 | 147 | Call the provided function on each item in `iterator` in chunks based on `batch_size`. 148 | 149 | ```gdscript 150 | # Update 2 nodes per frame 151 | var batch_iter := BatchIter.new(self, "update_child", get_children(), 2) 152 | Worker.run_async(batch_iter, "call_func") 153 | ``` 154 | 155 | #### TimedIter 156 | 157 | Call the provided function on each item in `iterator` in chunks based on `timeout`. 158 | 159 | ```gdscript 160 | # Update nodes for 2 milliseconds each frame 161 | var timed_iter := TimedIter.new(self, "update_child", get_children(), 2) 162 | Worker.run_async(timed_iter, "call_func") 163 | ``` 164 | 165 | ### Callable 166 | 167 | A custom implementation of `FuncRef`. The main benefit of `Callable` is it will increment the refrence counter for instances passed in. 168 | 169 | ### Cancelling 170 | 171 | You may want to cancel existing coroutines for reasons such as: 172 | * Stop them from running forever 173 | * The result is no longer needed 174 | 175 | Nylon will stop processing a coroutine if at any point it returns exactly `true`. 176 | Another option is to use the `cancel` method on `Coroutine` which is returned by `Worker.run_async(instance, funcname, retry)`. 177 | `cancel` can be used to stop processing entirely or to allow processing to finish resuming a function to it's final result. 178 | 179 | ```gdscript 180 | var job := Worker.run_async(instance, funcname, retry) 181 | job.cancel(true) # Allow job to finish resuming, emits `ended` once finished but never emits `completed` 182 | job.cancel(false) # Immediately terminates a job, emits `ended` once called and `completed` emits on the next frame 183 | ``` 184 | 185 | ## Silk 186 | 187 | The `Silk` class simplifies complex Nylon tasks using the builder pattern. 188 | 189 | It can transform a redundant expression like: 190 | 191 | ```gdscript 192 | var delayed_weak := WeakCallable.new(weakref(self), "print_wait") 193 | var delayed_resume := DelayedResume.new(delayed_weak, "call_func", 5) 194 | var delayed_start := DelayedCallable.new(delayed_resume, "call_func", 50) 195 | Worker.run_async(delayed_start, "call_func", 3) 196 | ``` 197 | 198 | into the following: 199 | 200 | ```gdscript 201 | Silk.new(weakref(self), "print_wait") \ # the base function 202 | .delayed_resume(5) \ # wait 5 milliseconds after each yield 203 | .delayed_callable(50) \ # wait 50 milliseconds before each retry 204 | .submit(Worker, 3) # tell worker to run the job three times async 205 | ``` 206 | 207 | Always remember that jobs are evaluated from bottom to top. In the above example it would be: 208 | 1. `delayed_callable` 209 | 2. `delayed_resume` 210 | 3. `print_wait` 211 | 212 | Passing a `WeakRef` into the contructor of `Silk` will create a `WeakCallable` and will automatically destroy the Nylon job when the instance is freed. Every other instance type will be handled normally meaning Nylon will contribute to the use count of `Reference` and you must manually cancel jobs using an `Object` before freeing them. 213 | --------------------------------------------------------------------------------