1个不稳定发布
0.1.0 | 2024年7月24日 |
---|
#1565 in 游戏开发
每月142次下载
25KB
213 行
✅ Moonshine Check
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);
}
}
虽然这个例子很简单,但对于必须依赖某些不变性才能正确协同工作的相互依赖的系统,这个问题会变得更加严重。
针对这个问题有各种解决方案。其中一些解决方案包括
虽然这些解决方案都是有效的,但每个解决方案都有缺陷
- 包不能重叠,并且无法保证包组件永远不会被删除。
- 过度使用
Expect<T>
可能导致大量崩溃,没有人喜欢。 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