-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
bevy_scene: Add SceneFilter
#6793
Conversation
I think I'm going to switch |
On second thought, I don't think we really gain much by simply wrapping struct Filter {
components: HashSet<TypeId>,
resources: HashSet<TypeId>,
// other metadata...
} But it probably makes more sense to move that kind of metadata/logic to the container types: struct DynamicSceneBuilder {
// ...
component_filter: SceneFilter,
resource_filter: SceneFilter,
// other metadata...
} So I think it should be okay to keep this as a plain |
This is just my opinion as an user: I think this solution is perfectly clear and overall a better approach than customizing the AppTypeRegistry directly, which I found confusing in my project while trying out scenes for the first time. Regarding the second open question, I personally think the ordering mixup is a real footgun especially for newbies, who might be inclined to think there's no need for a particular order considering we're using a builder here and not a concrete DynamicScene. I can't seem to understand the point about the tradeoff though. If the extraction is moved on Regarding the third question, I believe there are a few components which are currently included in de/serialization but shouldn't. I'm thinking of I hope I helped. Thank you very much for your work! |
The main issue is that we're not able to store the iterator without running into lifetime issues. This makes sense if we consider the fact that a query's elements may or may not change between frames (i.e. entities added or removed). So the solution would be to consume the iterator and get the entities right away. Then in This might not be awful (relevant video), but it's worth consideration— especially if we expect a lot of entities.
Yeah that might make more sense and would match |
Here's some random guy on the internet's opinions, as requested! Bikeshed: Allow/Deny API namesI think Move extract_entity behavior to build()I actually think that allowing this ordering to have meaning is a feature not a bug. There could be times when within the same scene you would like components serialized on some entities but not others. Coming up with an example on the spot, supposed there was a card game that ran over a network, where the cards all have a physical position in space, including on the table and in the player hands. You want to send that scene over the internet, but you don't want the players to know what cards are in the opponents hand or facedown on the table. So when you send the scene to a player, you could do something like
You can even use this to create special extraction methods that might use different filters than the rest of the scene.
Default filtersI also like the idea of having SceneBuilder have a default filter. I think a default filter would probaby be closer to DenyList behavior, where all user-defined components would be allowed by default. You'd have to make sure that the components filtered by default are okay with whatever feature flags are enabled though. Probably out of scope for now, but perhaps using an attribute on components to have them filtered by default could be a future improvement. That said, the big open question then becomes what is expected behavior if somebody then uses I think modifying the default list is ideal behavior, but add If If we don't decide that entities should be extracted on build (previous section), we can add a If we do implement a default list Scene Filter API improvementsEven without a default component list, I think adding Example:
We could add |
That's actually a really good point! We may want to change the filter between groups of entities, which makes this behavior actually useful. I think we could support both by adding a
Yeah
I think I'd prefer to just keep fn apply_filters(scene_builder: &mut DynamicSceneBuilder) {
// `except`:
// If `SceneFilter::Allowlist` -> denies `MyComponent`
// If `SceneFilter::Denylist` -> allows `MyComponent`
scene_builder.except::<MyComponent>();
// `deny`:
// Always denies `MyComponent`
scene_builder.deny::<MyComponent>();
} |
I think we'll leave off the default type filtering for now. A lot of the types we might want to filter are in crates that aren't dependencies of |
Love these improvement ! Just my 2 cents
|
Thanks for the feedback! This makes sense. I'll create a followup PR with a proper example once this gets merged. |
pub fn from_world(world: &World, type_registry: &AppTypeRegistry) -> Self { | ||
let mut builder = | ||
DynamicSceneBuilder::from_world_with_type_registry(world, type_registry.clone()); | ||
pub fn from_world(world: &World) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a trait implementation? We could move this into an impl FromWorld for DynamicScene
since it doesn't need the second argument anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I just checked the FromWorld
trait definition and it looks like it actually takes &mut World
. So looks like we can't do this :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could impl From<&World> for DynamicScene
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly. I'm also starting to wonder if doing From
impls has a negative effect on discoverability. Constructors directly on the struct are clearly visible from the documentation, but the documented From
impls can be hard to miss.
Maybe we save these changes for a followup PR to allow for further discussion (if any)?
@@ -37,14 +37,13 @@ pub struct DynamicEntity { | |||
|
|||
impl DynamicScene { | |||
/// Create a new dynamic scene from a given scene. | |||
pub fn from_scene(scene: &Scene, type_registry: &AppTypeRegistry) -> Self { | |||
Self::from_world(&scene.world, type_registry) | |||
pub fn from_scene(scene: &Scene) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a trait implementation? We could move this into an impl From<&Scene> for DynamicScene
since it doesn't need the second argument anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this makes sense as well. I'll do this too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also decided to hold off on this. If anything, we can save this for a future PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm fine to leave this to follow-up.
@MrGVSV are you able to rebase and complete this? I'd like to move this forward, but the conflicts are non-trivial and the suggestions are good :) |
Sure thing! I'll try to do that either tonight or tomorrow. Just need to rebase and apply the changes discussed in #scenes-dev. |
Rebased and updated to include changes from #6846 that allowed resources to be included in scenes. This meant the following methods were added:
These were made separate from the component methods so users could have much more control over the filtering between resources and components (e.g. denylist for resources and allowlist for components) and to improve the overall clarity of what each method does. I also updated the PR description to reflect these changes and to include a couple of followup tasks. |
/// This method may be called for multiple components. | ||
/// | ||
/// Please note that this method is mutually exclusive with the [`deny`](Self::deny) method. | ||
/// Calling this one will replace the denylist with a new allowlist. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm: this behavior here is surprising to me. I was expecting this to be additive / incremental, for easy tweaking.
But after some thought, there's no sensible way to have both a list of "allow this" and "deny that": what do you do about unlisted types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops I think this is outdated. We did move to an incremental behavior: allowing a type in a denylist simply removes it from that list. I'll update the docs to match!
/// Types not contained within this set should not be allowed to be saved to an associated [`DynamicScene`]. | ||
/// | ||
/// [`DynamicScene`]: crate::DynamicScene | ||
Allowlist(HashSet<TypeId>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should this be TypeId
rather than ComponentId
? The scene filter tool would be useful with dynamic components, and the component/resource types need to be registered already.
I can live with this being moved to follow-up work though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe my reasoning was purely an ergonomics one. I wanted this kind of behavior:
filter
.allow::<Foo>()
.allow::<Bar>()
.allow::<Baz>();
And I'm not sure if it would be possible to achieve that by relying on ComponentId
since it requires access to the world (please correct me if I'm wrong).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah hmm. Right, that's much trickier.
I think in a follow-up then we should add two more enum variants that store ComponentId
for the more advanced cases at the expense of ergonomics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few quibbles, but nothing that can't be done in follow-up work.
A dedicated example demonstrating this would be particularly nice, but I don't want to block this further.
@MrGVSV can you update the docs this weekend? If so, I'll merge this Monday :) |
It feels like this issue would be fixed by #5781 and adding methods to create a register from an existing one. Would this PR still has an advantage if that were done, or would it be adding two ways to do the same thing? |
I think that's a good callout. I think we definitely could continue using
Those are what I can think of, at least haha. The last reason is that this PR provides a better API while #5781 is still in review. |
I created a followup PR based on this branch to add a dedicated example: #8955. |
But it still acts as a filter as things need to be registered
That could be worked around by adding better APIs to derive a new type registry from an existing one.
So you mean with this PR, the user would define a filter when creating their scene, and another hidden one (the type registry) would also be used without it being visible?
That seems like an easy fix by adding that to the type registry rather than needing a whole new concept
Same, that can be fixed on the type registry api
That's probably OK? Or it could live in the type registry?
Scenes are another world, so that's ok
I think that to go the way of this PR, we need first to truly make the type registry global and hide it from the user when creating a scene so that they don't have to care which registry the world they're working with currently has. The alterantive is to not add a new concept like scene filter but to improve the api on the type registry |
I think it would help if we broke down registry-based filters for a moment. For those that don't know, we currently have a "global"1 And, from what I gather, the main reason to use a While that API does avoid relying on the "hidden"
This means that, unless we force users to manually deserialize and spawn in their scenes, they will always need to not only be aware of the main We could add mechanics for syncing the registry filters back up to the I suppose, if the argument is to make the And again, the
I'm not sure we really want to conflate the registry with filtering. Yes, it can be used as a filter, but that's just a byproduct of its actual purpose: storing dynamic type information, mainly for (de)serialization. It doesn't make much sense outside of the context of scenes to treat it like a filter, so I don't think we should cater its API for that one specific use case. One nice thing about At best, users would need to create a new type registry per filter and then indicate whether or not it's an allowlist or a denylist. Also worth noting here is that wrapping the Lastly, should requirements or design change, we wouldn't need to continue messing with the
Unfortunately, apart from auto-registering types or some future built-in Rust solution, I don't think there's any hope for a user to not have to be at least marginally aware of the registry when working with scenes, regardless of the filtering mechanism used. Footnotes
|
I will say that I could see there being confusion if there is a component missing in my scene after load/unload, where I might think there is something wrong with the filter instead of wrong with my registry. But when that happens I'd probably check the documentation and hopefully find information about the registry in the scene filter documentation. When I used the scene code with the registry, I thought it was weird that it's asking for it as input when the world I'm getting the registry from is already a parameter. I didn't even think that there was some other use for the registry, to act as a filter or anything like that. Having a parameter named filter definitely makes that purpose more clear. |
I stumbled across this PR after spending a couple of weeks reading the code for @MrGVSV does a great job of explaining why a filter is preferable to dedicated a BackgroundRight now in my editor you can save Tilesets (single Entity with a Creating an individual The best way I can describe why is that TangentFor concerns about non- Another TangentThis would also be incredibly helpful to me if it makes it into 0.11; without it I'm gonna have to do something cursed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the right path forward for scenes as they exist today and the patterns we're currently using.
That being said, I think the current state of scene definitions is suboptimal:
- It is easy to accidentally include a "runtime only" component in a scene by forgetting to exclude it.
- The information about whether or not to include components like ComputedSize in a given entity should exist in the declaration of that entity's types, not floating around in a global filter registration (ex: the Bundle, the individual Components, or some yet-to-be-designed Prefab-like thing). I suspect we will land on something that would make this separation implicit (and therefore impossible to get wrong). Ex: by creating runtime-only things from the Prefab-like-thing-that-is-yet-to-be-designed.
- The pattern of treating the "runtime state" of a "normal" app world as a scene (while interesting) seems like it might need to go. Having the "scene state vs runtime state" separation makes things much cleaner I think.
All of that is a conversation for a later date though. For now, this makes the current approach to things way more usable.
I also do understand the argument to use TypeRegistries instead of creating a new SceneFilter concept. The "do we need this new concept" reflex is a good one to have. I think we could make TypeRegistries work for this, but I also agree that it would be contorting them for something that they weren't made for (or optimized for). I think keeping them separate concepts is the right call.
Objective
Currently,
DynamicScene
s extract all components listed in the given (or the world's) type registry. This acts as a quasi-filter of sorts. However, it can be troublesome to use effectively and lacks decent control.For example, say you need to serialize only the following component over the network:
To do this, you'd need to:
AppTypeRegistry
NPC
Option<String>
If we skip Step 3, then the entire scene might fail to serialize as
Option<String>
requires registration.Not only is this annoying and easy to forget, but it can leave users with an impossible task: serializing a third-party type that contains private types.
Generally, the third-party crate will register their private types within a plugin so the user doesn't need to do it themselves. However, this means we are now unable to serialize just that type— we're forced to allow everything!
Solution
Add the
SceneFilter
enum for filtering components to extract.This filter can be used to optionally allow or deny entire sets of components/resources. With the
DynamicSceneBuilder
, users have more control over how theirDynamicScene
s are built.To only serialize a subset of components, use the
allow
method:To serialize everything but a subset of components, use the
deny
method:Or create a custom filter:
Similar operations exist for resources:
View Resource Methods
To only serialize a subset of resources, use the
allow_resource
method:To serialize everything but a subset of resources, use the
deny_resource
method:Or create a custom filter:
Open Questions
Took @soqb's suggestion and made it so that the opposing method simply removes that type from the list.allow
anddeny
are mutually exclusive. Currently, they overwrite each other. Should this instead be a panic?Based on the feedback from @Testare it sounds like it might be better to just keep the current functionality (if anything we can open a separate PR that adds deferred methods for extraction, so the choice/performance hit is up to the user).DynamicSceneBuilder
extracts entity data as soon asextract_entity
/extract_entities
is called. Should this behavior instead be moved to thebuild
method to prevent ordering mixups (e.g..allow::<Foo>().extract_entity(entity)
vs.extract_entity(entity).allow::<Foo>()
)? The tradeoff would be iterating over the given entities twice: once at extraction and again at build.DynamicSceneBuilder
and have it as a separate parameter to the extraction methods (either in the existing ones or as addedextract_entity_with_filter
-type methods). Is this preferable?Should we include constructors that include common types to allow/deny? For example, aConsensus suggests we should. I may split this out into a followup PR, though.SceneFilter::standard_allowlist
that includes things likeParent
andChildren
?Should we add the ability to remove types from the filter regardless of whether an allowlist or denylist (e.g.See the first list itemfilter.remove::<Foo>()
)?ShouldWith the addedSceneFilter
be an enum? Would it make more sense as a struct that contains anis_denylist
boolean?SceneFilter::None
state (replacing the need to wrap in anOption
or rely on an emptyDenylist
), it seems an enum is better suited nowBikeshed: Do we like the naming convention? Should we instead useSounds like we're sticking withinclude
/exclude
terminology?allow
/deny
!Does this feature need a new example? Do we simply include it in the existing one (maybe even as a comment?)? Should this be done in a followup PR instead?Example will be added in a followup PRFollowup Tasks
SceneFilter
exampleComputedVisibility
, allowParent
, etc)Changelog
SceneFilter
enum for filtering components and resources when building aDynamicScene
DynamicSceneBuilder::with_filter
DynamicSceneBuilder::allow
DynamicSceneBuilder::deny
DynamicSceneBuilder::allow_all
DynamicSceneBuilder::deny_all
DynamicSceneBuilder::with_resource_filter
DynamicSceneBuilder::allow_resource
DynamicSceneBuilder::deny_resource
DynamicSceneBuilder::allow_all_resources
DynamicSceneBuilder::deny_all_resources
DynamicSceneBuilder::from_world_with_type_registry
DynamicScene::from_scene
andDynamicScene::from_world
no longer require anAppTypeRegistry
referenceMigration Guide
DynamicScene::from_scene
andDynamicScene::from_world
no longer require anAppTypeRegistry
reference:Removed
DynamicSceneBuilder::from_world_with_type_registry
. Now the registry is automatically taken from the given world: