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.

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 。这样做也有助于在加载资源时对它们进行缓存。

参考: