Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Benutzerdefinierte Godot-Server

Einführung

Godot implementiert Multithreading als Server. Das sind Daemons, die Daten verwalten, verarbeiten und das Ergebnis übertragen. Server implementieren das Mediatormuster, das die Ressourcen-ID interpretiert und Daten für die Engine und andere Module verarbeitet. Darüber hinaus beansprucht der Server Ownership für seine RID-Zuweisungen.

Diese Anleitung setzt voraus, dass der Leser weiß, wie man C++-Module und Godot-Datentypen erstellt. Falls nicht, siehe Benutzerdefinierte Module in C++.

Referenzen

Wofür?

  • Künstliche Intelligenz hinzufügen.

  • Hinzufügen von benutzerdefinierten asynchronen Threads.

  • Unterstützung für ein neues Eingabegerät hinzufügen.

  • Schreibende Threads hinzufügen.

  • Hinzufügen eines benutzerdefinierten VoIP-Protokolls.

  • Und mehr...

Erstellen eines Godot Servers

Ein Server muss mindestens eine statische Instanz, einen Sleep-Timer, eine Thread-Schleife, einen Initialisierungsstatus und eine Aufräumprozedur haben.

hilbert_hotel.h
#ifndef HILBERT_HOTEL_H
#define HILBERT_HOTEL_H

#include "core/object/object.h"
#include "core/os/thread.h"
#include "core/os/mutex.h"
#include "core/templates/list.h"
#include "core/templates/rid.h"
#include "core/templates/set.h"
#include "core/variant/variant.h"

class HilbertHotel : public Object {
    GDCLASS(HilbertHotel, Object);

    static HilbertHotel *singleton;
    static void thread_func(void *p_udata);

private:
    bool thread_exited;
    mutable bool exit_thread;
    Thread *thread;
    Mutex *mutex;

public:
    static HilbertHotel *get_singleton();
    Error init();
    void lock();
    void unlock();
    void finish();

protected:
    static void _bind_methods();

private:
    uint64_t counter;
    RID_Owner<InfiniteBus> bus_owner;
    // https://github.com/godotengine/godot/blob/master/core/templates/rid.h
    Set<RID> buses;
    void _emit_occupy_room(uint64_t room, RID rid);

public:
    RID create_bus();
    Variant get_bus_info(RID id);
    bool empty();
    bool delete_bus(RID id);
    void clear();
    void register_rooms();
    HilbertHotel();
};

#endif
hilbert_hotel.cpp
#include "hilbert_hotel.h"

#include "core/variant/dictionary.h"
#include "core/os/os.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;
    Thread::wait_to_finish(thread);

    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/master/core/templates/rid.h
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_255.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,
        967,971,977,983,991,997,1009,1013,1019,
        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
};

Benutzerverwaltete Ressourcendaten

Godot-Server implementieren ein Mediator-Muster. Alle Datentypen erben RID_Data. RID_Owner<MyRID_Data> ist Eigentümer des Objekts, wenn make_rid aufgerufen wird. Nur im Debug-Modus verwaltet RID_Owner eine Liste von RIDs. In der Praxis sind RIDs ähnlich wie das Schreiben von objektorientiertem C-Code.

infinite_bus.h
class InfiniteBus : public RID_Data {
    RID self;

private:
    uint64_t prime_num;
    uint64_t num;

public:
    uint64_t next_room() {
        return prime_num * num++;
    }

    uint64_t get_bus_num() const {
        return prime_num;
    }

    uint64_t get_current_room() const {
        return prime_num * num;
    }

    _FORCE_INLINE_ void set_self(const RID &p_self) {
        self = p_self;
    }

    _FORCE_INLINE_ RID get_self() const {
        return self;
    }

    InfiniteBus(uint64_t prime) : prime_num(prime), num(1) {};
    ~InfiniteBus() {};
}

Referenzen

Registrierung der Klasse in GDScript

Server werden in register_types.cpp alloziert. Der Konstruktor setzt die statische Instanz und init() erzeugt den verwalteten Thread; unregister_types.cpp bereinigt den Server.

Da eine Godot-Server-Klasse eine Instanz erzeugt und diese an ein statisches Singleton bindet, verweist das Binden der Klasse möglicherweise nicht auf die richtige Instanz. Daher muss eine Dummy-Klasse erstellt werden, die auf den richtigen Godot-Server verweist.

In register_server_types(), wird Engine::get_singleton()->add_singleton verwendet, um die Dummy-Klasse in GDScript zu registrieren.

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();
register_types.cpp
#include "register_types.h"

#include "core/object/class_db.h"
#include "core/config/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);
    }
}

Methoden binden

Die Dummy-Klasse bindet Singleton-Methoden an GDScript. In den meisten Fällen werden die Methoden der Dummy-Klasse gewrappt.

Variant _HilbertHotel::get_bus_info(RID id) {
    return HilbertHotel::get_singleton()->get_bus_info(id);
}

Signale binden

Es ist möglich, Signale an GDScript zu senden, indem das GDScript-Dummyobjekt aufgerufen wird.

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
_HilbertHotel *_HilbertHotel::singleton = NULL;
_HilbertHotel *_HilbertHotel::get_singleton() { return singleton; }

RID _HilbertHotel::create_bus() {
    return HilbertHotel::get_singleton()->create_bus();
}

bool _HilbertHotel::delete_bus(RID rid) {
    return HilbertHotel::get_singleton()->delete_bus(rid);
}

void _HilbertHotel::_occupy_room(int room_number, RID bus) {
    emit_signal("occupy_room", room_number, bus);
}

Variant _HilbertHotel::get_bus_info(RID id) {
    return HilbertHotel::get_singleton()->get_bus_info(id);
}

void _HilbertHotel::_bind_methods() {
    ClassDB::bind_method(D_METHOD("get_bus_info", "r_id"), &_HilbertHotel::get_bus_info);
    ClassDB::bind_method(D_METHOD("create_bus"), &_HilbertHotel::create_bus);
    ClassDB::bind_method(D_METHOD("delete_bus"), &_HilbertHotel::delete_bus);
    ADD_SIGNAL(MethodInfo("occupy_room", PropertyInfo(Variant::INT, "room_number"), PropertyInfo(Variant::_RID, "r_id")));
}

void _HilbertHotel::connect_signals() {
    HilbertHotel::get_singleton()->connect("occupy_room", _HilbertHotel::get_singleton(), "_occupy_room");
}

_HilbertHotel::_HilbertHotel() {
    singleton = this;
}

_HilbertHotel::~_HilbertHotel() {
}

MessageQueue

Um Befehle in den SceneTree zu senden, ist MessageQueue ein thread-sicherer Puffer, um Set- und Call-Methoden für andere Threads in eine Warteschlange zu stellen. Um einen Befehl in die Warteschlange zu stellen, erhalten Sie die RID des Zielobjekts und verwenden entweder push_call, push_set oder push_notification, um das gewünschte Verhalten auszuführen. Die Warteschlange wird geleert, wenn entweder SceneTree::idle oder SceneTree::iteration ausgeführt wird.

Referenzen:

Fassen wir zusammen

Hier ist der GDScript-Beispiel-Code:

extends Node

func _ready():
    print("Start debugging")
    HilbertHotel.occupy_room.connect(_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))
    print(HilbertHotel.get_bus_info(r_id))

Anmerkungen

  • Das eigentliche Hilbert Hotel ist unmöglich.

  • Der Beispielcode für das Verbindungssignal ist ziemlich hacky.