#bevy #bevy-ecs #ecs #type-safety

moonshine-check

Bevy的验证和恢复解决方案

1个不稳定发布

0.1.0 2024年7月24日

#1565 in 游戏开发

Download history 135/week @ 2024-07-22 7/week @ 2024-07-29

每月142次下载

MIT许可证

25KB
213

✅ Moonshine Check

crates.io downloads docs.rs license stars

Bevy的验证和恢复解决方案。

概述

Bevy应用程序中常见的错误来源是对世界状态的无效假设。通常,这会导致由于查询不匹配而“错过”目标实体的查询。

use bevy::prelude::*;

#[derive(Component)]
struct Person;

let mut app = App::new();
app.add_systems(Update, (bad_system, unsafe_system));

fn bad_system(mut commands: Commands) {
    commands.spawn().insert(Person); // Bug: Name is missing.
}

// This system will silently skip over any `Person` entities without `Name`:
fn unsafe_system(people: Query<(&Person, &Name)>) {
    for (person, name) in people.iter() {
        println!("{:?}", name);
    }
}

虽然这个例子很简单,但对于必须依赖某些不变性才能正确协同工作的相互依赖的系统,这个问题会变得更加严重。

针对这个问题有各种解决方案。其中一些解决方案包括

  1. 将组件隐藏在包中以确保它们始终一起插入。
  2. 在系统查询中使用Expect<T>
  3. 使用Kind语义来强制系统边界之间的要求。

虽然这些解决方案都是有效的,但每个解决方案都有缺陷

  1. 包不能重叠,并且无法保证包组件永远不会被删除。
  2. 过度使用Expect<T>可能导致大量崩溃,没有人喜欢。
  3. Kind语义仅在查询时强制执行,如果需要则需要进行手动验证。

这个crate提供了一种“最后手段”的解决方案,以确保应用程序中的不变性。它提供了一种标准方式来检查实体的正确性,并允许您优雅地处理失败。

use bevy::prelude::*;
use moonshine_check::prelude::*;

#[derive(Component)]
struct Person;

let mut app = App::new();
// Check for `Person` entities without `Name` and purge them (despawn recursively):
app.check::<Person, Without<Name>>(purge());
app.add_systems(Update, (bad_system, safe_system));

fn bad_system(mut commands: Commands) {
    // Because of the check, this entity will be purged before the next frame:
    commands.spawn().insert(Person); // Bug: Name is missing.

}

// This system will never skip a `Person` ever again!
fn safe_system(people: Query<(&Person, &Name)>) {
    for (person, name) in people.iter() {
        println!("{:?}", name);
    }
}

用法

检查

check方法用于向应用程序添加新的检查

use bevy::prelude::*;
use moonshine_check::prelude::*;

#[derive(Component)]
struct A;

#[derive(Component)]
struct B;

let mut app = App::new();
// ...
app.check::<A, Without<B>>(purge());

此函数接受一个Kind、一个QueryFilter和一个Policy

内部,它添加了一个系统,将给定的策略应用于给定类型的新实体,这些实体与给定的过滤器匹配。

不符合查询的实体被认为是“有效”的。

一旦检查了实体,它将不会被再次检查,除非手动请求(请参阅check_again)。

策略

有4种可能的方法可以从无效实体中恢复

1. invalid()

此策略将实体标记为无效并生成错误消息。结合Valid,您可以定义容错系统

use bevy::prelude::*;
use moonshine_check::prelude::*;

#[derive(Component)]
struct A;

#[derive(Component)]
struct B;

let mut app = App::new();
// ...
app.check::<A, Without<B>>(invalid());
app.world_mut().spawn(A); // Bug!

// Pass `Valid` to your system query:
fn safe_system(query: Query<(Entity, &A), Valid>, b: Query<&B>) {
    for (entity, a) in query.iter() {
        // Safe:
        let b = b.get(entity).unwrap();
    }
}

2. purge()

此策略销毁实体及其所有子实体并生成错误消息。这阻止了任何系统对其进行查询。

use bevy::prelude::*;
use moonshine_check::prelude::*;

