I wanted to start building the GUI parts of the game, but create it in such a way that I will have a network interface that I can later replace with the actual implementation.
I created a LoginClient
that provides an interface to the login server functions. This client works in an asynchronous manner. Login requests gets queued, which then gets processed by a thread. The fun part was in the details, which I will explain here.
I feel this is one of the things that has been least documented in Godot's documentation and few people in Godot's Discord channel have knowledge of how this works.
The general code to create and run a method in a thread looks like this:
func _method_run_by_thread(unused_args):
# NOTE: `unused_args` aren't used, but is required for this
# method to be able to call by the thread
print("Ran this method")
func run_method_on_a_thread():
var thread = Thread.new()
thread.start(self, "_method_run_by_thread")
A few things to note here:
unused_args
are still required for the method that will be run by the threadThread.start()
can accept args that can be passed on to the function that gets run by theThread
instance
When a request is made via our network client, we want that communication to be asynchronous, i.e., non-blocking, so that the main thread where the graphics is renders the GUI still remains responsive.
There are a few ways to achieve this, the simplest being that the main thread qeueus the requests to the server, while a separate thread is dedicated to removing items from the queue and sending it to the server. Yet another thread listens for responses from the server.
There is no thread-safe queue in Godot and so creating one was my first task.
One of the interesting features of GDScript that I used in that was the yield
method. Particularly, the yield
with signal.
yield
method allows you to 'relinquish' the control back to the caller, which then can be resumed by the caller like so:
func method():
print(1)
yield()
print(2)
func method2():
var y = method()
print(3)
y.resume()
Here, the output will be 132
, because method
yielded to method2
after printing 1
, method2
then prints 3
and resumes method
, which goes on to print 2
.
When you yield to a signal from an object, the control gets passed back to the caller and the caller can continue with its operation, while the yielded method resumes once the signal has been triggered. I used this yield
-with-signal mechanism to resume a push()
when an element gets popped out, the the circular buffer is full. This allows for the circular buffer to not be strictly of a fixed size. You will still need to set a size for the queue such that the max size doesn't breach every time to cause yields to pile up.