17次发布

0.3.9 2024年7月21日
0.3.7 2024年5月4日
0.3.6 2024年3月12日
0.3.4 2023年11月6日
0.1.2 2023年3月19日

#50 in 游戏开发

Download history 219/week @ 2024-05-03 19/week @ 2024-05-10 46/week @ 2024-05-17 32/week @ 2024-05-24 15/week @ 2024-05-31 22/week @ 2024-06-07 25/week @ 2024-06-14 18/week @ 2024-06-21 50/week @ 2024-06-28 157/week @ 2024-07-05 39/week @ 2024-07-12 163/week @ 2024-07-19 48/week @ 2024-07-26 31/week @ 2024-08-02 24/week @ 2024-08-09 19/week @ 2024-08-16

每月 131 次下载
7 个Crate中使用 (5直接使用)

MIT 许可证

57KB
797

💾 Moonshine Save

crates.io downloads docs.rs license stars

Bevy游戏引擎的保存/加载框架。

概述

在Bevy中,可以使用World和一个DynamicScene进行序列化和反序列化(详见示例)。虽然这对于场景管理和编辑很有用,但在保存/加载游戏状态时可能会出现问题。

主要问题是,在大多数常见应用中,保存的游戏数据是整个场景的一个非常小的子集。变换、场景层次结构、相机或UI组件等视觉和美学元素通常在游戏开始或实体初始化期间添加到场景中。

此Crate旨在通过提供一个框架和一系列系统来解决此问题,以选择性地保存和加载世界。

use bevy::prelude::*;
use moonshine_save::prelude::*;

App::new()
    .add_plugins(DefaultPlugins)
    .add_plugins((SavePlugin, LoadPlugin))
    .add_systems(PreUpdate, save_default().into_file("world.ron").run_if(should_save))
    .add_systems(PreUpdate, load_from_file("world.ron").run_if(should_load));

fn should_save() -> bool {
    todo!()
}

fn should_load() -> bool {
    todo!()
}

特性

  • 美学(视图/模型)与保存状态(模型)之间的明确分离
  • 定义保存状态的样板最少
  • 后处理保存和加载状态的钩子
  • 自定义保存/加载管道
  • 无宏

理念

此Crate的主要设计目标是使用从MVC(模型-视图-控制器)架构中借用的概念来分离游戏的美学元素(游戏的“视图”)与其逻辑和保存状态(游戏的“模型”)。

为了按预期使用此Crate,您应该设计游戏逻辑时考虑到这种分离

  • 使用可序列化的组件来表示游戏的保存状态,并将它们存储在保存实体上。
    • 有关如何使组件可序列化的详细信息,请参阅Reflect
  • 如有必要,定义一个系统来为每个生成的保存实体生成一个视图实体。
    • 您可能希望使用Added来初始化视图实体。
  • 创建保存实体与其视图实体之间的链接。
    • 可以使用不可序列化的组件/资源来完成这项工作。

✨ 请参阅Moonshine View,了解此模式的自动通用实现。

例如,假设我们想在游戏中表示一个玩家角色。我们使用各种组件来存储玩家的逻辑状态,例如 Health(生命值)、Inventory(物品栏)或 Weapon(武器)。

每个玩家都使用一个2D SpriteBundle 来表示,该bundle展示玩家的当前视觉状态。

传统上,我们可能会使用单个实体(或层次结构)来表示玩家。这个实体将携带所有逻辑组件,如 Health,以及 SpriteBundle

use bevy::prelude::*;

#[derive(Bundle)]
struct PlayerBundle {
    health: Health,
    inventory: Inventory,
    weapon: Weapon,
    sprite: SpriteBundle,
}

#[derive(Component)]
struct Health;

#[derive(Component)]
struct Inventory;

#[derive(Component)]
struct Weapon;

一个可能更好的方法是将这些数据存储在完全独立的实体中

use bevy::prelude::*;
use moonshine_save::prelude::*;

#[derive(Bundle)]
struct PlayerBundle {
    player: Player,
    health: Health,
    inventory: Inventory,
    weapon: Weapon,
}

#[derive(Component)]
struct Player;

#[derive(Component)]
struct Health;

#[derive(Component)]
struct Inventory;

#[derive(Component)]
struct Weapon;

#[derive(Bundle)]
struct PlayerViewBundle {
    view: PlayerView,
    sprite: SpriteBundle,
}

#[derive(Component)]
struct PlayerView {
    player: Entity
}

fn spawn_player_sprite(mut commands: Commands, query: Query<Entity, Added<Player>>) {
    for player in query.iter() {
        commands.spawn(PlayerViewBundle {
            view: PlayerView { player },
            sprite: todo!(),
        });
    }
}

这种做法一开始可能看起来有些冗长,但它有几个优点

  • 可以在没有视图的情况下测试保存数据
  • 保存数据成为整个游戏状态的唯一真理来源
  • 保存数据可以使用不同的系统来表示,以进行专门的调试或分析

最终,是否将这种分离的额外复杂性视为对您的项目有益,取决于您自己。

该crate默认并非旨在成为通用的保存解决方案。然而,该crate的另一个设计目标是最大程度的可定制性。

该crate提供了一些标准和常用保存/加载管道,这些管道应该足以满足基于上述架构的大多数应用程序。这些管道由更小的子系统组成。

这些子系统可用于任何其他所需的配置,并与其他系统结合,以创建高度专业化的管道。

用法

保存管道

为了保存游戏状态,首先使用Save标记来标记必须保存的实体。这是一个可以像任何其他组件一样添加到bundle或插入到实体中的组件

use bevy::prelude::*;
use moonshine_save::prelude::*;

#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct Player;

#[derive(Component, Default, Reflect)]
#[reflect(Component)]
struct Level(u32);

