Up to date

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

自定义 Godot 服务器

前言

Godot将多线程实现为服务器. 服务器是管理数据, 处理数据和推送结果的守护进程. 服务器实现中介模式, 该模式解释引擎和其他模块的资源ID和处理数据. 此外, 服务器声明其RID分配的所有权.

This guide assumes the reader knows how to create C++ modules and Godot data types. If not, refer to 自定义 C++ 模块.

参考

可以做什么?

  • 添加人工智能.

  • 添加自定义异步线程.

  • 添加对新输入设备的支持.

  • 添加写线程.

  • 添加自定义 VoIP 协议。

  • 以及更多……

创建 Godot 服务器

服务器至少必须拥有静态的实例、睡眠计时器、线程循环、初始化状态、清理过程。

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

自定义托管资源数据

Godot 服务器实现了中介模式。所有数据类型都继承 RID_DataRID_Owner <MyRID_Data> 在调用 make_rid 时拥有对象。仅在调试模式期间,RID_Owner 维护 RID 列表。实际上,RID 类似于编写面向对象的 C 代码。

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

参考

在 GDScript 中注册类

服务器在 register_types.cpp 中分配。构造函数设置静态实例,init() 创建托管线程;unregister_types.cpp 清理服务器。

由于Godot服务类创建了一个实例并将其绑定到静态单例, 因此绑定该类可能不会引用正确的实例. 因此, 必须创建一个虚拟类来引用正确的Godot服务.

register_server_types() 中,Engine :: get_singleton() -> add_singleton 用于在 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();

绑定方法

虚拟类将单例方法绑定到GDScript. 在大多数情况下, 虚拟类方法封装在内.

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

绑定信号

可以通过调用GDScript虚拟对象向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

为了将命令发送到SceneTree中,MessageQueue是线程安全的缓冲区, 用于将其他线程的设置和调用方法排队. 要对命令进行排队, 请获取目标对象RID并使用 push_call, push_set, 或 push_notification 执行所需的行为. 每当执行 SceneTree::idleSceneTree::iteration 时, 都会刷新队列.

参考:

总结

这是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))

注意