#godot #gd-native

gdrust

一个简化与gdnative-rust交互的简单工具

1 个不稳定版本

0.1.0 2021年3月23日

#962游戏开发

MIT 许可证

27KB
234

gdrust

Rust

一个库,使gdnative-rust更接近GdScript。这包含两个主要部分

  1. 一个用于简化一些Rust代码并使其更接近GdScript的宏#[gdrust]
  2. 一组“不安全”的函数,在牺牲崩溃风险的同时使事情更简洁。

目标

最终,这个项目的目标是使90%的情况下Godot的Rust开发更简洁。可能存在一些只有“真正的”Rust才能解决的边缘情况,这个项目不应为了覆盖所有情况而牺牲其简洁性。

当前状态

目前,这个项目处于早期alpha阶段。已记录的部分应该按预期工作,但API可能会发生变化。

入门

gdrust基于gdnative-rust,因此您必须在查看gdrust之前设置gdnative-rust。遵循他们的入门指南

一旦安装了gdnative-rust,您可以通过将其添加为依赖项来安装gdrust。遗憾的是,由于gdnative-rust宏的工作方式,您必须同时将gdnative-rustgdrust添加为依赖项,并且必须选择兼容的版本。请参阅下面的“兼容性”部分。

[dependencies]
gdnative = "0.9.3"
gdrust = { git = "https://github.com/wyattjsmith1/gdrust.git" }

安装后,只需使用gdrust

use gdrust::macros::gdrust;
use gdnative::api::Node;

#[gdrust(extends = Node)]
struct HelloWorld {
    #[export]
    #[default(10)]
    test: u64,
}

就是这样!

下面将详细介绍导出属性和信号以及一个深入的全面示例。

#[gdrust]

导出类

#[gdrust]宏中的任何内容都可以导出。

#[gdrust(extends = Node)]
pub struct ClassName {
   // Same as `class_name ClassName extends Node` in GdScript.
}

extends = {classname}是可选的,如果您只是扩展Object,则可以省略。

#[gdrust::macros::gdrust]
struct ClassName {
    // Same as `class_name ClassName extends Object` in GdScript.
}

您仍然可以在类上拥有自定义的derives和attributes。任何在struct上的attributes将被添加

#[gdrust::macros::gdrust]
#[derive(Debug)]
struct ClassName {
    // `ClassName` will derive `Debug`
}

创建类并导出属性和信号后,像往常一样创建您的 impl 块。注意,您不应创建 new 函数。这是由宏提供的。


#[gdnative::methods]
impl ClassName {
    #[export]
    fn _ready(&self, _owner: TRef<KinematicBody>) {
        gdnative::godot_print!("Hello World!")
    }
}

导出属性

导出属性的语法旨在尽可能接近 GdScript。由于即将发布的 4.0 版本,gdrust 使用了 4.0 导出。您可以在那里了解所有不同类型的导出。

