└── addons └── dispatch_queue ├── LICENSE ├── dispatch_queue.gd ├── dispatch_queue_node.gd ├── dispatch_queue_resource.gd └── samples ├── SampleScene.gd └── SampleScene.tscn /addons/dispatch_queue/LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /addons/dispatch_queue/dispatch_queue.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name DispatchQueue 3 | 4 | ## Emitted when the last queued Task finishes. 5 | ## This signal is emitted deferred, so it is safe to call non Thread-safe APIs. 6 | signal all_tasks_finished() 7 | 8 | ## Helper object that emits "finished" after all Tasks in a list finish. 9 | class TaskGroup: 10 | extends RefCounted 11 | 12 | ## Emitted after Task executes, passing the result as argument. 13 | ## The signal is emitted in the same Thread that executed the Task, so you 14 | ## need to connect with CONNECT_DEFERRED if you want to call non Thread-safe APIs. 15 | signal finished(results) 16 | 17 | var task_count := 0 18 | var task_results = [] 19 | var mutex: Mutex = null 20 | 21 | 22 | func _init(threaded: bool) -> void: 23 | if threaded: 24 | mutex = Mutex.new() 25 | 26 | 27 | ## Helper method for connecting to the "finished" signal. 28 | ## 29 | ## This enables the following pattern: 30 | ## dispatch_queue.dispatch_group(task_list).then(continuation_callable) 31 | func then(callable: Callable, flags: int = 0) -> int: 32 | return finished.connect(callable, flags | CONNECT_ONE_SHOT) 33 | 34 | 35 | ## Alias for `then` that also adds CONNECT_DEFERRED to flags. 36 | func then_deferred(callable: Callable, flags: int = 0) -> int: 37 | return then(callable, flags | CONNECT_DEFERRED) 38 | 39 | 40 | func add_task(task: Task) -> void: 41 | task.group = self 42 | task.id_in_group = task_count 43 | task_count += 1 44 | task_results.resize(task_count) 45 | 46 | 47 | func mark_task_finished(task: Task, result) -> void: 48 | if mutex: 49 | mutex.lock() 50 | task_count -= 1 51 | task_results[task.id_in_group] = result 52 | var is_last_task = task_count == 0 53 | if mutex: 54 | mutex.unlock() 55 | if is_last_task: 56 | finished.emit(task_results) 57 | 58 | ## A single task to be executed. 59 | ## 60 | ## Connect to the `finished` signal to receive the result either manually 61 | ## or by calling `then`/`then_deferred`. 62 | class Task: 63 | extends RefCounted 64 | 65 | ## Emitted after all Tasks in the group finish, passing the results Array as argument. 66 | ## The signal is emitted in the same Thread that executed the last pending Task, so you 67 | ## need to connect with CONNECT_DEFERRED if you want to call non Thread-safe APIs. 68 | signal finished(result) 69 | 70 | var callable: Callable 71 | var priority: int 72 | var group: TaskGroup = null 73 | var id_in_group: int = -1 74 | 75 | 76 | ## Helper method for connecting to the "finished" signal. 77 | ## 78 | ## This enables the following pattern: 79 | ## dispatch_queue.dispatch(callable).then(continuation_callable) 80 | func then(callable: Callable, flags: int = 0) -> int: 81 | return finished.connect(callable, flags | CONNECT_ONE_SHOT) 82 | 83 | 84 | ## Alias for `then` that also adds CONNECT_DEFERRED to flags. 85 | func then_deferred(callable: Callable, flags: int = 0) -> int: 86 | return then(callable, flags | CONNECT_DEFERRED) 87 | 88 | 89 | func execute() -> void: 90 | var result = callable.call() 91 | finished.emit(result) 92 | if group: 93 | group.mark_task_finished(self, result) 94 | 95 | 96 | class _WorkerPool: 97 | extends RefCounted 98 | 99 | var threads: Array[Thread] = [] 100 | var should_shutdown := false 101 | var mutex := Mutex.new() 102 | var semaphore := Semaphore.new() 103 | 104 | 105 | func _notification(what: int) -> void: 106 | if what == NOTIFICATION_PREDELETE and self: 107 | shutdown() 108 | 109 | 110 | func shutdown() -> void: 111 | if threads.is_empty(): 112 | return 113 | should_shutdown = true 114 | for i in threads.size(): 115 | semaphore.post() 116 | for t in threads: 117 | if t.is_alive(): 118 | t.wait_to_finish() 119 | threads.clear() 120 | should_shutdown = false 121 | 122 | 123 | var _task_queue = [] 124 | var _workers: _WorkerPool = null 125 | 126 | 127 | func _notification(what: int) -> void: 128 | if what == NOTIFICATION_PREDELETE and self: 129 | shutdown() 130 | 131 | 132 | ## Creates a Thread of execution to process tasks. 133 | ## If queue was already serial, this is a no-op, otherwise calls `shutdown` and create a new Thread. 134 | func create_serial() -> void: 135 | create_concurrent(1) 136 | 137 | 138 | ## Creates `thread_count` Threads of execution to process tasks. 139 | ## If queue was already concurrent with `thread_count` Threads, this is a no-op. 140 | ## Otherwise calls `shutdown` and create new Threads. 141 | ## If `thread_count <= 1`, creates a serial queue. 142 | func create_concurrent(thread_count: int = 1) -> void: 143 | if thread_count == get_thread_count(): 144 | return 145 | 146 | if is_threaded(): 147 | shutdown() 148 | 149 | _workers = _WorkerPool.new() 150 | var run_loop = self._run_loop.bind(_workers) 151 | for i in max(1, thread_count): 152 | var thread = Thread.new() 153 | _workers.threads.append(thread) 154 | thread.start(run_loop) 155 | 156 | 157 | ## Create a Task for executing `callable`, optionally setting a `priority` 158 | ## On threaded mode, the Task will be queued to be executed on a Thread in `priority` order. 159 | ## On synchronous mode, the Task will be queued to be executed in `priority` order on the next frame. 160 | ## Tasks whose `priority` is lower will execute first. 161 | func dispatch(callable: Callable, priority: int = 0) -> Task: 162 | var task = Task.new() 163 | if callable.is_valid(): 164 | task.callable = callable 165 | task.priority = priority 166 | if is_threaded(): 167 | _workers.mutex.lock() 168 | _insert_task(task) 169 | _workers.mutex.unlock() 170 | _workers.semaphore.call_deferred("post") 171 | else: 172 | if _task_queue.is_empty(): 173 | call_deferred("_sync_run_next_task") 174 | _insert_task(task) 175 | else: 176 | push_error("Trying to dispatch an invalid callable, ignoring it") 177 | return task 178 | 179 | 180 | ## Create all tasks in `task_list` by calling `dispatch` on each value with priority `priority`, 181 | ## returning the TaskGroup associated with them. 182 | ## TaskGroups whose `priority` is lower will execute first. 183 | func dispatch_group(task_list: Array[Callable], priority: int = 0) -> TaskGroup: 184 | var group = TaskGroup.new(is_threaded()) 185 | for callable in task_list: 186 | var task: Task = dispatch(callable, priority) 187 | group.add_task(task) 188 | 189 | return group 190 | 191 | 192 | ## Returns whether queue is threaded or synchronous. 193 | func is_threaded() -> bool: 194 | return _workers != null 195 | 196 | 197 | ## Returns the current Thread count. 198 | ## Returns 0 on synchronous mode. 199 | func get_thread_count() -> int: 200 | if is_threaded(): 201 | return _workers.threads.size() 202 | else: 203 | return 0 204 | 205 | 206 | ## Returns the number of queued tasks. 207 | func size() -> int: 208 | var result 209 | if is_threaded(): 210 | _workers.mutex.lock() 211 | result = _task_queue.size() 212 | _workers.mutex.unlock() 213 | else: 214 | result = _task_queue.size() 215 | return result 216 | 217 | 218 | ## Returns whether queue is empty, that is, there are no tasks queued. 219 | func is_empty() -> bool: 220 | return size() <= 0 221 | 222 | 223 | ## Cancel pending Tasks, clearing the current queue. 224 | ## Tasks that are being processed will still run to completion. 225 | func clear() -> void: 226 | if is_threaded(): 227 | _workers.mutex.lock() 228 | _task_queue.clear() 229 | _workers.mutex.unlock() 230 | else: 231 | _task_queue.clear() 232 | 233 | 234 | ## Cancel pending Tasks, wait and release the used Threads. 235 | ## The queue now runs in synchronous mode, so that new tasks will run in the main thread. 236 | ## Call `create_serial` or `create_concurrent` to recreate the worker threads. 237 | ## This method is called automatically on `NOTIFICATION_PREDELETE`. 238 | ## It is safe to call this more than once. 239 | func shutdown() -> void: 240 | clear() 241 | if is_threaded(): 242 | var current_workers = _workers 243 | _workers = null 244 | current_workers.shutdown() 245 | 246 | 247 | func _run_loop(pool: _WorkerPool) -> void: 248 | while true: 249 | pool.semaphore.wait() 250 | if pool.should_shutdown: 251 | break 252 | 253 | pool.mutex.lock() 254 | var task = _pop_task() 255 | pool.mutex.unlock() 256 | if task: 257 | task.execute() 258 | 259 | 260 | func _sync_run_next_task() -> void: 261 | var task = _pop_task() 262 | if task: 263 | task.execute() 264 | call_deferred("_sync_run_next_task") 265 | 266 | 267 | func _insert_task(task: Task) -> void: 268 | if not _task_queue or _task_queue[-1].priority <= task.priority: 269 | _task_queue.append(task) 270 | else: 271 | var index := _task_queue.bsearch_custom(task.priority, func(p, tsk): return p < tsk.priority, false) 272 | _task_queue.insert(index, task) 273 | 274 | 275 | func _pop_task() -> Task: 276 | var task: Task = _task_queue.pop_front() 277 | if task and _task_queue.is_empty(): 278 | task.then_deferred(self._on_last_task_finished) 279 | return task 280 | 281 | 282 | func _on_last_task_finished(_result): 283 | if is_empty(): 284 | all_tasks_finished.emit() 285 | -------------------------------------------------------------------------------- /addons/dispatch_queue/dispatch_queue_node.gd: -------------------------------------------------------------------------------- 1 | ## Node that wraps a DispatchQueue. 2 | ## 3 | ## Useful for having a local queue in a scene or as an Autoload. 4 | ## 5 | ## Apart from creation, all DispatchQueue public methods and signals are supported. 6 | ## 7 | ## Creates the Threads when entering tree and shuts down when exiting tree. 8 | ## If `thread_count == 0`, runs queue in synchronous mode. 9 | ## If `thread_count < 0`, creates `OS.get_processor_count()` Threads. 10 | extends Node 11 | class_name DispatchQueueNode 12 | 13 | signal all_tasks_finished() 14 | 15 | @export var thread_count: int = -1: set = set_thread_count 16 | 17 | var _dispatch_queue = DispatchQueue.new() 18 | 19 | 20 | func _ready() -> void: 21 | _dispatch_queue.all_tasks_finished.connect(self._on_all_tasks_finished) 22 | 23 | 24 | func _enter_tree() -> void: 25 | set_thread_count(thread_count) 26 | 27 | 28 | func _exit_tree() -> void: 29 | _dispatch_queue.shutdown() 30 | 31 | 32 | func set_thread_count(value: int) -> void: 33 | if value < 0: 34 | value = OS.get_processor_count() 35 | thread_count = value 36 | if thread_count == 0: 37 | _dispatch_queue.shutdown() 38 | else: 39 | _dispatch_queue.create_concurrent(thread_count) 40 | 41 | 42 | # DispatchQueue wrappers 43 | func dispatch(callable: Callable, priority: int = 0) -> DispatchQueue.Task: 44 | return _dispatch_queue.dispatch(callable, priority) 45 | 46 | 47 | func dispatch_group(task_list: Array[Callable], priority: int = 0) -> DispatchQueue.TaskGroup: 48 | return _dispatch_queue.dispatch_group(task_list, priority) 49 | 50 | 51 | func is_threaded() -> bool: 52 | return _dispatch_queue.is_threaded() 53 | 54 | 55 | func get_thread_count() -> int: 56 | return _dispatch_queue.get_thread_count() 57 | 58 | 59 | func size() -> int: 60 | return _dispatch_queue.size() 61 | 62 | 63 | func is_empty() -> bool: 64 | return _dispatch_queue.is_empty() 65 | 66 | 67 | func clear() -> void: 68 | _dispatch_queue.clear() 69 | 70 | 71 | func shutdown() -> void: 72 | _dispatch_queue.shutdown() 73 | 74 | 75 | # Private functions 76 | func _on_all_tasks_finished() -> void: 77 | all_tasks_finished.emit() 78 | -------------------------------------------------------------------------------- /addons/dispatch_queue/dispatch_queue_resource.gd: -------------------------------------------------------------------------------- 1 | ## Resource that wraps a DispatchQueue. 2 | ## 3 | ## Useful for sharing queues with multiple objects between scenes without resorting to Autoload. 4 | ## 5 | ## Apart from creation, all DispatchQueue public methods and signals are supported. 6 | ## 7 | ## If `thread_count == 0`, runs queue in synchronous mode. 8 | ## If `thread_count < 0`, creates `OS.get_processor_count()` Threads. 9 | extends Resource 10 | class_name DispatchQueueResource 11 | 12 | signal all_tasks_finished() 13 | 14 | @export var thread_count: int = -1: set = set_thread_count 15 | 16 | var _dispatch_queue = DispatchQueue.new() 17 | 18 | 19 | func _init(initial_thread_count: int = -1) -> void: 20 | _dispatch_queue.all_tasks_finished.connect(self._on_all_tasks_finished) 21 | set_thread_count(initial_thread_count) 22 | 23 | 24 | func set_thread_count(value: int) -> void: 25 | if value < 0: 26 | value = OS.get_processor_count() 27 | thread_count = value 28 | if thread_count == 0: 29 | _dispatch_queue.shutdown() 30 | else: 31 | _dispatch_queue.create_concurrent(thread_count) 32 | emit_changed() 33 | 34 | 35 | # DispatchQueue wrappers 36 | func dispatch(callable: Callable, priority: int = 0) -> DispatchQueue.Task: 37 | return _dispatch_queue.dispatch(callable, priority) 38 | 39 | 40 | func dispatch_group(task_list: Array[Callable], priority: int = 0) -> DispatchQueue.TaskGroup: 41 | return _dispatch_queue.dispatch_group(task_list, priority) 42 | 43 | 44 | func is_threaded() -> bool: 45 | return _dispatch_queue.is_threaded() 46 | 47 | 48 | func get_thread_count() -> int: 49 | return _dispatch_queue.get_thread_count() 50 | 51 | 52 | func size() -> int: 53 | return _dispatch_queue.size() 54 | 55 | 56 | func is_empty() -> bool: 57 | return _dispatch_queue.is_empty() 58 | 59 | 60 | func clear() -> void: 61 | _dispatch_queue.clear() 62 | 63 | 64 | func shutdown() -> void: 65 | _dispatch_queue.shutdown() 66 | 67 | 68 | # Private functions 69 | func _on_all_tasks_finished() -> void: 70 | all_tasks_finished.emit() 71 | -------------------------------------------------------------------------------- /addons/dispatch_queue/samples/SampleScene.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | @export var dispatch_queue_resource: DispatchQueueResource 4 | 5 | @onready var _dispatch_queue_node = $DispatchQueue 6 | 7 | 8 | func _ready() -> void: 9 | if not dispatch_queue_resource: 10 | dispatch_queue_resource = DispatchQueueResource.new() 11 | 12 | 13 | func _double(i): 14 | print("Processing ", i) 15 | return i * 2 16 | 17 | 18 | func _finished(i) -> void: 19 | print("Finished ", i) 20 | 21 | 22 | func _group_finished(results, from: int, to: int) -> void: 23 | print("Group [%d, %d) finished: %s" % [from, to, results]) 24 | 25 | 26 | func _all_finished() -> void: 27 | print("Over!\n") 28 | 29 | 30 | func _on_NodeButton_pressed() -> void: 31 | _dispatch_all(_dispatch_queue_node) 32 | 33 | 34 | func _on_ResourceButton_pressed() -> void: 35 | _dispatch_all(dispatch_queue_resource) 36 | 37 | 38 | func _dispatch_all(queue) -> void: 39 | for i in 5: 40 | _dispatch_group(queue, i * 10, (i + 1) * 10) 41 | queue.all_tasks_finished.connect(self._all_finished, CONNECT_ONE_SHOT) 42 | 43 | 44 | func _dispatch_group(queue, from: int, to: int) -> void: 45 | var tasks: Array[Callable] = [] 46 | for i in range(from, to): 47 | tasks.append(self._double.bind(i)) 48 | queue.dispatch_group(tasks).then(self._group_finished.bind(from, to)) 49 | -------------------------------------------------------------------------------- /addons/dispatch_queue/samples/SampleScene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://bqc2min8ilnei"] 2 | 3 | [ext_resource type="Script" path="res://addons/dispatch_queue/samples/SampleScene.gd" id="1"] 4 | [ext_resource type="Script" path="res://addons/dispatch_queue/dispatch_queue_node.gd" id="2"] 5 | [ext_resource type="Script" path="res://addons/dispatch_queue/dispatch_queue_resource.gd" id="3"] 6 | 7 | [sub_resource type="Resource" id="1"] 8 | script = ExtResource("3") 9 | thread_count = -1 10 | 11 | [node name="Node" type="Node"] 12 | script = ExtResource("1") 13 | dispatch_queue_resource = SubResource("1") 14 | 15 | [node name="NodeButton" type="Button" parent="."] 16 | anchors_preset = 8 17 | anchor_left = 0.5 18 | anchor_top = 0.5 19 | anchor_right = 0.5 20 | anchor_bottom = 0.5 21 | offset_left = -265.655 22 | offset_top = -35.0 23 | offset_right = -39.6548 24 | offset_bottom = 35.0 25 | text = "RUN Node" 26 | 27 | [node name="ResourceButton" type="Button" parent="."] 28 | anchors_preset = 8 29 | anchor_left = 0.5 30 | anchor_top = 0.5 31 | anchor_right = 0.5 32 | anchor_bottom = 0.5 33 | offset_left = 39.6548 34 | offset_top = -35.0 35 | offset_right = 265.655 36 | offset_bottom = 35.0 37 | text = "RUN Resource" 38 | 39 | [node name="DispatchQueue" type="Node" parent="."] 40 | script = ExtResource("2") 41 | 42 | [connection signal="pressed" from="NodeButton" to="." method="_on_NodeButton_pressed"] 43 | [connection signal="pressed" from="ResourceButton" to="." method="_on_ResourceButton_pressed"] 44 | --------------------------------------------------------------------------------