#[derive(Bundle)]
struct PlayerBundle {
    player: Player,
    level: Level,
    name: Name,
    save: Save,
}

⚠️ 保存组件必须实现 Reflect 并是 注册类型

添加SavePlugin并注册您的序列化组件

app.add_plugins(SavePlugin)
    .register_type::<Player>()
    .register_type::<Level>();

要调用保存过程,您必须定义一个 SavePipeline。每个保存管道是一组管道系统。

您可以使用save_default来启动保存管道,该管道保存所有具有Save组件的实体。

app.add_systems(PreUpdate, save_default().into_file("saved.ron"));

另外,您也可以使用save_all来保存所有实体,并使用save提供您保存实体的自定义QueryFilter

还有save_all_withsave_with,它们与SaveFilter一起使用。

当单独使用时,管道会在每次应用更新周期中保存世界状态。这通常是不希望的,因为您通常希望在运行时特定时间进行保存过程。为此,您可以将保存管道与 .run_if 结合使用。

app.add_systems(PreUpdate, save_default().into_file("saved.ron").run_if(should_save));

fn should_save( /* ... */ ) -> bool {
    todo!()
}

保存资源

默认情况下,资源不包括在保存数据中。

要将资源包含到保存管道中,请使用 .include_resource<R>

app.add_systems(PreUpdate, save_default().include_resource::<R>().into_file("saved.ron"));

移除组件

默认情况下,所有可序列化的组件都包括在保存数据中。

要排除组件从保存管道中,请使用 .exclude_component<T>

app.add_systems(PreUpdate, save_default().exclude_component::<T>().into_file("saved.ron"));

加载管道

在加载之前,使用 Unload 标记您的视觉和美学实体(“视图”实体)。

类似于 Save,这是一个可以添加到包或像常规组件一样插入实体的标记。

在加载开始之前,任何带有 Unload 标记的实体都会递归地被销毁。

use bevy::prelude::*;
use moonshine_save::prelude::*;

#[derive(Bundle)]
struct PlayerSpriteBundle {
    /* ... */
    unload: Unload,
}

您应该设计游戏逻辑,使保存数据与游戏视觉效果分离。

任何引用实体的保存组件必须实现 MapEntities

use bevy::prelude::*;
use bevy::ecs::entity::{EntityMapper, MapEntities};
use moonshine_save::prelude::*;

#[derive(Component, Default, Reflect)]
#[reflect(Component, MapEntities)]
struct PlayerWeapon(Option<Entity>);

impl MapEntities for PlayerWeapon {
        fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
        if let Some(weapon) = self.0.as_mut() {
            *weapon = entity_mapper.map_entity(*weapon);
        }
    }
}

确保已添加 LoadPlugin 并注册了您的类型

app.add_plugins(LoadPlugin)
    .register_type::<Player>()
    .register_type::<Level>();

要调用加载过程,您必须添加加载管道。默认的加载管道是 load_from_file

app.add_systems(PreUpdate, load_from_file("saved.ron"));

类似于保存管道,您通常希望使用 load_from_file.run_if 结合使用。

app.add_systems(PreUpdate, load_from_file("saved.ron").run_if(should_load));

fn should_load( /* ... */ ) -> bool {
    todo!()
}

示例

请参阅 examples/army.rs,以获取一个展示如何详细保存/加载游戏状态的简化应用程序示例。

动态保存文件路径

在提供的示例中,保存文件路径通常是静态的(即编译时已知)。然而,在某些应用程序中,可能需要在运行时选择路径进行保存。

您可以使用 SaveIntoFileRequestLoadFromFileRequest 特性来实现这一点。

您的保存/加载请求可以是 ResourceEvent

use std::path::{Path, PathBuf};
use bevy::prelude::*;
use moonshine_save::prelude::*;

// Save request with a dynamic path
#[derive(Resource)]
struct SaveRequest {
    pub path: PathBuf,
}

impl FilePath for SaveRequest {
    fn path(&self) -> &Path {
        self.path.as_ref()
    }
}

// Load request with a dynamic path
#[derive(Resource)]
struct LoadRequest {
    pub path: PathBuf,
}

impl FilePath for LoadRequest {
    fn path(&self) -> &Path {
        self.path.as_ref()
    }
}

您可以使用它与 .into_file_on_requestload_from_file_on_request 结合使用。

app.add_systems(PreUpdate, save_default().into_file_on_request::<SaveRequest>());
app.add_systems(PreUpdate, load_from_file_on_request::<LoadRequest>());

然后您可以通过将请求作为资源插入来调用保存/加载。

fn trigger_save(mut commands: Commands) {
    commands.insert_resource(SaveRequest { path: "saved.ron".into() });
}

fn trigger_load(mut commands: Commands) {
    commands.insert_resource(LoadRequest { path: "saved.ron".into() });
}

类似地,要使用事件进行保存/加载请求,您可以使用 .into_file_on_eventload_from_file_on_event 代替。

app.add_event(SaveRequest)
    .add_event(LoadRequest)
    .add_systems(PreUpdate, save_default().into_file_on_event::<SaveRequest>())    
    .add_systems(PreUpdate, load_from_file_on_event::<SaveRequest>());

然后您可以通过发送请求作为事件来调用保存/加载。

fn trigger_save(mut events: EventWriter<SaveRequest>) {
    events.send(SaveRequest { path: "saved.ron".into() });
}

fn trigger_load(mut events: EventWriter<SaveRequest>) {
    events.send(LoadRequest { path: "saved.ron".into() });
}

支持

提交问题报告任何错误、疑问或建议。

您还可以在官方 Bevy Discord 服务器上联系我,我的昵称是 @Zeenobit

依赖

16–51MB
~887K SLoC