Skip to content
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

'Entity with Focused component must also have a Focusable component error when adding Focusable to non-UI element #34

Open
PetoMPP opened this issue Apr 30, 2023 · 2 comments
Labels
bug Something isn't working documentation Improvements or additions to documentation

Comments

@PetoMPP
Copy link

PetoMPP commented Apr 30, 2023

Hey, I encountered a problem some days ago when trying to make sprite entities with Focusable. The bundle I'm trying to spawn looks as following:

#[derive(Bundle)]
pub struct FighterBundle {
    fighter: Fighter,
    style: FighterStyle,
    sprite: SpriteBundle,
    focusable: Focusable,
}

Unfortunately, after spawning the bundle and moving mouse over game window program panics with thread 'Compute Task Pool (0)' panicked at 'Entity with Focusedcomponent must also have a Focusable component: QueryDoesNotMatch(3v0)', ...\bevy-ui-navigation-0.24.0\src\systems.rs:396:56.
This issue happens regardless of existence of FocusableButtons in the World or input_mapping.focus_follows_mouse value.
During the testing I noticed that the entity spawned with bundle has the Focusable component, but after mouse move it's replaced by Focused component instead of added to it.

@nicopap nicopap added bug Something isn't working documentation Improvements or additions to documentation labels May 1, 2023
@nicopap
Copy link
Owner

nicopap commented May 1, 2023

Hello. I see what's going on here.

For a sprite-based focus system, you need to do a bit more work than the base UI case.

You can see an example implementation for handling mouse input in the infinite_upgrades.rs example:

// Since we do not use UI nodes for navigation, but instead 2d sprites,
// we need to define our own mouse pointer system.
//
// One additional complexity is that since we move the camera,
// we have to account for it in the mouse picking system.
//
// TODO: make some functions in bevy_ui/navigation/systems.rs public so that
// this is more user-friendly.
pub fn mouse_pointer_system(
camera: Query<(&GlobalTransform, &Camera), With<Camera2d>>,
camera_moving: Query<(), (Changed<GlobalTransform>, With<Camera2d>)>,
primary_query: Query<&Window, With<PrimaryWindow>>,
mouse: Res<Input<MouseButton>>,
focusables: Query<(&GlobalTransform, &Sprite, Entity), With<Focusable>>,
focused: Query<Entity, With<Focused>>,
mut nav_cmds: EventWriter<NavRequest>,
) {
// If the camera is currently moving, skip mouse pointing
if camera_moving.iter().next().is_some() {
return;
}
let Ok(window) = primary_query.get_single() else { return; };
let cursor_pos = match window.cursor_position() {
Some(c) => c,
None => return,
};
let (camera_transform, camera) = match camera.iter().next() {
Some(c) => c,
None => return,
};
let window_size = Vec2::new(window.width(), window.height());
let ndc = (cursor_pos / window_size) * 2.0 - Vec2::ONE;
let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse();
let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0));
let world_cursor_pos: Vec2 = world_pos.truncate();
let released = mouse.just_released(MouseButton::Left);
let pressing = mouse.pressed(MouseButton::Left);
let focused = match focused.get_single() {
Ok(c) => c,
Err(_) => return,
};
let under_mouse = focusables
.iter()
.filter(|(transform, sprite, _)| is_in_sizeable(world_cursor_pos, *transform, *sprite))
.max_by_key(|elem| FloatOrd(elem.0.translation().z))
.map(|elem| elem.2);
let to_target = match under_mouse {
Some(c) => c,
None => return,
};
let hover_focused = under_mouse == Some(focused);
if (pressing || released) && !hover_focused {
nav_cmds.send(NavRequest::FocusOn(to_target));
}
if released {
nav_cmds.send(NavRequest::Action);
}
}

You need to add it as a system as follow:

