Skip to content

Conversation

it-me-joda
Copy link
Contributor

@it-me-joda it-me-joda commented Sep 5, 2025

Objective

Solution

  • To avoid this duplicate case, I added a simple guard check to avoid adding a duplicate record. According to the documentation, there is rarely a case where there is more than one VisibilityClass. That said, there is probably a better option to fix it, but I didn't want to destroy performance or make a massive change to the underlying data structures.

Testing

  • There is some unit testing to cover this case
  • This was visually tested as well as verifying using bevy_inspector_egui

Before the fix:
image

After the fix:
image

Sorry for the janky screenshots

Comment on lines 682 to 690
if let Some(mut visibility_class) = world.get_mut::<VisibilityClass>(entity)
&& !visibility_class.contains(&TypeId::of::<C>())
{
visibility_class.push(TypeId::of::<C>());
}
Copy link
Contributor

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);
    }

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@dloukadakis dloukadakis Sep 5, 2025

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)]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something similar to:

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")
),
);

@hukasu hukasu added A-Rendering Drawing game state to the screen A-ECS Entities, components, systems, and events D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 5, 2025
@dloukadakis
Copy link
Contributor

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);
    }

Co-authored-by: Dimitrios Loukadakis <[email protected]>
@it-me-joda
Copy link
Contributor Author

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.

@dloukadakis
Copy link
Contributor

No worries, I wonder if failing loudly with a panic is more appropriate, because if this hook runs on an entity without a VisibilityClass component, we would be technically skipping adding the visibility class for that entity.

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?

@alice-i-cecile alice-i-cecile added the X-Contentious There are nontrivial implications that should be thought through label Sep 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events A-Rendering Drawing game state to the screen D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

VisibilityClass gets duplicated when cloning an entity, causing issues with transparency
5 participants