#bevy-ui #file-format #bevy #ui #scene-description #dsl #cuicui-dsl

cuicui_chirp

基于cuicui_dsl描述bevy UIs的文件格式

6个版本 (3个破坏性更新)

0.12.0 2023年11月10日
0.11.0 2023年11月4日
0.10.2 2023年10月25日
0.9.0 2023年8月30日

#881 in 游戏开发

每月下载量31次
4 crates 中使用

MIT/Apache

240KB
4K SLoC

cuicui_chirp

The Book Documentation

cuicui_chirp 定义了一个基于文本的bevy场景描述的文件格式。

它用于 cuicui 的UI,但可以描述任何类型的场景。

它包括

  • 文件格式的解析器。
  • 一个bevy加载器,用于在bevy中加载这些文件,包括 loader::Plugin
  • 一个特质 (ParseDsl),用于将您的自定义类型的方法用作chirp方法
  • 一个宏来自动实现此特质 (parse_dsl_impl)

语法非常接近于 cuicui_dsldsl! 宏,增加了 一些功能

何时使用 cuicui_chirp

  • 您想要一个强大且可扩展的场景定义格式,用于替换bevy的内置 cmds.spawn().insert().with_children() 操作。
  • 您希望使用热重载和有用的错误消息来实现快速迭代时间。
  • 您希望最大限度地减少编写rust代码以管理场景的数量。
  • 您希望使用可重用的场景定义格式。

请注意,cuicui_chirp由于其本质,不是一个小的依赖项。如果依赖项的大小对您很重要,请考虑使用cuicui_dsl

此外,截至0.10cuicui_chirp不支持WASM用于图像和字体资源。

如何使用cuicui_chirp

Cargo功能

  • fancy_errors(默认):以美观的格式打印解析错误信息。
  • macros(默认):定义并导出parse_dsl_impl
  • load_font(默认):将Handle<Font>作为方法参数加载
  • load_image(默认):将Handle<Image>作为方法参数加载
  • more_unsafe:将一些运行时检查转换为不安全的假设。从理论上讲,这是合理的,但根据我的口味,cuicui_chirp还没有经过足够的测试,以默认方式做出这些假设。

用法

cuicui_chirp读取以.chirp结尾的文件。要加载.chirp文件,请使用以下ChirpBundle

# #[cfg(feature = "doc_and_test")] mod test {
# use cuicui_chirp::__doc_helpers::*; // ignore this line pls
use bevy::prelude::*;
use cuicui_chirp::ChirpBundle;

fn setup(mut cmds: Commands, assets: Res<AssetServer>) {
    cmds.spawn((Camera2dBundle::default(), LayoutRootCamera));
    cmds.spawn(ChirpBundle::from(assets.load("my_scene.chirp")));
}
# }

但是,您需要添加加载插件(loader::Plugin),这样才能正常工作。插件参数化于DSL类型。DSL类型需要实现ParseDsl特质。

以下是一个使用cuicui_layout_bevy_ui的DSL的示例

# #[cfg(feature = "doc_and_test")] mod test {
# use cuicui_chirp::__doc_helpers::*; // ignore this line pls
# fn setup() {}
use bevy::prelude::*;
use cuicui_layout_bevy_ui::UiDsl;

fn main() {
    App::new()
      .add_plugins((
        DefaultPlugins,
        cuicui_chirp::loader::Plugin::new::<UiDsl>(),
      ))
      .add_systems(Startup, setup)
      .run();
}
# }

文档

chirp文件中可用的方法是所选DSL类型中可用的方法(在这种情况下,将是UiDsl方法)。检查您使用的相应DSL类型的文档页面。所有接受&mut self的方法都是候选项。

DSL特定文档

括号内的标识符是ParseDsl上的方法。

由于chirp格式是ParseDsl的包装,请参考您添加的ParseDsl impl上的方法。

使DslBundlecuicui_chirp兼容

让我们重新使用cuicui_dsl中的示例,并将其扩展到与cuicui_chirp一起工作。

我们有一个实现了 DslBundleMyDsl,现在我们还需要为其实现 ParseDsl。这样就可以在 ParseDsl 中访问方法,使用 parse_dsl_impl 属性宏,并将其添加到所有 DSL 方法定义的 impl 块中

     font_size: f32,
 }
+#[cuicui_chirp::parse_dsl_impl]
 impl MyDsl {
     pub fn style(&mut self, style: Style) {
         self.style = style;

是的,简单情况下就是这样。如果你想要利用热重载,请确保方法内部不要引发恐慌。

.chirp 文件格式

基本语法类似于 cuicui_dsldsl! 宏。

一个主要区别是,代码块被替换为一个函数注册表。你可以使用 WorldHandles 资源来注册一个函数。注册的函数对所有使用 cuicui_chirp 加载的 chirp 文件都是全局的。

其他区别还包括增加了导入语句(use)、模板定义(fn)和模板调用(template!())。

导入语句

目前尚未实现,请继续下一节。

草案设计

注意 导入尚未实现

cuicui_chirp 中,你不仅限于单个文件。你可以 导入 其他 chirp 文件。

要这样做,请使用导入语句。导入语句 是文件中的第一个语句;它们以 use 关键字开始,后跟要导入文件的源路径和一个可选的 "as imported_name",这是在此文件中引用导入的名称。

use different/file
// ...

你有两种使用导入的方式

  1. 作为 整个文件导入。你可以导入任何文件,并直接使用它,就像使用不带参数的模板一样。如果你想在不同的文件中编写多个复杂的菜单,这很有用。
  2. 作为 模板集合。你可以导入文件中定义的个别模板。只需将路径与 .template_name 完成即可。

类似于 rust,你可以组合导入,但只能组合来自同一文件的模板,因此以下有效

use different/file.template
use different/file.{template1, template2}
// ...

不支持通配符导入。

公开性

然而,为了能够导入模板,你需要将它们标记为 pub 在源模板中。只需在 fn 前面加上 pub 即可。

模板定义

chirp 文件在文件开头允许一系列 fn 定义。一个 fn 定义看起来非常类似于 rust 函数定义。它有一个名称和零个或多个参数。它们的主体是单个语句

// file: <scene.chirp>
// template name
//
// vvvvvv
fn spacer() {
	Spacer(height(10px) width(10%) bg(coral))
}
//             parameter
// template name  ↓
//    ↓           ↓
// vvvvvv vvvvvvvvvvv
fn button(button_text) {
    Entity(named(button_text) width(95%) height(200px) bg(purple) row) {
        ButtonText(text(button_text) rules(0.5*, 0.5*))
    }
}

你可以像调用 rust 宏一样调用模板,通过编写模板名称后跟 ! 和括号来调用

# fn sys(cmds: &mut cuicui_dsl::EntityCommands) { cuicui_dsl::dsl!(cmds,
// file: <scene.chirp> (following)
Menu(screen_root row bg(darkgrey)) {
    TestSpacer(width(30%) height(100px) bg(pink))
    spacer!()
    button!("Hello world")
}
# )}

当模板被调用时,它将被替换为定义该模板的 fn 定义主体中的单个根语句。

模板附加功能

模板调用可以跟随着 模板附加功能

# fn sys(cmds: &mut cuicui_dsl::EntityCommands) { cuicui_dsl::dsl!(cmds,
// file: <scene.chirp> (following)
Menu(screen_root row bg(darkgrey)) {
    TestSpacer(width(30%) height(100px) bg(pink))

    // Additional method list after the template arguments list
    //       vvvvvvvvvvvvvvvvvvvvvv
    spacer!()(width(50%) bg(coral))

    // Both additional methods and additional children added after the argument list
    //                    vvvvvvvvvv
    button!("Hello world")(column) {
        MoreChildren(text("Hello"))
        MoreChildren(text("World"))
    }
}
# )}

附加方法将被添加到模板根语句方法列表的末尾。而附加子语句将被添加为模板根语句的子语句。

以这个 chirp 文件为例

fn deep_trailing2(line, color) {
    Trailing2Parent {
        Trailing2 (text(line) bg(color) width(1*))
    }
}
fn deep_trailing1(line2, line1) {
    deep_trailing2!(line1, red) {
        Trailing1 (text(line2) bg(green) width(2*))
    }
}
deep_trailing1!("Second line", "First line") (column bg(beige) rules(1.1*, 2*) margin(20)) {
    Trailing0 (text("Third line") bg(blue))
}

它等同于

# fn sys(cmds: &mut cuicui_dsl::EntityCommands) { cuicui_dsl::dsl!(cmds,
Trailing2Parent(column bg(beige) rules(1.1*, 2*) margin(20)) {
    Trailing2 (text("First line") bg(red) width(1*))
    Trailing1 (text("Second line") bg(green) width(2*))
    Trailing0 (text("Third line") bg(blue))
}
# )}

参数替换

注意 “参数”在这里可能指两件事:(1)传递给模板的值,在 template!(foo_bar) 中,foo_bar 是一个参数。(2)传递给 方法 的参数,在 Entity(text(method_argument)) 中,method_argument 是一个方法参数。

fn 名称中括号内声明的名称是一个 参数。在 fn button(button_text) 中,button_text 是一个模板参数。

当调用模板时,fn 的主体将插入到调用处,传递给模板的参数将内联在模板主体语句中。

请特别注意参数的内联方式

  • 参数仅在 方法参数 中内联
  • 参数不在引号内 内联
  • 参数仅在 它们是整个参数 时内联
❗ 兼容性通知 ❗
将来,参数将在更多上下文中允许使用
  • 在方法列表中(例如 Entity(parameter)
  • 作为模板名称(例如 parameter!()
  • 嵌入到更复杂的方法参数中(例如 Entity(mehod({ width: parameter }))
为了避免痛苦的破坏性更改,避免将参数命名为 DSL 方法或模板的名称。
# fn sys(cmds: &mut EntityCommands) { dsl!(cmds,
fn button(button_text) {
    // Will spawn an entity without name, with tooltip set to whatever
    // was passed to `button!`.
    Entity(tooltip(button_text) width(95%) height(200px) bg(purple) row) {
        // Will spawn an entity named "button_text" with text "button_text"
        button_text(text("button_text") rules(0.5*, 0.5*))

        // Current limitation:
        // `gizmo` method will be called with `GizmoBuilder(button_text)` as first
        // argument and whatever was passed to `button!` as second argument
        Gizmo(gizmo(GizmoBuilder(button_text), button_text) rules(0.5*, 0.5*))
    }
}
# )}

技巧和窍门

请参阅 专门的文档页面 了解 parse_dsl_impl 上所有可用的配置选项。

性能

请考虑显式依赖于 logtracing 包,并启用这些包的 "release_max_level_debug" 功能,以便从发布构建中省略日志消息。

cuicui_chirp 包含在非常热的循环中大量跟踪日志。 "release_max_level_debug" 将在编译时移除跟踪日志,这不仅会使代码更快(否则将读取锁原子的代码),还使 更多优化、内联和循环展开成为可能。

首先,使用以下命令找到依赖树中 logtracing 包的版本

cargo tree -p log -p tracing

然后,将它们添加到您的 Cargo.toml 并启用一个 max_level 功能。请注意,它们已经在您的依赖树中,所以这样做没有缺点

log = { version = "<version found with `cargo tree`>", features = ["release_max_level_debug"] }
tracing = { version = "<version found with `cargo tree`>", features = ["release_max_level_debug"] }
# Note: I recommend `release_max_level_warn` instead.
# `debug` is specific for performance regarding `cuicui_chirp`

下次您编译您的游戏时,您可能需要重新编译整个依赖树,因为 tracinglog 通常相当深入。

继承

还记得来自 cuicui_dsl 的继承技巧吗?parse_dsl_impl 与之兼容。使用 delegate 参数指定要委托 MyDsl 实现中找不到的方法的字段。

// pub struct MyDsl<D = ()> {
//     #[deref]
//     inner: D,
// }
#[parse_dsl_impl(delegate = inner)]
impl<D: DslBundle> MyDsl<D> {
    // ...
}

请参阅 parse_dsl_impl::delegate

ReflectDsl

cuicui_dsl 不同,可以使用 Reflect 来定义 DSL。有关详细信息,请参阅 ReflectDsl 文档。

自定义解析器

由于 .chirp 文件是文本格式,我们需要将文本转换为方法参数。根据类型的不同,parse_dsl_impl 解析方法参数的方式也不同。

请参阅 parse_dsl_impl::type_parsers 以获取详细信息。

cuicui_dslcuicui_chirp 之间的关系是什么?

cuicui_dsl 是一个宏(dsl!),而 cuicui_chirp 是一个场景文件格式、解析器和 bevy 加载器。在 cuicui_dsl 的基础上构建 cuicui_chirp,并且具有与 cuicui_dsl 不同的功能。以下是功能矩阵

功能 cuicui_dsl cuicui_chirp
语句与方法
带有内联 rust 代码的 code
调用已注册函数的 code
fn 模板 rust
从其他文件导入 rust
热重载
基于反射的方法
颜色、规则的特殊语法
轻量级
允许使用非 Reflect 组件

您可以结合使用 cuicui_dslcuicui_chirp,这两个 crate 都填补了不同的空白。

依赖项

~23–62MB
~1M SLoC