Repository navigation
Description
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 and commands.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
- Reducing false positives in system scheduling ambiguity detection (Ambiguous system ordering #1312)
- Assumption checking for safety
- More granular overlapping queries (to expand on Less permissive component conflict allowance in system queries #1320)
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 subset- 'A.always_with(B)': entities that have component A always also have component B
- `A.only_with(my_bundle)': component A never occurs with components other than those in the bundle
Derived:
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 aforbiddenarchetype invariant for each pairwise combination- bundle versions of all of the primitives, where A and B can be replaced by tuples of components (bundles) instead
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNeeds SME Triage
Activity
alice-i-cecile commentedon Feb 27, 2021
A new thing that I want:
element_of(bundle), which allows for "any one of the underlying components" and can be dropped in anywhere you could put a component. This enables enum-like component groups in a really nice way.For context, I want this for overriding behavior in a checked and elegant way.
alice-i-cecile commentedon Mar 13, 2021
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
Resourcemarker component or the like.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.
alice-i-cecile commentedon Mar 25, 2021
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.
alice-i-cecile commentedon Mar 25, 2021
Once all archetype invariants are known, we need to check that no contradictory (impossible to satisfy) rules exist.
BoxyUwU commentedon Mar 25, 2021
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
concave-sphere commentedon Apr 8, 2021
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.
Table.spawncommand now takes a type defining the table to spawn inside:commands.spawn::<MyTable>().insert(...).Table<MyTable>.Query, the scheduler, the ambiguity checker, etc understand that differentTablesare disjoint.Entity.Tableis defined.A rough realization of this in Rust:
The
Columntrait opts in a column to being usable with a particular table. The type signatures ofspawnandinsertrequire the inserted component to be a column of the table. TheBundletrait also is parameterized by a table, and all fields of a derivedBundlemust implementColumn<Table>. There's no way to stop a type from implementing bothTableandColumn, but doing so is going to be extremely confusing at best and should be avoided.To ensure a single definition of a
Table, it is added fromAppBuilderlike this:Constraints are defined like this:
A table can only be defined once: invoking
define_tabletwice for the same type will return anErr. The table can't be used until it is inserted (e.g,spawnwill fail with an error that the table isn't defined).I'm not sure that's implementable, but I've spent too much time on it already.
alice-i-cecile commentedon Apr 8, 2021
@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.
alice-i-cecile commentedon May 21, 2021
From @SanderMertens on Discord.
alice-i-cecile commentedon Jan 10, 2022
When combined with command error handling, we could use this to reliably swap out incompatible sets of components. Instead of panicking, we could handle the result of an insertion error by removing the offending components.
arialpew commentedon May 12, 2022
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.
sixfold-origami commentedon Jun 22, 2022
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
TypeId. I'll probably take another crack at it soon with a different approach, but if I don't, here's what I have:https://github.com/plof27/bevy/tree/basic-archetype-invariants
Some things we worked out:
ArchetypeInvariantto construct common invariants is a nice design. These would then be accompanied byadd_archetype_invariantmethods on bothWorldandApp. The app method would add the invariant to all worlds by default.ComponentIds means that the user needs to pass&mut Worldwhenever they initialize an archetype invariant, since we cannot ensure that the invariant's components have been intialized in the world before the invariant is added. This causes problems with the app builder API, in addition to being messy.TypeIds causes problems because we cannot initialize components fromTypeIds, and we can't guarantee that all components will be initialized before the invariants are first checked.Archetypes store their components as a list (not really but it's basically a list) ofComponentIds, the archetype invariant system needs to useComponentIds at some level. This means that dynamic component support will likely come for free if the architecture is designed with it in mind. (This is why the prototype exposes theUntypedArchetypeInvariantstruct.)Customvariant toArchetypeStatementthat allows the user to provide a custom function, as a safety valve.ComponentIdin the invariant list, check if the archetype has this component. Store the results in a boolean array or as bits in an unsigned integer!predicate || consequence){A}and{A, B, C}are valid, but{A, B}is not, addingBfollowed byCwill cause the invariants to panic, but adding{B, C}simultaneously will not. We came to the conclusion that handling this case would be quite difficult without knowing which archetypes are currently in use. Furthermore, disallowing transitive invalidation improves safety. So, instead of handling this in the archetype invariant system, we propose two complementary strategies to mitigate this:EntityCommandscreate useless temporary archetypes #5074Open questions
TypeIds,ComponentIds) have non-trivial problems or limitations.&mut Archetypesappears, but this will likely cause bugs in the future. (The likely solution here is to add aMut-style smart pointer.)AppBuilderwould solve this 🙃10 remaining items
tbillington commentedon Feb 9, 2025
Should/would you be able to share the same identity between multiple components?
ElliottjPierce commentedon Apr 1, 2025
Some thoughts on general designs here:
First, I think the easiest part is to add:
I think that part is pretty uncontroversial, and we can implement it pretty quickly. When we create an
Archetypewe can just return a result based on if the included component's invariants are upheld. We can cache that result inEdges, and if an operation creates an invalid archetype, we can just return an error.I think we should start there. That also unblocks #18566. Taking advantage of these invariants to speed up queries, reduce access lists, etc, is really good (obviously), but I think those are separate issues for now.
Then, we can look at something like:
This specifies that a
Playercan not be spawned withoutPlayerInputDataand thatOtherInputDataandPlayerInputDataare inseparable. (everyis the inverse ofneeds, anduniteis the combination ofeveryandneeds).This is a bit more controversial, as downstream crates can add invariants to components. For example, if one crate inserts component
Ain a system, but another crate makes it invariant thatAhas aB, then the utility crate may break due to something it can not control. Proper error handling would help here, but this is still controversial.But, this doesn't need any additional design.
everyandunitecan reuse the infrastructure fromneeds.Finally, regarding
identityandelement_of:I think this can really naturally be part of #18540 (component groups). Specifically, a component group can be made unique such that only one component of that group may be on an archetype at any time.
For example,
Identitycan be a unique component group:Or, as another example,
bevy-trait-queryhasSinglewhich has better performance than&dyn MyTriat, but it assumes that only one of the entity's components implements the trait. This would allow that to be enforced at the archetype level for even more performance wins.This is easily the most controversial and hardest to design part of this. But I don't think designing this part should block
needsandneverIMO.Zeenobit commentedon Apr 1, 2025
With component requirements, enforcing archetype requirements can be done much more easily without any traits or new macro magic. I've been playing with
#[require(Expect<T>)]from my own library:Zeenobit/moonshine_util@3ae98ff
It's just a component that checks the existence of another component via a hook.
I imagine the same pattern could be extended to support things like
#[require(Never<T>)].ElliottjPierce commentedon Apr 1, 2025
Hooks are a viable path to this, but they are much slower than just caching the result in the archetype edges. I'm not opposed to this if we want archetype invariants that depend on the value of some components. But if we only want to enforce this at the component id/archetype id level, we can do this much faster without hooks.
Zeenobit commentedon Apr 1, 2025
I'm just weary of additional API complexity. It'd be a lot better if we could define archetype invariants within the same
requireclause/API rather than creating yet another new macro-driven syntax.If we could optimize this at the archetype level and still keep it within the scope of the
requireAPI, it'd be the best option in my opinion.ElliottjPierce commentedon Apr 1, 2025
I agree but I think it's worth it.
100% Agree here. That's the plan with #18566. But I also think we need the new macro syntax in addition for users that need it. Adding extra invariants could improve query performance in the long run, so allowing users to specify invariants beyond just requirements is important. And also, some
requied components will only be required on creation. We might not want an invariant for all of them, but that's a discussion for #18566.andriyDev commentedon Jun 5, 2025
First, the hook approach is the easy way out but provides very little of the benefit of archetype invariants here. One thing I would like to see as a result of Archetype Invariants is being able to show that
Query<&Transform, With<A>>andQuery<&mut Transform, With<B>>are disjoint (and so can be put into one system), given the archetype invariantAis mutually exclusive withB.I think we should split this feature into three PRs (or more):
add_really_required_component) - sidestep bikeshedding and just get the invariants applied and checked. This API should be like the runtime required components API - sidesteps any derive macro bikeshedding, and it's something we want anyway for dynamic components.I suspect these are all doable and don't have significant blockers. The main thing that's not clear to me is whether an archetype invariant should be a trait or not. Something I read above made me think that was the suggestion. I'm not sure if that's necessary though. I feel like we only need to handle a few cases for invariants:
On Insert of component A, an invariant does one of:
On remove of component A:
I think this is all the primitives we need to support pretty much all invariants. We'd need a little bit of verification on these "actions" in order for them to apply constraints to system access (e.g., required components today does not allow us to assume that A and B are always together, since a user can remove B afterwards).
SoyokazeRoom201 commentedon Feb 23, 2026
Hiiiii!!
I have searched lots of materials. This is a particularly complex chain of design and implementation IMO.
Component): If we want to achievekindly entityandarchetype invariant, we need everything-as-entities it can be directly implemented through relation, and users do not need two sets of languages. Implementation will also be much much simpler.when, notif. It is necessary to upgrade the archetype state change to form of transactional, which depends on the successful implementation of the above two. Only in this way can users use these features with peace of mind.Only when all the above are successfully implemented, this feature can be safely released.
alice-i-cecile commentedon Apr 4, 2026
My current analysis here, prompted by #23657, for posterity:
SoyokazeRoom201 commentedon Apr 5, 2026
Hi!!!! refer to #23658, some insights:
a) already have benches, but currently there are only very naive tests. However, based my personal analysis, I believe that the performance cost is very low. (only my personal view)
b) If only analysis is conducted without any restrictions(refuses), then options c) and d) cannot be achieved. The guarantee of soundness is in the return of Result Err, which prevents any downstream code from continuing to write Components to the Table.
c) The TODOs in the draft PR illustrate the possible directions, using the
Withoutlist injection method to theFilteredAccess. In short,Componentsstores a mutex list and injects it directly during registeration phase. In terms of mutex list implementation, this is a roughly SAT solver, but it has a polynomial complexity. There is a performance cost during the registration process, while the benefit is the runtime cost.d) This is the same concept as c) (for me). If it is ensured that the queries refer to different archetypes(in code, it sames both call the same API
is_ruled_out_by), then this situation will never occur.But more significant problems were exposed:
EntityCommandscreate useless temporary archetypes #5074, I think the core issue is not performance optimization, but transactional semantics consistent with the other database language. In short, it should be either all successful or all failed. But this is not the breaking of an invariant; rather, it will lead to unexpected behaviors for users.Another tradeoff that has to be made is the side-effects vs the guarantee of invariants. When side-effects(any replacement, insertion, remove) are introduced, maintaining invariants becomes impossible, at least it is an extremely complex problem with no guarantee of performance. This does not mean we do not allow the side-effects; instead, it means ensuring that they are orthogonal to verification. That is to say, side effects come first, and verification comes second. Eventually, verification may still fail, but invariants are guaranteed. This explains why the existing required components can never prevent removal.
Hope this will be of some help!