自定义 Godot 服务器
前言
Godot将多线程实现为服务器. 服务器是管理数据, 处理数据和推送结果的守护进程. 服务器实现中介模式, 该模式解释引擎和其他模块的资源ID和处理数据. 此外, 服务器声明其RID分配的所有权.
本指南假设读者知道如何创建C++模块和Godot数据类型. 如果没有, 请参考 自定义 C++ 模块.
参考
可以做什么?
添加人工智能.
添加自定义异步线程.
添加对新输入设备的支持.
添加写线程.
添加自定义 VoIP 协议。
以及更多……
创建 Godot 服务器
服务器至少必须拥有静态的实例、睡眠计时器、线程循环、初始化状态、清理过程。
#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/3.x/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/3.x/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
};
自定义托管资源数据
Godot 服务器实现了中介模式。所有数据类型都继承 RID_Data
。RID_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/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();
绑定方法
虚拟类将单例方法绑定到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::idle
或 SceneTree::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))
注意
实际的 希尔伯特旅馆悖论 是不可能的.
连接信号的示例代码是很旁门左道的.