// Add your own cursor navigation system
// by using `NavigationPlugin::<MyOwnNavigationStrategy>::new()`
// See the [`bevy_ui_navigation::MenuNavigationStrategy`] trait.
//
// You can use a custom gamepad directional handling system if you want to.
// This could be useful if you want such navigation in 3d space
// to take into consideration the 3d camera perspective.
//
// Here we use the default one provided by `bevy_ui` because
// it is already capable of handling navigation in 2d space
// (even using `Sprite` over UI `Node`)
.add_plugin(NavigationPlugin::new())
// Since gamepad input already works for Sprite-based menus,
// we add back the default gamepad input handling from `bevy_ui`.
// default_gamepad_input depends on NavigationInputMapping so we
// need to also add this resource back.
.init_resource::<InputMapping>()
.add_system(default_gamepad_input.before(NavRequestSystem))
.add_system(mouse_pointer_system.before(NavRequestSystem))

To be fair, the documentation is very misleading on this point. So I'll leave this issue open as a reminder to update the docs to fit the way you are supposed to be using bevy-ui-navigation.

Also note you can't both have ui navigation and sprite navigation plugin running at the same time. This should be an easy fix, but I never came across the need, so I didn't realize this is a needed feature…

Make sure to not add DefaultNavigationPlugins, but instead NavigationPlugin::new(), and manually add the input handling as shown in infinite_upgrade

@PetoMPP
Copy link
Author

PetoMPP commented May 1, 2023

Thanks a lot for the reply! I managed to apply the changes so it works now, but I had to navigate between entities and buttons, so I had come up with following mouse handling system, the rest is done as in the snippets you've provided. This solution doesn't care about camera movement and is using Resource to store last cursor location. It also uses Sizeable component, which is just a trait implementation for ScreenSize.
It's not definitely most optimized approach, but it's doing its job ;)

fn mouse_pointer_system(
    primary_windows: Query<&Window, With<PrimaryWindow>>,
    cameras: Query<(&GlobalTransform, &Camera)>,
    mut last_location: ResMut<LastPointerLocation>,
    mouse_buttons: Res<Input<MouseButton>>,
    focusables: Query<(
        &GlobalTransform,
        Option<&Sizeable>,
        Option<&Node>,
        Entity,
        &Focusable,
    )>,
    focused: Query<Entity, With<Focused>>,
    mut nav_cmds: EventWriter<NavRequest>,
) {
    let (camera_transform, camera) = cameras.single();
    let window = primary_windows.single();
    if let Some(cursor_position) = window.cursor_position() {
        let just_released = mouse_buttons.just_released(MouseButton::Left);
        match cursor_position == **last_location {
            true => {
                if !just_released {
                    return;
                }
            }
            false => **last_location = cursor_position,
        }
        if let Some(cursor_position_world) =
            camera.viewport_to_world_2d(camera_transform, cursor_position)
        {
            let focused = match focused.get_single() {
                Ok(c) => c,
                Err(_) => return,
            };
            let cursor_position = Vec2::new(cursor_position.x, window.height() - cursor_position.y);
            let under_mouse = focusables
                .iter()
                .filter(|(_, _, _, _, f)| f.state() != FocusState::Blocked)
                .filter(|(t, s, n, _, _)| {
                    if let Some(sizeable) = s {
                        if is_in_sizeable(cursor_position_world, t, *sizeable) {
                            return true;
                        }
                    }
                    if let Some(node) = n {
                        if is_in_sizeable(cursor_position, t, *node) {
                            return true;
                        }
                    }
                    false
                })
                .max_by_key(|elem| FloatOrd(elem.0.translation().z))
                .map(|(_, _, _, e, _)| e);

            let to_target = match under_mouse {
                Some(c) => c,
                None => return,
            };

            if just_released {
                nav_cmds.send(NavRequest::Action);
            }
            if to_target != focused {
                nav_cmds.send(NavRequest::FocusOn(to_target));
            }
        }
    }
}

@PetoMPP PetoMPP closed this as completed May 1, 2023
@PetoMPP PetoMPP reopened this May 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

2 participants