-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Archetype Invariants #1481
Comments
A new thing that I want: For context, I want this for overriding behavior in a checked and elegant way. |
As an interesting twist: a user asked in #1635 to enforce that a resource must always exist. In theory, because #1525 stores resources as components on a singleton entity, we could provide a wrapper to ensure that, by tying it to a While I think that archetype invariants are the right tool for ultimately solving the other requests in #1635, I'm much less convinced on this use case. It feels like it's exploiting a quirk of the implementation details, and that systems panicking when they request a resource that doesn't exist gets us the desired behavior already. |
From @BoxyUwU, archetype invariants should only be checked once all commands are flushed, in order to allow for comfortable gradual creation / modification of new entities. |
Once all archetype invariants are known, we need to check that no contradictory (impossible to satisfy) rules exist. |
we actually need to check invariants basically after any time we hand out an &mut World as that would be enough to violate invariants and cause unsoundness if we rely on invariants for query disjointedness. so this basically means that
|
These "invariants" could be viewed as a bottom-up "proto-schema". It might be worth comparing to how traditional database schemas are put together:
Two things that jump out at me here are (1) the entire schema is explicitly defined in one place, not scattered around the code base, and (2) the definition includes a basic hierarchy that can be used to organize thinking about the system. Purely as a thought experiment, here is an attempt at describing this in Bevy ECS terms. Parts of this are probably contrary to Bevy's design goals, but maybe some ideas can be pulled from it.
A rough realization of this in Rust: trait Table { ... }
trait Column<Table: Table> { ... }
/// See below.
trait SubTable {
type Parent;
} The To ensure a single definition of a app.define_table::<MyTable>().unwrap()
.add_column::<MyColumn>()
// Sub tables allow modules to have private Column types
// that are inserted into the schema in a well defined way and
// don't collide with the Columns of the parent table.
//
// A sub table is a collection of columns and constraints that
// are logically separate from the main table. Specifically, the
// sub table can only add constraints that involve its own
// columns. Sub tables can NOT have multiple "rows" per
// Entity -- they just add "normal" columns to the Entity that
// are kept separate from the parent schema.
//
// The sub table *as a whole* is either present or not-present
// for a given Entity. Invoking `insert::<MySubTable>` on an
// `EntityCommands` causes the sub table to be inserted.
// Must-exist constraints are triggered at this time. If the
// sub-table is required for a given `Entity`, the parent table
// should assert a must-exist constraint with the sub-table's
// type (if known).
//
// `add_sub_schema_bundle` on `TableBuilder<MyTable>` invokes
// the callback with a `SchemaBuilder`. Any tables defined by
// the callback will become sub-tables of `MyTable`.
.add_sub_schema_bundle(|builder| foreign_package::define_table(builder))
.add_constraint(Constraint::always_present::<(MyColumn, MyOtherColumn)>())
.insert_table();
trait SchemaBuilder {
fn define_table<Table>(&mut self) -> Result<impl TableBuilder<Table>, anyhow::Error>;
fn add_column<C: Column>(self) -> Self;
fn add_sub_schema(self, cb: FnOnce<impl TableDefiner>) -> Self;
fn add_constraint(self, c: Constraint<Table>) -> Self;
fn insert_table(self) -> Result<(), anyhow::Error>;
} Constraints are defined like this: impl Constraint<Table> {
// This type is always present for any Entity in the table.
//
// ColumnSet is a trait that's implemented for anything that is
// Column or a tuple of ColumnSets.
fn always_present<CS: ColumnSet<Table>>();
// More constraints can go here. Constraints always involve columns of `Table`.
// Cross `Table` constraints are not supported.
} A table can only be defined once: invoking I'm not sure that's implementable, but I've spent too much time on it already. |
@concave-sphere this is fascinating. I agree with your intuition that they feel like bottom-up schema. I'll come back to this in depth as I work on an RFC for these. |
From @SanderMertens on Discord. |
When you start to model something with marker components, you inevitably fall into this issue. Markers components like Dead and Alive can be represented as an enum and it's often the first intuitive way of doing it. Then, you are probably going to use markers components, you flatten the game state. It make easy for Bevy to filter components at runtime and parallelize systems with fine-grained query. But there's a cost, by using marker components instead of enum, you make illegal state representable in your own logic. Not a trivial issue to solve. You really need theses markers components because it's a major point of an ECS and the concept is easy to understand, but at the end you need to be carefull about what you do with your game state and your code. You can introduce illegal state like a Player that is Swimming and Dead when you expected a Player that is Idle and Dead. |
I started working on a basic prototype for this (with help from @alice-i-cecile), but came to an impasse trying to initialize components by Some things we worked out:
Open questions
|
I have been toying with a very simple solution to this issue. It's not perfect, mainly because I tried to implement it without touching Bevy internals. In summary, my idea involves a bundle called #[derive(Bundle)]
struct Require<T: Bundle> {
bundle: RequiredBundle,
#[bundle(ignore)]
marker: PhantomData<T>,
} Usage: #[derive(Bundle)]
struct B {
require: Require<(X, Y)>,
/* ... */
} This bundle stores its given type. A system then checks all instances of this bundle at the end of every app cycle, and react as needed. In my implementation, I'm just calling This implementation could also be extended to support different kinds of requirements, such as Full gist along with a test case is included here: The benefit of this approach is mainly it's simplicity. There is no need to register anything with the app, and the bundle requirements are very explicitly stated on the bundle itself, which makes it very readable to me. The biggest issue with this implementation is that the inner bundle must be spawned at least once so that it's registered with the app. I don't think I can solve this issue without touching Bevy internals, because The other issue is that bundles of bundles with the same requirements would cause a duplicate bundle error. This could be easily solved by ignoring duplicated I'm curious what the Bevy community thinks of this approach, and if anyone has any ideas for the first issue I mentioned above. |
As an alternative, this is a pattern I've used for "exclusive marker types": mod cam_marker {
#![allow(unused)]
use bevy::prelude::{Component, With, Without};
#[derive(Component)]
pub struct Main;
#[derive(Component)]
pub struct LeftEye;
#[derive(Component)]
pub struct RightEye;
pub type OnlyMain = (With<Main>, Without<LeftEye>, Without<RightEye>);
pub type OnlyLeftEye = (Without<Main>, With<LeftEye>, Without<RightEye>);
pub type OnlyRightEye = (Without<Main>, Without<LeftEye>, With<RightEye>);
}
use cam_marker::*; The above would probably easily generalise to a macro. Add the main structs as components to entities: // spawn the left camera
commands
.spawn(Camera3dBundle {
transform: Transform::from_xyz(-0.1, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
camera: Camera {
priority: -1,
..default()
},
camera_3d: Camera3d {
clear_color: ClearColorConfig::Default,
..default()
},
..default()
})
.insert(LeftEye);
// spawn the right camera
commands
.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.1, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
camera: Camera {
priority: 1,
..default()
},
camera_3d: Camera3d {
clear_color: ClearColorConfig::None,
..default()
},
..default()
})
.insert(RightEye); The // these systems can run in parallel
fn spin_left_camera(
mut query: Query<&mut Transform, OnlyLeftEye>,
// ...
) {
// ...
}
fn spin_right_camera(
mut query: Query<&mut Transform, OnlyRightEye>,
// ...
) {
// ...
} This doesn't enforce anything as such at runtime, but it removes the ambiguities. If any entity has more than one member in that exclusive set the queries simply won't match anything. It might be nicer with rust-lang/rfcs#2593 As I write this comment, I discover: |
Another attempt at this: |
As an extension of disjoint components it would be nice if there were a way to define a "disjoint wrapper" or "state" type.
Such that you can define that any Such a type would behave as one component type for the purposes of insertion, but act as distinct types for queries. I know that a something similar can be achieved with Enum, but I have two issues with using enums:
|
Required components (#7272) are on the path to being added to the engine, and will fill some of the niche of these. They don't account for mutually exclusive components, aren't enforced at runtime and can't be used for borrow checker purposes though. |
here's how i'm enforcing mutually exclusive components in the meantime: bevy_mutually_exclusive_components |
Introduced in #1312. The complete design will be found at RFC #5.
The Basics
An archetype is the set of components that are found together on a single entity. The set of archetypes present in our
World
is the union of the archetypes of every entity in the world.Archetype invariants are rules that limit which components can coexist, limiting the possible archetypes that can co-occur. Because of the flexible power of
commands.spawn
andcommands.insert
, the components that an entity could have is unknowable at compile time, preventing us from .We can use this information to allow for more granular component access in ways that would otherwise result in inconsistent runtime behavior, or verify that certain logical rules are followed during execution.
Due to the overhead of verifying these rules against the
World
's archetypes, this is likely best done only during debug mode, or perhaps by post-hoc inspection of logs.Use Cases
API for specifying archetype invariants
Primitives:
forbidden(my_bundle)
: an entities archetype cannot have have the specific combination of components listed in the bundle as a subsetDerived:
inseparable(my_bundle)
: entities that have either component A or B never occur without each other. Equivalent to combiningA.always_with(B)
with the reverse for every pairwise combinationdisjoint(my_bundle)
: entities can only have at most one component in the bundle. Equivalent to creating aforbidden
archetype invariant for each pairwise combinationThe text was updated successfully, but these errors were encountered: