#persistent #resources #bevy #gamedev #session-management #game-state

bevy-persistent

一个帮助您轻松管理需要跨游戏会话持久化的资源的 Bevy 辅助工具

13 个不稳定版本 (5 个重大更改)

0.6.0 2024年7月5日
0.5.0 2024年2月20日
0.4.3 2024年1月11日
0.4.2 2023年12月3日
0.1.0 2023年3月22日

#42 in 游戏开发

Download history 31/week @ 2024-05-03 21/week @ 2024-05-10 49/week @ 2024-05-17 66/week @ 2024-05-24 41/week @ 2024-05-31 30/week @ 2024-06-07 33/week @ 2024-06-14 71/week @ 2024-06-21 18/week @ 2024-06-28 342/week @ 2024-07-05 172/week @ 2024-07-12 66/week @ 2024-07-19 60/week @ 2024-07-26 90/week @ 2024-08-02 86/week @ 2024-08-09 42/week @ 2024-08-16

每月282次下载
用于 bevy-persistent-windows

MIT/Apache

78KB
1K SLoC

bevy-persistent

一个 Bevy 辅助工具,用于轻松管理需要跨游戏会话持久化的资源。

背景

在游戏中,有很多资源需要在游戏会话之间持久化

  • 统计数据(例如,高分,死亡次数,游戏时间)
  • 设置(例如,按键绑定,游戏难度,音频设置)
  • 状态(例如,最后一个窗口的位置和大小,保存)
  • 等等...

这个包旨在简化此类资源的管理!

安装

支持所有存储格式

cargo add bevy-persistent --features all

或明确指定

cargo add bevy-persistent --features bincode,ini,json,toml,yaml

当然,您也可以仅选择您计划使用的存储格式

cargo add bevy-persistent --features bincode,toml

使用方法

序言

您只需要 Persistent<R>StorageFormat 类型即可使用此库,并且它们是从序言模块导出的。

use bevy_persistent::prelude::*;

定义

您需要定义您想要持久化的 Resource,并且它需要实现来自 serdeSerializeDeserialize 特性。

#[derive(Resource, Serialize, Deserialize)]
struct KeyBindings {
    jump: KeyCode,
    crouch: KeyCode,
}

创建

在您的设置系统中,您可以创建持久化资源并将其插入到您的游戏中。

fn setup(mut commands: Commands) {
    let config_dir = dirs::config_dir().unwrap().join("your-amazing-game");
    commands.insert_resource(
        Persistent::<KeyBindings>::builder()
            .name("key bindings")
            .format(StorageFormat::Toml)
            .path(config_dir.join("key-bindings.toml"))
            .default(KeyBindings { jump: KeyCode::Space, crouch: KeyCode::C })
            .build()
            .expect("failed to initialize key bindings")
    )
}

如果是第一次运行,资源将具有指定的默认值,并将该默认值保存到指定的路径(指定的格式)。否则,按键绑定将从指定的路径(使用指定的格式)加载。

访问

要访问资源,您可以使用类型为 Res<Persistent<R>> 的参数。

fn access_key_bindings(key_bindings: Res<Persistent<KeyBindings>>) {
    log::info!("you can crouch using {:?}", key_bindings.crouch);
}

Persistent<R> 实现了 Deref<Target = R>,因此您可以轻松访问资源的公共字段和方法。

修改

要修改资源,您可以使用类型为 ResMut<Persistent<R>> 的参数。

fn modify_key_bindings(mut key_bindings: ResMut<Persistent<KeyBindings>>) {
    key_bindings.update(|key_bindings| {
        key_bindings.crouch = KeyCode::ControlLeft;
    }).expect("failed to update key bindings");
}

Persistent<R>setupdate 方法来修改底层资源。这两个方法在返回之前都会将更新后的资源写入磁盘。

如果在任何环节发生错误(例如,没有权限读取/写入指定的路径),将返回错误,但底层对象将被更新,新值将在游戏的其余部分可见。然而,它不会持久化到下一次游戏会话!

手动持久化

有些资源更新频繁,每次小更新都进行持久化可能并不理想。或者持久化可能需要手动触发(例如,在游戏中的某些特定点自动保存)。

对于此类情况,您可以避免使用 setupdate 方法,并直接更新资源。

fn modify_key_bindings(mut key_bindings: ResMut<Persistent<KeyBindings>>) {
    key_bindings.crouch = KeyCode::ControlLeft;
}

当您希望资源与其当前值一起持久化时,可以使用 persist 方法。

fn persist_key_bindings(key_bindings: Res<Persistent<KeyBindings>>) {
    key_bindings.persist().expect("failed to save new key bindings");
}

回滚

对于某些持久化资源,将其回滚到默认值可能是有意义的。想象一下有一个键绑定设置页面,将其“还原为默认值”按钮放置在此页面上是一个好主意,因为如果玩家弄错了设置,将所有内容还原到默认状态会比手动调整每个键要容易得多。

此类持久化资源需要可回滚创建。

fn setup(mut commands: Commands) {
    let config_dir = dirs::config_dir().unwrap().join("your-amazing-game");
    commands.insert_resource(
        Persistent::<KeyBindings>::builder()
            .name("key bindings")
            .format(StorageFormat::Toml)
            .path(config_dir.join("key-bindings.toml"))
            .default(KeyBindings { jump: KeyCode::Space, crouch: KeyCode::C })
            .revertible(true)
            //^^^^^^^^^^^^^^^ using this
            .build()
            .expect("failed to initialize key bindings")
    )
}

可回滚的持久化资源可以使用 revert_to_default 方法。

fn revert_key_bindings_to_default(key_bindings: Res<Persistent<KeyBindings>>) {
    key_bindings.revert_to_default().expect("failed to revert key bindings to default");
}

还有一个选项可以在反序列化错误时自动回滚到默认值(例如,玩家造成的错误修改)。当启用时,如果持久化对象由于其内容而无法从持久化存储中加载,它将回滚到其默认值。

fn setup(mut commands: Commands) {
    let config_dir = dirs::config_dir().unwrap().join("your-amazing-game");
    commands.insert_resource(
        Persistent::<KeyBindings>::builder()
            .name("key bindings")
            .format(StorageFormat::Toml)
            .path(config_dir.join("key-bindings.toml"))
            .default(KeyBindings { jump: KeyCode::Space, crouch: KeyCode::C })
            .revertible(true)
            //^^^^^^^^^^^^^^^ requires this
            .revert_to_default_on_deserialization_errors(true)
            //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ enable with this
            .build()
            .expect("failed to initialize key bindings")
    )
}

如果 key-bindings.toml 无法反序列化为 KeyBindings 对象,它将回滚到持久化存储中指定的默认值。

卸载/重新加载

默认情况下,持久化资源被保留在内存中。这可能会导致不必要的内存使用过高。为了克服这一点,您可以使用 unload 方法。

fn unload_key_bindings(key_bindings: Res<Persistent<KeyBindings>>) {
    key_bindings.unload().expect("failed to persist key bindings before unloading");
}

一旦运行此系统,最新版本的底层资源将被写入底层存储,底层资源将从内存中移除。

如果您不想在卸载之前持久化资源的最新版本,或者您知道最新版本已经持久化,可以使用 unload_without_persisting 方法。

fn unload_key_bindings_without_persisting(key_bindings: Res<Persistent<KeyBindings>>) {
    key_bindings.unload_without_persisting();
}

如果不想在创建时立即加载到内存中,持久化资源可以以卸载状态创建。

fn setup(mut commands: Commands) {
    let config_dir = dirs::config_dir().unwrap().join("your-amazing-game");
    commands.insert_resource(
        Persistent::<KeyBindings>::builder()
            .name("key bindings")
            .format(StorageFormat::Toml)
            .path(config_dir.join("key-bindings.toml"))
            .default(KeyBindings { jump: KeyCode::Space, crouch: KeyCode::C })
            .loaded(false)
            //^^^^^^^^^^^^ using this or ".unloaded(true)"
            .build()
            .expect("failed to initialize key bindings")
    )
}

当持久资源卸载时,直到使用reload方法将其重新加载到内存中,底层资源将无法访问。

fn reload_key_bindings(key_bindings: Res<Persistent<KeyBindings>>) {
    key_bindings.reload().expect("failed to reload key bindings");
}

重新加载还可以用于已加载的持久资源,以同步外部更改。这在某些情况下非常有用,例如保存编辑器。保存编辑器会更新保存文件,调用重新加载会将这些更新带到游戏中,而无需重新启动游戏!

使用未加载的持久资源时请务必小心!

// all of these will panic
fn incorrect_usage_of_unloaded_key_bindings(key_bindings: Res<Persistent<KeyBindings>>) {
    // can't access the fields since it's not in memory
    println!("{}", key_bindings.crouch);
    key_bindings.crouch = KeyCode::ControlLeft;

    // can't get the underlying resource since it's not in memory
    key_bindings.get();
    key_bindings.get_mut();

    // can't update, persist, or unload the underlying resource since it's not in memory
    key_bindings.update(...);
    key_bindings.persist();
    key_bindings.unload(); // this will call persist before unloading so it'll panic as well
}

// none of these will panic
fn correct_usage_of_unloaded_key_bindings(key_bindings: Res<Persistent<KeyBindings>>) {
    // can get properties of the persistent resource
    key_bindings.name();
    key_bindings.format();
    key_bindings.storage();
    key_bindings.is_loaded();
    key_bindings.is_unloaded();

    // can try to get the underlying resource
    key_bindings.try_get();
    key_bindings.try_get_mut();

    // can set the resource to a new value
    key_bindings.set(...);

    // can unload without persisting as it's a no-op
    key_bindings.unload_without_persisting(...);

    // can reload the persistent resource
    key_bindings.reload(...);

    // can revert the persistent resource to its default
    key_bindings.revert_to_default(...);
    key_bindings.revert_to_default_in_memory(...);
}

美化

在开发过程中,将一些资源以美化格式存储是一个好主意,以便轻松观察/修改它们。

您可以使用pretty功能来启用美化文本格式

[features]
debug = ["bevy-persistent/pretty"]

在你的游戏中

fn setup(mut commands: Commands) {
    let config_dir = dirs::config_dir().unwrap().join("your-amazing-game");
    commands.insert_resource(
        Persistent::<KeyBindings>::builder()
            .name("key bindings")
            .format({
                #[cfg(feature = "debug")]
                {
                    StorageFormat::JsonPretty
                }
                #[cfg(not(feature = "debug"))]
                {
                    StorageFormat::Json
                }
            })
            .path(config_dir.join("key-bindings.toml"))
            .default(KeyBindings { jump: KeyCode::Space, crouch: KeyCode::C })
            .build()
            .expect("failed to initialize key bindings")
    )
}

然后你可以使用以下方式开发你的游戏

cargo run --features debug

要发布你的游戏,你可以使用以下方式编译

cargo build --release

WebAssembly

...是支持的!

在构建持久资源时,您需要指定一个路径。通常,此路径用于指定文件系统中的位置,但WebAssembly中没有文件系统。取而代之的是,它有本地存储会话存储

更改库的API或为WebAssembly创建单独的API会使库的使用变得复杂。相反,库使用了可以使用路径选择存储的事实。

  • /local/settings/key-bindings.toml将使用键settings/key-bindings.toml将持久资源存储在本地存储中
  • /session/settings/key-bindings.toml将使用键settings/key-bindings.toml将持久资源存储在会话存储中

起初可能看起来很复杂,但确实使支持原生和WebAssembly应用程序变得非常容易。

use std::path::Path;

fn setup(mut commands: Commands) {
    let config_dir = dirs::config_dir()
        .map(|native_config_dir| native_config_dir.join("your-amazing-game"))
        .unwrap_or(Path::new("local").join("configuration"));

    commands.insert_resource(
        Persistent::<KeyBindings>::builder()
            .name("key bindings")
            .format(StorageFormat::Json)
            .path(config_dir.join("key-bindings.toml"))
            .default(KeyBindings { jump: KeyCode::Space, crouch: KeyCode::C })
            .build()
            .expect("failed to initialize key bindings")
    )
}

使用上面的代码,您不需要进行任何条件编译来支持原生和WebAssembly应用程序。

  • 在原生应用程序中,它将确定平台的配置目录(例如,~/.config),并将您的游戏名称添加到其中(例如,~/.config/your-amazing-game),并将其用作文件系统的基目录。
  • 在WebAssembly应用程序中,它将使用以configuration为基键的本地存储,一旦您将其与key-binding.toml连接起来,资源将使用键configuration/key-bindings.toml进行存储。

