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.
Checking the stable version of the documentation...
Object 类
参见
本页介绍了Godot中对象的C++实现. 寻找Object类参考? 请看这里.
一般定义
Object 是几乎所有事物的基类。Godot 中的大多数类都直接或间接地继承自它。声明它们非常简单,只需要使用一个类似这样的宏就行啦:
class CustomObject : public Object {
GDCLASS(CustomObject, Object); // This is required to inherit from Object.
};
Object(对象)自带了大量的内置功能,比如反射(reflection)和可编辑的属性(editable properties):
CustomObject *obj = memnew(CustomObject);
print_line("Object class: ", obj->get_class()); // print object class
OtherClass *obj2 = Object::cast_to<OtherClass>(obj); // Converting between classes, similar to dynamic_cast
参考:
注册对象类
大多数 Object 的子类都是通过调用 GDREGISTER_CLASS 来注册的。
GDREGISTER_CLASS(MyCustomClass)
这会将它作为一个公开的命名类注册到 ClassDB 中,从而允许该类被脚本、代码或反序列化过程实例化。需要注意的是,被注册为 GDREGISTER_CLASS 的类,应该做好随时被自动实例化或释放的准备,比如可能会被编辑器或者文档系统自动调用。
除了 GDREGISTER_CLASS 之外,还有几种其他的‘私有化’模式:
// Registers the class publicly, but prevents automatic instantiation through ClassDB.
GDREGISTER_VIRTUAL_CLASS(MyCustomClass);
// Registers the class publicly, but prevents all instantiation through ClassDB.
GDREGISTER_ABSTRACT_CLASS(MyCustomClass);
// Registers the class in ClassDB, but marks it as private,
// such that it is not visible to scripts or extensions.
// This is the same as not registering the class explicitly at all
// - in this case, the class is registered as internal automatically
// when it is first constructed.
GDREGISTER_INTERNAL_CLASS(MyCustomClass);
// Registers the class such that it is only available at runtime (but not in the editor).
GDREGISTER_RUNTIME_CLASS(MyCustomClass);
你也可以使用 GDSOFTCLASS(MyCustomClass, SuperClass) 来代替 GDCLASS(MyCustomClass, SuperClass)。通过这种方式定义的类,根本不会在 ClassDB (Godot 的全局类数据库)中进行注册。这种做法有时会用于特定于平台的子类。
注册绑定
派生自 Object(对象)的类,可以重写(override)一个名为 static void _bind_methods()。当该类被注册到引擎时,这个静态函数就会被调用,用来注册该类所有的对象方法、属性、常量等等。它只会被调用一次。
在 _bind_methods 里面, 有几件事可以做. 注册函数是一个:
ClassDB::bind_method(D_METHOD("methodname", "arg1name", "arg2name", "arg3name"), &MyCustomType::method);
参数的默认值可以在(参数列表的)末尾传入:
ClassDB::bind_method(D_METHOD("methodname", "arg1name", "arg2name", "arg3name"), &MyCustomType::method, DEFVAL(-1), DEFVAL(-2)); // Default values for arg2name (-1) and arg3name (-2).
默认值必须按照它们声明的顺序来提供,跳过那些必填的参数,然后只为可选的参数提供默认值。这与 C++ 中声明方法的语法是保持一致的。
D_METHOD 是一个宏,它将 “methodname” 转换为 StringName 以提高效率。参数名称用于自我检查,但在发布时进行编译时,宏会忽略它们,因此未使用字符串从而对其进行了优化。
有关更多示例, 请查看Control或Object的 _bind_methods .
如果只是添加不希望被彻底记录的模块和功能, 可以安全地忽略 D_METHOD() 宏, 并且为了简洁起见, 可以传递传递名称的字符串.
参考:
常量
类通常有枚举, 例如:
enum SomeMode {
MODE_FIRST,
MODE_SECOND
};
为了让这些枚举在绑定到方法时能够正常工作,必须将它们声明为可转换为 int(整数)的类型。为此,引擎提供了一个宏(macro)来协助完成这件事:
VARIANT_ENUM_CAST(MyClass::SomeMode); // now functions that take SomeMode can be bound.
常量也可以绑定在 _bind_methods 中, 通过使用:
BIND_CONSTANT(MODE_FIRST);
BIND_CONSTANT(MODE_SECOND);
属性(设置/获取)
对象导出属性, 这些属性可用于以下用途:
序列化和反序列化对象.
为Object派生类创建可编辑值列表.
属性通常由 PropertyInfo() 类来定义,其构造方式如下:
PropertyInfo(type, name, hint, hint_string, usage_flags)
例如:
PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "0,49,1", PROPERTY_USAGE_EDITOR)
这是一个名为 "amount" 的整数(integer)属性。它的提示类型(hint)被设定为范围(range),该范围从 0 到 49,步进为 1(即每次增减 1 个整数)。这个设定仅用于编辑器(方便在界面上直观地调整数值),但不会被序列化(即不会随资源一起被保存下来)。
另一个示例:
PropertyInfo(Variant::STRING, "modes", PROPERTY_HINT_ENUM, "Enabled,Disabled,Turbo")
这是一个字符串属性,可以接受任何字符串,但编辑器只允许定义的提示字符串。由于未指定使用标志,因此默认值为 PROPERTY_USAGE_STORAGE 和 PROPERTY_USAGE_EDITOR。
在object.h中有很多提示和用法标记, 请对其进行检查.
属性也可以像 C# 属性一样工作,并且可以使用索引从脚本访问,但通常不鼓励这种用法,因为使用函数是易读性的首选。许多属性也与类别绑定,例如“动画/帧”,除非使用运算符 [],否则也无法建立索引。
从 _bind_methods() 开始, 只要存在set/get函数, 就可以创建和绑定属性. 例如:
ADD_PROPERTY(PropertyInfo(Variant::INT, "amount"), "set_amount", "get_amount")
这将使用setter和getter创建属性.
使用 _set/_get/_get_property_list 绑定属性
当需要更大的灵活性时(即在上下文中添加或删除属性), 存在另一种创建属性的方法.
可以在 Object 派生类中重写以下函数,它们不是虚函数,不要将它们设置为虚,它们会在每次重写时调用,而之前的函数不会失效(多级调用)。
protected:
void _get_property_list(List<PropertyInfo> *r_props) const; // return list of properties
bool _get(const StringName &p_property, Variant &r_value) const; // return true if property was found
bool _set(const StringName &p_property, const Variant &p_value); // return true if property was found
由于 p_property 必须按顺序与所需的名称进行比较, 因此效率也较低.
信号
对象可以定义一组信号(类似于其他编程语言中的 Delegate/委托)。这个例子展示了如何连接(绑定)到这些信号:
// This is the function signature:
//
// Error connect(const StringName &p_signal, const Callable &p_callable, uint32_t p_flags = 0)
//
// For example:
obj->connect("signal_name_here", callable_mp(this, &MyCustomType::method), CONNECT_DEFERRED);
callable_mp 是一个宏,用来创建一个指向成员函数的自定义可调用函数指针。关于 p_flags 参数的取值,请参见 ConnectFlags 。
使用 ADD_SIGNAL 宏在 _bind_methods 中添加信号到类中, 例如:
ADD_SIGNAL(MethodInfo("been_killed"))
“对象所有权” 和 “类型转换”
对象都是在堆(heap)上分配内存的。它们主要有两种不同的所有权模型:
继承自
RefCounted的对象,是采用引用计数来管理内存的。所有其他(不属于前面提到的那些特殊类别的)对象,都采用手动内存管理。
这些(对象)的所有权模型有着根本性的区别。请分别参考各自对应的章节,来学习如何创建、存储以及释放该对象。
当你拿到一个通过 Object * 传给你的对象,但不确定它到底是不是 RefCounted 时,如果你需要把它保存下来,你应该存储它的 ObjectID 而不是直接存指针(具体原因在下面‘手动内存管理’那一节有解释)。
当一个对象通过 Variant 传递给你时——尤其是在使用延迟回调(deferred callbacks)的情况下——很有可能等到你的函数真正开始执行时,里面包含的那个 Object * 已经被释放掉了。因此,你不应该直接把它转换成 Object * ,而是应该使用 get_validated_object 这个方法:
void do_something(Variant p_variant) {
Object *object = p_variant.get_validated_object();
ERR_FAIL_NULL(object);
}
Manual memory management(手动内存管理)
需要手动管理内存的对象,是通过 memnew 来创建,并使用 memdelete 来释放的:
Node *node = memnew(Node);
// ...
memdelete(node);
node = nullptr;
当你并非某个对象的唯一所有者时,直接存储指向它的指针是非常危险的:该对象随时可能因为其他引用被释放(销毁),从而导致你手里的指针变成悬空指针,这最终必然会引发程序崩溃。
当你存储那些并非由你独占所有权的对象时,你应该保存它的 ObjectID ,而不是直接保存指针。
Node *node = memnew(Node);
ObjectID node_id = node.get_instance_id();
// ...
Object *maybe_node = ObjectDB::get_instance(node_id);
ERR_FAIL_NULL(maybe_node); // The node may have been freed between calls.
RefCounted 内存管理
RefCounted 的子类采用 reference counting semantics 来进行内存管理。
它们需要使用 memnew 来创建,并且应该保存在 Ref 实例中。当最后一个 Ref 实例被销毁(或释放)时,该对象会自动自我销毁。
class MyRefCounted: public RefCounted {
GDCLASS(MyReference, RefCounted);
};
Ref<MyRefCounted> my_ref = memnew(MyRefCounted);
// ...
// Ref holds shared ownership over the object, so the object
// will not be freed. As long as you have a valid, non-null
// Ref, it can be safely assumed the object is still valid.
my_ref->get_class_name();
你永远不应该对 RefCounted 的子类调用 memdelete ,因为该对象可能还有其他拥有者(持有者)。
你也绝对不应该使用裸指针(raw pointers)来存储 RefCounted 的子类,比如写成 RefCounted *object = memnew(RefCounted) 这样。这种做法非常不安全,因为其他的引用者(owners)可能会把这个对象销毁掉,导致你手里的指针变成一个‘悬空指针(dangling pointer)’,最终必然会引发程序崩溃。
参考:
动态转型
Godot在Object派生类之间提供动态转换, 例如:
void some_func(Object *p_object) {
Button *button = Object::cast_to<Button>(p_object);
}
如果类型转换失败,会返回 nullptr 。它的工作机制和 C++ 标准的 dynamic_cast 一模一样,但区别在于它并没有使用 C++ RTTI (运行时类型信息)。
通知
Godot 中的所有对象都有一个 _notification 方法,这让它们能够响应引擎底层可能与之相关的各种回调。更多相关信息可以在 Godot 通知 (Godot 通知机制)页面中找到。
资源
Resource 类继承自 RefCounted(引用计数类),因此所有的资源都是基于引用计数的。资源可以选择性地包含一个路径,用来指向磁盘上的某个文件。这个路径可以通过 resource.set_path(path) 来设置,不过通常情况下都是由资源加载器(resource loader)自动完成的。任何两个不同的资源都不能拥有相同的路径,如果尝试这么做,引擎就会报错。
资源也可以没有路径.
参考:
资源加载
可以使用ResourceLoader API加载资源, 如下所示:
Ref<Resource> res = ResourceLoader::load("res://someresource.res")
如果先前已加载对该资源的引用并且该引用在内存中, 则 ResourceLoader 将返回该引用. 这意味着只能同时从磁盘上引用的文件加载一个资源.
参考:
资源保存
可以使用资源保存器API保存资源:
ResourceSaver::save("res://someresource.res", instance)
该实例将会被保存,而那些拥有文件路径的子资源,会被保存为对该资源的引用(也就是存个路径)。没有文件路径的子资源则会和当前保存的资源打包在一起,并被分配一个子ID,格式类似于 res://someresource.res::1 。这样做也有助于在加载资源时对它们进行缓存。