Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Servidores Godot personalizados

Introducción

Godot implementa el multihilo como servidores. Los servidores son demonios que gestionan datos, los procesan y envían el resultado. Los servidores implementan el patrón mediador, que interpreta el ID del recurso y procesa datos para el motor y otros módulos. Además, el servidor reclama la propiedad de sus asignaciones de RID (Resource ID).

This guide assumes the reader knows how to create C++ modules and Godot data types. If not, refer to Módulos personalizados en C++.

Referencias

¿Para qué?

  • Agregando inteligencia artificial.

  • Agregando hilos asincronos personalizados.

  • Agregando soporte para un nuevo dispositivo de entrada.

  • Añadiendo hilos de escritura.

  • Agregando protocolo VoIP personalizado.

  • Y mas...

Creando un servidor Godot

Como mínimo, un servidor debe tener una instancia estática, un temporizador de pausa (sleep timer), un bucle de hilo, un estado de inicialización y un procedimiento de limpieza.

#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
#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_225.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
};

Datos de recursos personalizados gestionados

Los servidores de Godot implementan un patrón mediador. Todos los tipos de datos heredan de RID_Data. RID_Owner<MyRID_Data> es el propietario del objeto cuando se llama a make_rid. Durante el modo de depuración solamente, RID_Owner mantiene una lista de RIDs. En la práctica, los RIDs son similares a escribir código en C orientado a objetos.

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

Referencias

Registrar una clase en GDScript

Los servidores se asignan en register_types.cpp. El constructor establece la instancia estática y init() crea el hilo gestionado; mientras que unregister_types.cpp limpia el servidor.

Ya que una clase de servidor Godot crea una instancia y la enlaza a un singleton estático, enlazar la clase podría no hacer referencia a la instancia correcta. Por lo tanto, se debe crear una clase dummy para hacer referencia al servidor Godot apropiado.

En register_server_types(), se utiliza Engine::get_singleton()->add_singleton para registrar la clase ficticia (dummy class) en GDScript.

/* 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);
        }
}
/* 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étodos de vinculación

La clase dummy une los métodos singleton a GDScript. En la mayoría de los casos, los métodos de la clase dummy se utilizan.

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

Señales Binding

Es posible emitir señales a GDScript llamando al objeto ficticio de 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

Para enviar comandos al SceneTree, MessageQueue es un búfer seguro para hilos que encola métodos set y call para otros hilos. Para encolar un comando, obtén el RID del objeto destino y utiliza push_call, push_set, o push_notification para ejecutar el comportamiento deseado. La cola se vaciará siempre que se ejecute SceneTree::idle o SceneTree::iteration.

Referencias:

En resumen

Aquí está el código de ejemplo de 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))

Notas