如果指定路径的第一个元素不是"local""session",则库将引发恐慌!

如果您不喜欢这种方法,并希望对类型进行严格限制,则可以使用Persistent<R>new方法。

fn setup(mut commands: Commands) {
    use bevy_persistent::Storage;

    let name = "key bindings";
    let format = StorageFormat::Toml;
    let storage = Storage::LocalStorage { key: "key bindings".to_owned() };
    let loaded = true;
    let default = KeyBindings { jump: KeyCode::Space, crouch: KeyCode::C };
    let revertible = false;
    let revert_to_default_on_deserialization_errors = false;

    commands.insert_resource(
        Persistent::new(
            name,
            format,
            storage,
            loaded,
            default,
            revertible,
            revert_to_default_on_deserialization_errors,
        )
        .expect("failed to initialize key bindings"),
    );
}

示例

您可以在examples文件夹中直接运行并使用这些示例。

cargo run --release --features all --example name-of-the-example

要使用WebAssembly在浏览器中运行示例,您可以使用wasm-server-runner

cargo run --release --features all --target wasm32-unknown-unknown --example name-of-the-example

在顶部创建插件

如果您想创建使用持久资源的插件,您可以使用library功能来避免指定存储格式。

[package]
name = "bevy-amazing-library"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy-persistent = { version = "0.3" }

[dev-dependencies]
bevy-persistent = { version = "0.3", features = ["all"] }

[features]
default = []
library = ["bevy-persistent/library"]

您可以在启用 library 功能的情况下开发您的库(例如,使用以下命令:cargo build --features library),此时 bevy-persistent 不会在构建您的库时发出警告,但应用程序仍然会报错。

与以下内容相关

bevy_pkv

bevy_pkv 是为 Bevy 提供的通用键值存储库。这是一个非常优秀的库,可以作为 bevy-persistent 的替代品。以下是这两个库之间的一些区别!

  • bevy_pkv 更加灵活

假设您想要为两个不同的用户设置两种不同的 Settings

目前,使用 bevy-persistent 来实现这一点并不直接,因为 Persistent<R> 是一种资源,只能有一个资源的实例。因此,您不能在同一个应用程序中拥有两个 Persistent<Settings>。您可以通过定义一个自定义结构体(例如,Persistent<AllSettings>)、使用元组(例如,Persistent<(Settings, Settings)>)、使用向量(例如,Persistent<Vec<Settings>>)或使用哈希表(例如,Persistent<HashMap<Settings>>)来解决这个问题。

使用 bevy_pkv 来做这件事非常简单!

fn setup(mut pkv: ResMut<PkvStore>) {
    // ...
    let blue_settings: Settings = ...;
    let red_settings: Settings = ...;
    // ...
    pkv.set("blue-settings", &blue_settings).unwrap();
    pkv.set("red-settings", &red_settings).unwrap();
    // ...
}

fn utilize_settings(pkv: Res<PkvStore>) {
    // ...
    let blue_settings: Settings = pkv.get("blue-settings").unwrap();
    let red_settings: Settings = pkv.get("red-settings").unwrap();
    // ...
}

也许在社区提出请求的情况下,bevy-persistent 在某个时候可以提供解决这个问题的方法!我真的很喜欢提供 PersistentVec<R>PersistentMap<K, R> where K: impl AsRef<str> 的想法。

fn setup(mut settings: ResMut<PersistentMap<&'static str, Settings>>) {
    // ...
    let blue_settings: Settings = ...;
    let red_settings: Settings = ...;
    // ...
    settings.insert("blue", blue_settings)?;
    settings.insert("red", red_settings)?;
    // ...
}

fn utilize_settings(settings: Res<PersistentMap<&'static str, Settings>>) {
    // ...
    let blue_settings: &Settings = settings["blue"];
    let red_settings: &Settings = settings["red"];
    // ...
}
  • bevy_pkv 使用非常优化的键值存储引擎(在本地应用程序中)

