Custom Godot servers
This guide assumes the reader knows how to create C++ modules and Godot data types. If not, refer to .
What for?
Adding artificial intelligence.
Adding custom asynchronous threads.
Adding support for a new input device.
Adding a custom VoIP protocol.
And more…
At minimum, a server must have a static instance, a sleep timer, a thread loop, an initialization state and a cleanup procedure.
#include "core/dictionary.h"
#include "core/list.h"
#include "core/os/os.h"
#include "core/variant.h"
#include "prime_225.h"
void HilbertHotel::thread_func(void *p_udata) {
HilbertHotel *ac = (HilbertHotel *) p_udata;
uint64_t msdelay = 1000;
while (!ac->exit_thread) {
if (!ac->empty()) {
ac->lock();
ac->register_rooms();
ac->unlock();
}
OS::get_singleton()->delay_usec(msdelay * 1000);
}
}
Error HilbertHotel::init() {
thread_exited = false;
counter = 0;
mutex = Mutex::create();
thread = Thread::create(HilbertHotel::thread_func, this);
return OK;
}
HilbertHotel *HilbertHotel::singleton = NULL;
HilbertHotel *HilbertHotel::get_singleton() {
return singleton;
}
void HilbertHotel::register_rooms() {
for (Set<RID>::Element *e = buses.front(); e; e = e->next()) {
auto bus = bus_owner.getornull(e->get());
if (bus) {
uint64_t room = bus->next_room();
_emit_occupy_room(room, bus->get_self());
}
}
}
void HilbertHotel::unlock() {
if (!thread || !mutex) {
return;
}
mutex->unlock();
}
void HilbertHotel::lock() {
if (!thread || !mutex) {
return;
}
mutex->lock();
}
void HilbertHotel::_emit_occupy_room(uint64_t room, RID rid) {
_HilbertHotel::get_singleton()->_occupy_room(room, rid);
}
Variant HilbertHotel::get_bus_info(RID id) {
InfiniteBus *)bus = bus_owner.getornull(id);
if (bus) {
Dictionary d;
d["prime"] = bus->get_bus_num();
d["current_room"] = bus->get_current_room();
return d;
}
return Variant();
}
void HilbertHotel::finish() {
if (!thread) {
return;
}
exit_thread = true;
memdelete(thread);
if (mutex) {
memdelete(mutex);
}
thread = NULL;
}
RID HilbertHotel::create_bus() {
lock();
InfiniteBus *ptr = memnew(InfiniteBus(PRIME[counter++]));
RID ret = bus_owner.make_rid(ptr);
ptr->set_self(ret);
buses.insert(ret);
unlock();
return ret;
}
// https://github.com/godotengine/godot/blob/3.x/core/rid.h#L187
bool HilbertHotel::delete_bus(RID id) {
if (bus_owner.owns(id)) {
lock();
InfiniteBus *b = bus_owner.get(id);
bus_owner.free(id);
buses.erase(id);
memdelete(b);
unlock();
return true;
}
return false;
}
void HilbertHotel::clear() {
for (Set<RID>::Element *e = buses.front(); e; e = e->next()) {
delete_bus(e->get());
}
}
bool HilbertHotel::empty() {
return buses.size() <= 0;
}
void HilbertHotel::_bind_methods() {
}
HilbertHotel::HilbertHotel() {
singleton = this;
}
/* prime_225.h */
#include "core/int_types.h"
const uint64_t PRIME[225] = {
2,3,5,7,11,13,17,19,23,
29,31,37,41,43,47,53,59,61,
67,71,73,79,83,89,97,101,103,
107,109,113,127,131,137,139,149,151,
157,163,167,173,179,181,191,193,197,
199,211,223,227,229,233,239,241,251,
257,263,269,271,277,281,283,293,307,
311,313,317,331,337,347,349,353,359,
367,373,379,383,389,397,401,409,419,
421,431,433,439,443,449,457,461,463,
467,479,487,491,499,503,509,521,523,
541,547,557,563,569,571,577,587,593,
599,601,607,613,617,619,631,641,643,
647,653,659,661,673,677,683,691,701,
709,719,727,733,739,743,751,757,761,
769,773,787,797,809,811,821,823,827,
829,839,853,857,859,863,877,881,883,
887,907,911,919,929,937,941,947,953,
1021,1031,1033,1039,1049,1051,1061,1063,1069,
1087,1091,1093,1097,1103,1109,1117,1123,1129,
1151,1153,1163,1171,1181,1187,1193,1201,1213,
1217,1223,1229,1231,1237,1249,1259,1277,1279,
1283,1289,1291,1297,1301,1303,1307,1319,1321,
1327,1361,1367,1373,1381,1399,1409,1423,1427
};
Custom managed resource data
Godot servers implement a mediator pattern. All data types inherit RID_Data
. RID_Owner<MyRID_Data>
owns the object when make_rid
is called. During debug mode only, RID_Owner maintains a list of RIDs. In practice, RIDs are similar to writing object-oriented C code.
Servers are allocated in register_types.cpp
. The constructor sets the static instance and creates the managed thread; unregister_types.cpp
cleans up the server.
In register_server_types()
, Engine::get_singleton()->add_singleton
is used to register the dummy class in GDScript.
/* register_types.cpp */
#include "register_types.h"
#include "core/class_db.h"
#include "core/engine.h"
#include "hilbert_hotel.h"
static HilbertHotel *hilbert_hotel = NULL;
static _HilbertHotel *_hilbert_hotel = NULL;
void register_hilbert_hotel_types() {
hilbert_hotel = memnew(HilbertHotel);
hilbert_hotel->init();
_hilbert_hotel = memnew(_HilbertHotel);
ClassDB::register_class<_HilbertHotel>();
Engine::get_singleton()->add_singleton(Engine::Singleton("HilbertHotel", _HilbertHotel::get_singleton()));
}
void unregister_hilbert_hotel_types() {
if (hilbert_hotel) {
hilbert_hotel->finish();
memdelete(hilbert_hotel);
}
if (_hilbert_hotel) {
memdelete(_hilbert_hotel);
}
}
/* register_types.h */
/* Yes, the word in the middle must be the same as the module folder name */
void register_hilbert_hotel_types();
void unregister_hilbert_hotel_types();
The dummy class binds singleton methods to GDScript. In most cases, the dummy class methods wraps around.
Binding Signals
It is possible to emit signals to GDScript by calling the GDScript dummy object.
void HilbertHotel::_emit_occupy_room(uint64_t room, RID rid) {
_HilbertHotel::get_singleton()->_occupy_room(room, rid);
}
class _HilbertHotel : public Object {
GDCLASS(_HilbertHotel, Object);
friend class HilbertHotel;
static _HilbertHotel *singleton;
protected:
static void _bind_methods();
private:
void _occupy_room(int room_number, RID bus);
public:
RID create_bus();
void connect_signals();
bool delete_bus(RID id);
static _HilbertHotel *get_singleton();
Variant get_bus_info(RID id);
_HilbertHotel();
~_HilbertHotel();
};
#endif
MessageQueue
In order to send commands into SceneTree, MessageQueue is a thread-safe buffer to queue set and call methods for other threads. To queue a command, obtain the target object RID and use either push_call
, push_set
, or push_notification
to execute the desired behavior. The queue will be flushed whenever either SceneTree::idle
or SceneTree::iteration
is executed.
Here is the GDScript sample code:
extends Node
func _ready():
print("Start debugging")
HilbertHotel.connect("occupy_room", self, "_print_occupy_room")
var rid = HilbertHotel.create_bus()
OS.delay_msec(2000)
HilbertHotel.create_bus()
OS.delay_msec(2000)
HilbertHotel.create_bus()
OS.delay_msec(2000)
print(HilbertHotel.get_bus_info(rid))
HilbertHotel.delete_bus(rid)
print("Ready done")
func _print_occupy_room(room_number, r_id):
print("Room number: " + str(room_number) + ", RID: " + str(r_id))
The actual Hilbert Hotel is impossible.