-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Guard against VisibilityClass duplicates #20891
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
Conversation
if let Some(mut visibility_class) = world.get_mut::<VisibilityClass>(entity) | ||
&& !visibility_class.contains(&TypeId::of::<C>()) | ||
{ | ||
visibility_class.push(TypeId::of::<C>()); | ||
} |
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.
We can reuse the TypeId
, we should probably have some warning when the entity does not have VisibilityClass
too.
let Some(mut visibility_class) = world.get_mut::<VisibilityClass>(entity) else {
return;
};
let type_id = TypeId::of::<C>();
if !visibility_class.contains(&type_id) {
visibility_class.push(type_id);
}
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 thought about doing it this way but didn't for some reason. If there were to be a warning, what would you expect it to tell you? It seems like the most likely scenario is that it gets automatically handled when someone adds a different component. I'm not sure in what scenario it would warn in the real world and if that would just confuse people.
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 updated the code match what you have. I think it's cleaner to do it this way but I'm not sure about the conventions of warning users so I'd like to get some feedback on that before making that call.
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'm not sure in what scenario it would warn in the real world and if that would just confuse people.
It could happen in case someone used this hook but forgot to #[require(VisibilityClass)]
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 something similar to:
bevy/crates/bevy_ecs/src/hierarchy.rs
Lines 463 to 472 in 1dfcde6
warn!( | |
"warning[B0004]: {}{name} with the {ty_name} component has a parent ({parent}) without {ty_name}.\n\ | |
This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004", | |
caller.map(|c| format!("{c}: ")).unwrap_or_default(), | |
ty_name = debug_name.shortname(), | |
name = name.map_or_else( | |
|| format!("Entity {entity}"), | |
|s| format!("The {s} entity") | |
), | |
); |
I wrote a test for this, we can include it to make sure it works as expected: #[derive(Component, Default, Clone, Reflect)]
#[require(VisibilityClass)]
#[reflect(Component, Default, Clone)]
#[component(on_add = add_visibility_class::<Self>)]
struct TestVisibilityClassHook;
#[test]
fn test_add_visibility_class_hook() {
let mut world = World::new();
let entity = world.spawn(TestVisibilityClassHook).id();
let entity_clone = world.spawn_empty().id();
world
.entity_mut(entity)
.clone_with_opt_out(entity_clone, |_| {});
let entity_visibility_class = world.entity(entity).get::<VisibilityClass>().unwrap();
assert_eq!(entity_visibility_class.len(), 1);
let entity_clone_visibility_class =
world.entity(entity_clone).get::<VisibilityClass>().unwrap();
assert_eq!(entity_clone_visibility_class.len(), 1);
} |
I added this test and also added you as a co-author. Thanks for the assists. I've been trying to get into Bevy contributions so it's nice to have the help. I'm still considering the warning situation and I'll report back when I make a decision for that. |
No worries, I wonder if failing loudly with a panic is more appropriate, because if this hook runs on an entity without a We could panic like this to include some debug information: pub fn add_visibility_class<C>(
mut world: DeferredWorld<'_>,
HookContext { entity, caller, .. }: HookContext,
) where
C: 'static,
{
let Some(mut visibility_class) = world.get_mut::<VisibilityClass>(entity) else {
let component_debug_name = DebugName::type_name::<C>();
let visibility_class_debug_name = DebugName::type_name::<VisibilityClass>();
panic!(
"{}Entity {entity} with the {} component must have the {} component",
caller.map(|c| format!("{c}: ")).unwrap_or_default(),
component_debug_name.shortname(),
visibility_class_debug_name.shortname(),
);
};
let type_id = TypeId::of::<C>();
if !visibility_class.contains(&type_id) {
visibility_class.push(type_id);
}
} Also, even though it's out of scope for this pull request, it's worth noting that nothing currently removes the visibility class once it has been added. Maybe we should track this as a separate issue? |
Co-authored-by: Dimitrios Loukadakis <[email protected]>
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, I understand why this happens now, but it's a bit weird. Or, maybe feels like there needs to be a way for certain components to be ignored when cloning @alice-i-cecile.
Still, if we're going to assert uniqueness, can we remove the Deref
and DerefMut
derives on VisibilityClass
and add an impl
block (plus iter)? It's a bit more work and I don't think it's likely that anyone is interacting with it manually atm but let's enforce invariants outside of just the hook, if they exist.
@eugineerd may have better opinions than me here. I know that you can blacklist components, but maybe we should have a way to configure defaults at the component level too. |
Cloning does trigger #[derive(Clone, Component, Default, Reflect, Deref, DerefMut)]
#[reflect(Component, Default, Clone)]
#[component(clone_behavior=Ignore)]
pub struct VisibilityClass(pub SmallVec<[TypeId; 1]>); |
I think we should open a new PR with that approach. Thanks! |
Thanks everyone for taking a look at the PR. Can I apply this new approach in this PR? Or do we want to make an entirely new one? |
Disregard my last comment. I'm going to close this one and create a new PR with the agreed upon approach. |
# Objective - This is the second attempt at "Fixes #20884" after conversation in #20891 ## Solution - I followed the approach discussed in #20891 to add `#[component(clone_behavior=Ignore)]` to avoid cloning this Component ## Testing - I tested the change with a simple repro. I'm not sure if I can unit test just the existence of this line but I'll see if I can and update accordingly. ```rust use bevy::{prelude::*, render::view::NoIndirectDrawing}; fn main() { App::new() .insert_resource(ClearColor(Color::BLACK)) .add_plugins(DefaultPlugins) .add_systems(Startup, startup) .run(); } fn startup( mut cmd: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>, mut al: ResMut<AmbientLight>, ) { al.brightness = 5000.; cmd.spawn(( Mesh3d(meshes.add(Cuboid::from_size(Vec3::new(4., 0.5, 0.1)))), MeshMaterial3d(materials.add(Color::srgb(1., 1., 0.))), Transform::from_xyz(0., 0., -2.), )); let mut id = cmd.spawn(( Mesh3d(meshes.add(Cuboid::from_length(1.))), MeshMaterial3d(materials.add(Color::srgba(0., 0., 1., 0.5))), Transform::from_xyz(-0.5, 0., 0.), )); id.clone_and_spawn() .insert(Transform::from_xyz(0.5, 0., 0.)); cmd.spawn(( Camera3d::default(), Transform::from_xyz(0., 0., 10.).looking_at(Vec3::ZERO, Vec3::Y), NoIndirectDrawing, )); } ``` ## Showcase Before <img width="178" height="202" alt="486232152-9f8685f6-fbc3-4bc1-b9bd-7e28177cc901" src="https://github.com/user-attachments/assets/2bca4f28-b956-49dc-83b4-edefef6ca595" /> After <img width="120" height="173" alt="486232412-bafd5e4a-69be-4249-894a-5f03dfe28b63" src="https://github.com/user-attachments/assets/1b911bb6-01c4-451a-b010-20aab505217e" /> Co-authored-by: Dimitrios Loukadakis <[email protected]>
Objective
clone_and_spawn
a Mesh3D, you get a duplicate in the VisibilityClass Component. This is especially noticable with transaparent materials.Solution
Testing
bevy_inspector_egui
Before the fix:

After the fix:

Sorry for the janky screenshots