据我所知,当前版本的 bevy_pkv 有两个存储选项,分别是 sledRocksDB,它们都非常优化。这意味着如果持久化对象经常更新,bevy_pkv 的性能将会非常出色!

另一方面,bevy-persistent 将每个持久化对象存储为单独的文件,这意味着持久化对象将触发直接的磁盘写入!这对于大多数用例(例如,设置或手动保存)来说是可行的,但如果您的应用程序需要非常频繁的更新,那么 bevy_pkv 就是正确的选择!

关于这一点,请务必在做出性能决策之前进行性能分析!

  • bevy_pkv 在每次读取时都会解析对象

写入操作进行了极致优化,但在bevy_pkv中读取可能较慢,因为读取持久化对象需要解析,每次读取都要进行解析!

bevy-persistent中,读取基本上是免费的,因为Persistent<R>只是实际对象的包装器。除非您使用unloadunload_without_persisting方法卸载它们以节省内存。当您使用reload方法重新加载它们时,解析只发生一次,读取再次免费。

再次强调,在出于性能考虑做出决策之前,请始终进行性能分析!

  • bevy_pkv不进行自动管理

使用bevy_pkv时,修改对象并使更改持久化始终是两个不同的步骤。

fn modify_key_bindings(mut pkv: ResMut<PkvStore>) {
    let mut key_bindings = pkv.get::<KeyBindings>("key-bindings");
    key_bindings.crouch = KeyCode::ControlLeft;
    pkv.set("key-bindings", &key_bindings)
}

另一方面,bevy-persistent提供了自动化此过程的API(请参阅修改)。

  • bevy_pkv使外部编辑变得非常困难(在本地应用程序中)

bevy-persistent将每个持久化对象存储为指定格式的单独文件。如果使用文本格式,则对象将变得非常容易编辑!在创建示例中,将在指定位置创建key-bindings.toml,内容如下

jump = "Space"
crouch = "C"

您可以轻松地将此文件更改为

jump = "Space"
crouch = "ControlLeft"

并且在下一次运行中它将完美工作!

另一方面,bevy_pkv将对象存储在单个数据库中,以二进制格式,这更高效,但不适于外部修改。

  • bevy_pkv使在不同位置存储对象变得有些不便(在本地应用程序中)

这一点类似于上面提到的,但更多地关乎如何构建以满足您的需求!例如,您可能希望通过Steam同步用户保存,但用户设置则不同,因为它们需要在不同的机器上有所不同。

这种场景可以使用bevy_pkv实现,但需要创建新类型,这些类型包含PkvStore的包装,并使用这些类型而不是单个PkvStore,这有些不便。然而,使用bevy-persistent则非常简单,因为您可以使用单个类型在文件系统中以任何您希望的方式结构化您的持久化对象!

  • bevy_pkv只支持JSON和二进制存储格式

bevy-persistent支持广泛的存储格式!如果您将游戏从其他引擎迁移到Bevy,您可能只需定义一些对象的结构(例如,设置、保存),然后让bevy-persistent处理其余部分!

然而,使用bevy_pkv时,您需要创建一个转换层,因为它与任何广泛使用的格式都不直接兼容。

  • bevy_pkv不是类型安全的

bevy-persistent 集成了 Bevy 的类型系统。持久性资源的类型在系统定义中指定,一切由 Bevy 处理。

另一方面,bevy_pkv 提供了一个单一的资源,您可以使用它来在运行时查询持久性键值数据库中的任何类型。这非常灵活,但您需要在每次访问时指定类型,因此容易出错。此外,不查看函数体就无法轻松地了解系统正在做什么。

  • bevy_pkv 可能会阻碍并行性

bevy-persistent 中的每个持久性资源都是单独的资源。这意味着系统可以同时被安排访问/修改不同的持久性资源。

这不可能适用于 bevy_pkv,因为它只有一个资源类型(pkv: Res<PkvStore>mut pkv: ResMut<PkvStore>),这对于所有系统来说都是通用的,这阻止了对不同持久对象的并发读取/写入。

许可证

bevy-persistent 是免费、开源且许可宽松的,就像 Bevy 一样!

此存储库中的所有代码都根据以下任一许可证双授权:

这意味着您可以选择您喜欢的许可证!

依赖项

~23MB
~428K SLoC