
# Objective Fix #8267. Fixes half of #7840. The `ComputedVisibility` component contains two flags: hierarchy visibility, and view visibility (whether its visible to any cameras). Due to the modular and open-ended way that view visibility is computed, it triggers change detection every single frame, even when the value does not change. Since hierarchy visibility is stored in the same component as view visibility, this means that change detection for inherited visibility is completely broken. At the company I work for, this has become a real issue. We are using change detection to only re-render scenes when necessary. The broken state of change detection for computed visibility means that we have to to rely on the non-inherited `Visibility` component for now. This is workable in the early stages of our project, but since we will inevitably want to use the hierarchy, we will have to either: 1. Roll our own solution for computed visibility. 2. Fix the issue for everyone. ## Solution Split the `ComputedVisibility` component into two: `InheritedVisibilty` and `ViewVisibility`. This allows change detection to behave properly for `InheritedVisibility`. View visiblity is still erratic, although it is less useful to be able to detect changes for this flavor of visibility. Overall, this actually simplifies the API. Since the visibility system consists of self-explaining components, it is much easier to document the behavior and usage. This approach is more modular and "ECS-like" -- one could strip out the `ViewVisibility` component entirely if it's not needed, and rely only on inherited visibility. --- ## Changelog - `ComputedVisibility` has been removed in favor of: `InheritedVisibility` and `ViewVisiblity`. ## Migration Guide The `ComputedVisibilty` component has been split into `InheritedVisiblity` and `ViewVisibility`. Replace any usages of `ComputedVisibility::is_visible_in_hierarchy` with `InheritedVisibility::get`, and replace `ComputedVisibility::is_visible_in_view` with `ViewVisibility::get`. ```rust // Before: commands.spawn(VisibilityBundle { visibility: Visibility::Inherited, computed_visibility: ComputedVisibility::default(), }); // After: commands.spawn(VisibilityBundle { visibility: Visibility::Inherited, inherited_visibility: InheritedVisibility::default(), view_visibility: ViewVisibility::default(), }); ``` ```rust // Before: fn my_system(q: Query<&ComputedVisibilty>) { for vis in &q { if vis.is_visible_in_hierarchy() { // After: fn my_system(q: Query<&InheritedVisibility>) { for inherited_visibility in &q { if inherited_visibility.get() { ``` ```rust // Before: fn my_system(q: Query<&ComputedVisibilty>) { for vis in &q { if vis.is_visible_in_view() { // After: fn my_system(q: Query<&ViewVisibility>) { for view_visibility in &q { if view_visibility.get() { ``` ```rust // Before: fn my_system(mut q: Query<&mut ComputedVisibilty>) { for vis in &mut q { vis.set_visible_in_view(); // After: fn my_system(mut q: Query<&mut ViewVisibility>) { for view_visibility in &mut q { view_visibility.set(); ``` --------- Co-authored-by: Robert Swain <robert.swain@gmail.com>
106 lines
3.4 KiB
Rust
106 lines
3.4 KiB
Rust
use std::marker::PhantomData;
|
|
|
|
use bevy_app::{App, Last, Plugin};
|
|
use bevy_core::Name;
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_log::warn;
|
|
use bevy_utils::{get_short_name, HashSet};
|
|
|
|
use crate::Parent;
|
|
|
|
/// When enabled, runs [`check_hierarchy_component_has_valid_parent<T>`].
|
|
///
|
|
/// This resource is added by [`ValidParentCheckPlugin<T>`].
|
|
/// It is enabled on debug builds and disabled in release builds by default,
|
|
/// you can update this resource at runtime to change the default behavior.
|
|
#[derive(Resource)]
|
|
pub struct ReportHierarchyIssue<T> {
|
|
/// Whether to run [`check_hierarchy_component_has_valid_parent<T>`].
|
|
pub enabled: bool,
|
|
_comp: PhantomData<fn(T)>,
|
|
}
|
|
|
|
impl<T> ReportHierarchyIssue<T> {
|
|
/// Constructs a new object
|
|
pub fn new(enabled: bool) -> Self {
|
|
ReportHierarchyIssue {
|
|
enabled,
|
|
_comp: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T> PartialEq for ReportHierarchyIssue<T> {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.enabled == other.enabled
|
|
}
|
|
}
|
|
|
|
impl<T> Default for ReportHierarchyIssue<T> {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: cfg!(debug_assertions),
|
|
_comp: PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// System to print a warning for each [`Entity`] with a `T` component
|
|
/// which parent hasn't a `T` component.
|
|
///
|
|
/// Hierarchy propagations are top-down, and limited only to entities
|
|
/// with a specific component (such as `InheritedVisibility` and `GlobalTransform`).
|
|
/// This means that entities with one of those component
|
|
/// and a parent without the same component is probably a programming error.
|
|
/// (See B0004 explanation linked in warning message)
|
|
pub fn check_hierarchy_component_has_valid_parent<T: Component>(
|
|
parent_query: Query<
|
|
(Entity, &Parent, Option<&Name>),
|
|
(With<T>, Or<(Changed<Parent>, Added<T>)>),
|
|
>,
|
|
component_query: Query<(), With<T>>,
|
|
mut already_diagnosed: Local<HashSet<Entity>>,
|
|
) {
|
|
for (entity, parent, name) in &parent_query {
|
|
let parent = parent.get();
|
|
if !component_query.contains(parent) && !already_diagnosed.contains(&entity) {
|
|
already_diagnosed.insert(entity);
|
|
warn!(
|
|
"warning[B0004]: {name} with the {ty_name} component has a parent without {ty_name}.\n\
|
|
This will cause inconsistent behaviors! See https://bevyengine.org/learn/errors/#b0004",
|
|
ty_name = get_short_name(std::any::type_name::<T>()),
|
|
name = name.map_or("An entity".to_owned(), |s| format!("The {s} entity")),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run criteria that only allows running when [`ReportHierarchyIssue<T>`] is enabled.
|
|
pub fn on_hierarchy_reports_enabled<T>(report: Res<ReportHierarchyIssue<T>>) -> bool
|
|
where
|
|
T: Component,
|
|
{
|
|
report.enabled
|
|
}
|
|
|
|
/// Print a warning for each `Entity` with a `T` component
|
|
/// whose parent doesn't have a `T` component.
|
|
///
|
|
/// See [`check_hierarchy_component_has_valid_parent`] for details.
|
|
pub struct ValidParentCheckPlugin<T: Component>(PhantomData<fn() -> T>);
|
|
impl<T: Component> Default for ValidParentCheckPlugin<T> {
|
|
fn default() -> Self {
|
|
Self(PhantomData)
|
|
}
|
|
}
|
|
|
|
impl<T: Component> Plugin for ValidParentCheckPlugin<T> {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<ReportHierarchyIssue<T>>().add_systems(
|
|
Last,
|
|
check_hierarchy_component_has_valid_parent::<T>
|
|
.run_if(resource_equals(ReportHierarchyIssue::<T>::new(true))),
|
|
);
|
|
}
|
|
}
|