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.

自訂 Godot 伺服器

前言

Godot 以伺服器的方式實作多執行緒。伺服器是管理資料、處理資料並推送結果的常駐程式(daemon)。伺服器實現了中介者模式,能解譯資源 ID 並為引擎及其他模組處理資料。此外,伺服器會主張其 RID 配置的擁有權。

本指南假設讀者已瞭解如何建立 C++ 模組與 Godot 資料型別。如未熟悉,請先參閱 以 C++ 自訂模組

參考資料

用途說明?

  • 新增人工智慧。

  • 新增自訂非同步執行緒。

  • 新增對新輸入裝置的支援。

  • 新增寫入執行緒。

  • 新增自訂 VoIP 協定。

  • 更多用途……

建立 Godot 伺服器

最低需求下,伺服器必須具備靜態實體、睡眠計時器、執行緒循環、初始化狀態與清理程序。

hilbert_hotel.h
#pragma once

#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();
};
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_Data。當呼叫 make_rid 時,RID_Owner<MyRID_Data> 會擁有該物件。僅在除錯模式下,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_callpush_setpush_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))

說明