自定义 Godot 服务器

前言

Godot 以服务器的形式实现多线程。服务器是用来管理数据、处理数据、推送结果的守护服务。服务器实现了中介者模式,能够为引擎和其他模块解释资源 ID、处理数据。另外服务器还拥有分配的 RID 的所有权。

本指南需要读者知道如何创建 C++ 模块、了解 Godot 的数据类型。如果你还没有准备好,请参考 自定义 C++ 模块

参考

可以做什么?

  • 添加人工智能。

  • 添加自定义异步线程。

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

  • 添加写线程。

  • 添加自定义 VoIP 协议。

  • 以及更多……

创建 Godot 服务器

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

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

自定义托管资源数据

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

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

参考

在 GDScript 中注册类

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

由于 Godot 的服务器类会创建实例并将其与静态单例绑定,将类绑定后可能引用错误的实例。因此,必须创建一个虚设类,引用正确的 Godot 服务器。

register_server_types() 中使用 Engine :: get_singleton() -> add_singleton 向 GDScript 注册虚设类。

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

绑定方法

虚设类将单例方法绑定到 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.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))

注意