#[derive(Component)]
struct A;

#[derive(Component)]
struct B;

let mut app = App::new();
// ...
app.check::<A, Without<B>>(purge());
app.world_mut().spawn(A); // Bug!

// No need for `Valid`:
fn safe_system(query: Query<(Entity, &A)>, b: Query<&B>) {
    for (entity, a) in query.iter() {
        // Safe:
        let b = b.get(entity).unwrap();
    }
}

4. panic()

此策略生成错误消息并立即停止程序执行。

这主要用于调试,在生产环境中应避免使用。它相当于因为你的咖啡太烫而把整个世界都烧毁!☕🔥

use bevy::prelude::*;
use moonshine_check::prelude::*;

#[derive(Component)]
struct A;

#[derive(Component)]
struct B;

let mut app = App::new();
// ...
app.check::<A, Without<B>>(panic());
app.world_mut().spawn(A); // Bug!

// No need for `Valid`:
fn safe_system(query: Query<(Entity, &A)>, b: Query<&B>) {
    // Doesn't matter, we'll never get here...
    unreachable!();
}

5. repair(f)

此策略生成警告消息并尝试使用给定的Fixer修复实体。

当您可以自动将无效实体恢复到有效状态时,这很有用。例如,它只是缺少一个随机的标记组件。没有必要对此大惊小怪。

此策略对于向后兼容性也很有用,因为它可以用来自动将保存的实体升级到新版本。

✨此crate专门设计用于与moonshine-save一起使用。所有检查系统都在LoadSystem::Load之后插入,以确保加载数据始终有效。👍

use bevy::prelude::*;
use moonshine_check::prelude::*;

#[derive(Component)]
struct A;

#[derive(Component)]
struct B;

let mut app = App::new();
// ...
app.check::<A, Without<B>>(repair(|entity, commands| {
    // It's fine, we can fix it! :D
    commands.entity(entity).insert(B);
}));

app.world_mut().spawn(A); // Bug!

// No need for `Valid`:
fn safe_system(query: Query<(Entity, &A)>, b: Query<&B>) {
    for (entity, a) in query.iter() {
        // Safe:
        let b = b.get(entity).unwrap();
    }
}

指南和限制

检查并非免费。每次检查都会向您的应用程序添加一个新的系统,这可能会影响您的性能。

避免过度使用检查。相反,尝试检查对应用程序正确性至关重要的广泛假设。例如,您可以使用检查来确保所有Kind类型转换都是有效的

use bevy::prelude::*;
use moonshine_check::prelude::*;
use moonshine_kind::prelude::*;

#[derive(Component)]
struct Fruit;

#[derive(Component)]
struct Apple;

#[derive(Bundle)]
struct AppleBundle {
    fruit: Fruit,
    apple: Apple,
}

kind!(Apple is Fruit); // Enforced by the bundle

let mut app = App::new();
// ...
app.check::<Apple, Without<Fruit>>(purge()); // Encorced by checking

请记住,一旦实体经过检查,它就不会再次检查,除非明确要求。这意味着如果实体被检查后任何不变量发生更改,它将不会被检测到。

您可以通过调用check_again强制实体再次进行检查

commands.entity(entity).check_again();

您还可以在Fixer内部使用check_again,以确保修复后实体确实被修复

app.check::<A, Without<B>>(repair(|entity, commands| {
    // ...
    // Did we actually fix it? Not sure? Check again!
    commands.entity(entity).check_again();
}));

此外,建议将您的检查分为两大类

调试检查

这些检查应保留用于内部验证、调试和测试。

您应考虑使用Rust功能标志禁用这些检查,以在性能至关重要的目标上禁用这些检查

#[cfg(feature = "debug_checks")]
app.check::<A, (Without<B>, Without<B2>)>(panic());

运行时检查

这些检查应保留用于验证外部输入,例如反序列化、网络或用户生成数据。

app.check::<A, With<B>>(repair(|entity, commands| {
    // Update B -> B2
    commands.entity(entity).remove::<B>();
    commands.entity(entity).insert(B2::default());
}));

依赖项

~16–51MB
~866K SLoC