通常,使用属性语法(#[export_...),并从 GdScript 导出中移除起始的 @。例如

@export_range(1, 10, 2, "or_greater") var my_range: int

变为

#[export_range(1, 10, 2, "or_greater")]
my_range: i32 // or i64 if you want

所有内容都应该按照 Godot 文档中的定义实现,除了以下内容

  1. #[no_export] 可以用来不导出一个变量。这应该用于所有 Rust 本地类型(不实现 Export)或如果您希望变量为“私有”。
  2. 4.0 文档将 @export_node_path(Type1, Type2) 定义为导出 NodePath 的方法,该路径仅匹配具有给定类型的节点。这已部分实现,但不会在 4.0 中完成,因为目前没有 NodePaths 的导出提示。您目前可以在代码中包含此导出,但它将允许任何类型的 NodePath
  3. 可空性使用 Option 处理。
  4. 每个导出属性都需要一个类型和一个默认值。如果没有提供默认值,将使用 Default::default()。如果您引用的是 Godot 对象而不是“原始”对象,则必须将其包装在 Ref 中。
  5. 目前,不支持数组。这仅仅是因为我还没有信心语法已经最终确定。在 Godot 的网站上,它显示传统的 export(Array, int) var ints = [1, 2, 3]。我猜它们将切换到某种 @export_array 风格。一旦这确定,添加它应该很容易。

默认值

您可以使用 #[default(value)] 注解设置自定义默认值。如果没有定义,将使用 Default::default()

导出信号

导出信号的语法也旨在尽可能接近 GdScript。语法是

#[gdrust]
#[signal(signal_name(arg_name: I64, arg2_name: F64 = 10.0))]
#[signal(other_signal(arg_name: Bool = true, arg2_name: GodotString = "default"))]
struct Class;

与属性类似,信号也有一些需要注意的地方

  1. 与属性一样,每个信号都必须有一个类型。与属性不同,类型必须是以下之一
  • VariantType
  • 没有Ref(例如KinematicBody)的Godot对象。

我知道这有点奇怪,我想让它更加平滑。欢迎提出建议。

  1. 与GdScript不同,gdrust信号参数可以有可选的默认值。

当信号导出时,将会有一个具有其名称的const。看看下面的例子中的simple_signal信号,看看它是如何使用的。

综合示例

此示例应包含导出属性和信号的所有可能性。它也用于测试。

use gdnative::api::{KinematicBody, Node, RigidBody, Texture};
use gdnative::prelude::{Color, InitHandle, NodePath, ToVariant};
use gdnative::{godot_init, Ref, TRef};
use gdrust::macros::gdrust;

#[gdrust(extends = Node)]
#[signal(my_signal(arg1: F64, arg2: GodotString = "test".to_string()))]
#[signal(simple_signal(arg:I64))]
#[derive(Debug)]
struct HelloWorld {
    #[export]
    #[default(10)]
    test_a: u8,

    #[no_export]
    test_failure: u8,

    #[default(10.0)]
    test_c: f32,

    #[export_range(0.0, 10.0)]
    simple_range: f32,

    #[export_range(0, 10, 2)]
    #[default(2)]
    step_range: u8,

    #[export_range(0, 10, "or_lesser")]
    #[default(10)]
    simple_range_or_lesser: u64,

    #[export_range(0.0, 10.0, 1.5, "or_lesser")]
    #[default(10.0)]
    simple_range_step_or_lesser: f64,

    #[export_range(0, 10, "or_greater")]
    #[default(10)]
    simple_range_or_greater: u64,

    #[export_range(0, 10, 10, "or_greater")]
    #[default(10)]
    simple_range_step_or_greater: u64,

    #[export_range(0, 10, 10, "or_lesser", "or_greater")]
    #[default(10)]
    range_with_all: u64,

    #[export]
    texture: Option<Ref<Texture>>,

    #[export_enum("This", "is", "a", "test")]
    #[default("This".to_string())]
    string_enum: String,

    #[export_enum("This", "will", "be", "enum", "ordinals")]
    int_enum: u32,

    #[export_file]
    file: String,

    #[export_file("*.png")]
    png_file: String,

    #[export_dir]
    dir: String,

    #[export_global_file("*.png")]
    glob_file: String,

    #[export_global_dir]
    glob_dir: String,

    #[export_multiline]
    #[default("This\nis\nmultiline\ntext".to_string())]
    multiline: String,

    #[export_exp_range(0.0, 10.0)]
    simple_exp_range: f32,

    #[export_exp_range(0, 10, 2)]
    #[default(2)]
    step_exp_range: u8,

    #[export_exp_range(0, 10, "or_lesser")]
    #[default(10)]
    simple_exp_range_or_lesser: u64,

    #[export_exp_range(0.0, 10.0, 1.5, "or_lesser")]
    #[default(10.0)]
    simple_exp_range_step_or_lesser: f64,

    #[export_exp_range(0, 10, "or_greater")]
    #[default(10)]
    simple_exp_range_or_greater: u64,

    #[export_exp_range(0, 10, 10, "or_greater")]
    #[default(10)]
    simple_exp_range_step_or_greater: u64,

    #[export_exp_range(0, 10, 10, "or_lesser", "or_greater")]
    #[default(10)]
    exp_range_with_all: u64,

    #[export]
    #[default(Color::rgba(0.0, 0.0, 0.0, 0.5))]
    color: Color,

    #[export_color_no_alpha]
    #[default(Color::rgb(0.0, 0.0, 0.0))]
    color_no_alpha: Color,

    #[export_flags("Fire", "Water", "Earth", "Wind")]
    spell_elements: u32,

    // TODO: NodePath types are only supported in 4.0
    #[export_node_path(KinematicBody, RigidBody)]
    physics_body: NodePath,

    #[export_flags_2d_physics]
    layers_2d_physics: u32,

    #[export_flags_2d_render]
    layers_2d_render: u32,

    #[export_flags_3d_physics]
    layers_3d_physics: u32,

    #[export_flags_3d_render]
    layers_3d_render: u32,
}

#[gdnative::methods]
impl HelloWorld {
    #[export]
    fn _ready(&self, owner: TRef<Node>) {
        gdnative::godot_print!("Hello World!");
        gdnative::godot_dbg!(self);
        owner
           .upcast::<Node>()
           .emit_signal(Self::SIMPLE_SIGNAL, &[0.to_variant()]);
    }
}

优缺点

像任何软件一样,它也并非没有问题。这个列表旨在准确记录优缺点,以帮助人们决定这是否是适合他们的项目。

优点

  1. 简化了ClassBuilder链,使代码看起来更像GdScript
  2. 生成一个新的new
  3. 将属性默认值与new的默认值同步。不再更改默认属性值而代码中没有反映。

缺点

  1. 像许多宏一样,当输入正确时,它们工作得很好。当输入无效时,它们会给出模糊的错误信息。我正在尝试用清晰的错误信息覆盖大多数常见的错误情况。如果您看到奇怪的消息,请提交问题,我会帮助您解决问题。一般来说,#[export*应始终使用相同类型的文字(所有整数或所有浮点数)。

不安全函数

Rust的其中一个优点是它迫使你处理每一个可能的案例,以确保运行时顺利。一个问题是在游戏开发中充满了“希望这能行”的情况,直到运行时才忽略错误处理。

例如,假设你想获取一个子节点并调用set_emitting()。在gdnative-rust中,你会这样做

unsafe {
    owner.get_node("Particles")
        .unwrap()
        .assume_safe()
        .cast::<Particles>()
        .unwrap()
        .set_emitting(true);
}

与GdStript(没有$糖)比较

get_node("Particles").start_emitting()

是的,静态类型确实在Rust示例中引起了一些冗余,但这仍然很多。gdrust提供了一个更干净的方法

owner.expect_node::<Particles, _>("Particles").set_emitting(true)

并不像GdScript那样简洁,但仍然比gdnative-rust简洁。要注意的一点是:这个函数几乎就是上面代码的字面翻译。有一个显式的unsafe块和各种unwraps。这是非常不安全的,但这个函数什么时候会失败?只有在你请求一个无效的节点或破坏内存模型时。Rust被设计成让你恢复,但你怎么在运行时恢复丢失的节点?你可能只是为了安抚编译器而unwrap

因此,这被称为unsafe_functions,因为在Rust看来这是不安全的,但与GdScript相比,这相当正常且安全。

你绝对应该了解每个方法可以产生的恐慌,并理解gdnative-rust的内存模型。一旦你做到了,你应该能够正确地判断何时使用这些辅助函数。

兼容性

遗憾的是,gdrust需要依赖gdnative,由于gdnative宏的工作方式,它不能被pub use。因此,您必须确保您拥有兼容的gdrustgdnative版本。此表将更新为所有兼容版本

gdrust gdnative-rust
0.1.0 0.9.+

阅读更多

许可:MIT

依赖项

~4–7MB
~128K SLoC