Serveurs Godot personnalisés

Introduction

Godot met en œuvre le multithreading en tant que serveurs. Les serveurs sont des processus en arrière plan qui gèrent les données, les traitent et poussent le résultat. Les serveurs implémentent le patron de conception Mediator qui interprète l'identification des ressources ID et traite les données pour le moteur et les autres modules. De plus, le serveur revendique la propriété de ses allocations RID.

Ce guide part du principe que le lecteur sait comment créer des modules C++ et des types de données Godot. Si ce n'est pas le cas, voir Modules personnalisées en C++.

Pourquoi ?

  • Ajouter une intelligence artificielle.
  • Ajouter des threads asynchrones personnalisés.
  • Ajouter la prise en charge d'un nouveau périphérique d'entrées.
  • Ajout de threads d'écriture.
  • Ajouter un protocole de VoIP personnalisé.
  • Et plus...

Créer un serveur Godot

Au minimum, un serveur doit avoir une instance statique, un minuteur de sommeil, une boucle d’exécution, un état d'initialisation et une procédure de nettoyage.

#ifndef HILBERT_HOTEL_H
#define HILBERT_HOTEL_H

#include "core/list.h"
#include "core/object.h"
#include "core/os/thread.h"
#include "core/os/mutex.h"
#include "core/rid.h"
#include "core/set.h"
#include "core/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/rid.h#L196
        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
#include "hilbert_hotel.h"

#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;
        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/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,
                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
};

Données de ressources gérées sur mesure

Les serveurs Godot mettent en œuvre un modèle médiateur. Tous les types de données héritent de RID_Data. RID_Owner<MyRID_Data> possède l'objet lorsque make_rid est appelé. En mode de débogage uniquement, RID_Owner maintient une liste de RIDs. En pratique, les RIDs sont similaires à l'écriture de code C orienté objet.

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() {};
}

References

Enregistrement de la classe dans GDScript

Les serveurs sont alloués dans register_types.cpp. Le constructeur définit l'instance statique et init() créé le thread associé ; unregister_types.cpp nettoie le serveur.

Puisqu'une classe serveur Godot créée une instance et la lie à un singleton statique, lier la classe pourrait ne pas faire référence à la bonne instance. Toutefois, une classe factice doit être créée pour référencer le serveur Godot approprié.

Dans register_server_types(), Engine::get_singleton()->add_singleton est utilisé pour enregistrer la classe factice dans 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();

Méthodes liées

La classe factice lie les méthodes singleton au GDScript. Dans la plupart des cas, les méthodes de la classe factices sont englobées.

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

Signaux de liaison

Il est possible d'émettre des signaux vers GDScript en appelant l'objet factice GDScript.

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

Afin d'envoyer des commandes dans SceneTree, MessageQueue est un tampon thread-safe qui permet de mettre en file d'attente et d'appeler des méthodes pour d'autres threads. Pour mettre une commande en file d'attente, obtenez le RID de l'objet cible et utilisez soit push_call, push_set, ou push_notification pour exécuter le comportement souhaité. La file d'attente sera vidée à chaque fois que soit SceneTree::idle soit SceneTree::iteration sera exécuté.

Références :

En résumé

Voici un exemple de code en GDScript :

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))
    print(HilbertHotel.get_bus_info(r_id))

Notes

  • Le véritable Hilbert Hotel est impossible.
  • Le code d'exemple de connexion de signal est assez hacky.