#bevy-ui #navigation #bevy #ui #keyboard #gamepad #bevy-plugin

bevy-ui-navigation

一个使您的游戏实现UI导航变得易如反掌的Bevy插件

35个版本 (破坏性)

0.33.1 2023年11月15日
0.32.0 2023年10月11日
0.28.0 2023年7月20日
0.24.0 2023年3月6日
0.14.0 2022年3月21日

#85 in 游戏开发

MIT/Apache

145KB
2K SLoC

Bevy UI导航

Bevy tracking Latest version MIT/Apache 2.0 Documentation

Bevy引擎默认UI库的通用UI导航算法。

[dependencies]
bevy-ui-navigation = "0.33.1"

详细设计规范可在此处找到

示例

查看examples目录中的Bevy示例。

Demonstration of "Ultimate navigation" example

Cargo功能

此包公开了cuicui_dsl功能。默认禁用。启用它将添加dsl模块,并定义可使用dsl!宏的NavigationDsl

此包公开了bevy_ui功能。默认启用。关闭此功能可以让您在没有需要Bevy render功能的情况下编译此包,但是它需要您实现自己的输入处理。查看systems模块以获取实现您自己的输入处理的提示。

此包公开了pointer_focus功能。默认启用。禁用它将删除鼠标支持,并删除对bevy_mod_picking的依赖。

用法

查看此示例以获取快速入门指南。

包文档详尽,但出于实际原因不包括许多示例。此页面包含大多数文档示例,您应查看示例目录以查看展示此包所有功能的示例。

简单案例

要创建一个简单的菜单,在按钮之间进行导航,只需将ButtonBundle的使用替换为FocusableButtonBundle

您需要创建自己的系统来更改聚焦元素的颜色,并手动添加输入系统,但有了这个设置,您将获得:基于物理位置的完整导航,支持控制器、鼠标和键盘。包括可重绑定的映射

use bevy::prelude::*;
use bevy_ui_navigation::prelude::*;
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(DefaultNavigationPlugins)
        .run();
}

使用 InputMapping 资源来更改键盘和游戏手柄按钮的映射。

如果您想完全更改输入的处理方式,请按照以下步骤操作。所有与导航引擎的交互都是通过 EventWriter<NavRequest> 实现的。

use bevy::prelude::*;
use bevy_ui_navigation::prelude::*;

fn custom_input_system_emitting_nav_requests(mut events: EventWriter<NavRequest>) {
    // handle input and events.send(NavRequest::FooBar)
}

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, NavigationPlugin::new()))
        .add_systems(Update, custom_input_system_emitting_nav_requests)
        .run();
}

查看 examples 目录 以获取更多示例代码。

bevy-ui-navigation 提供了多种处理导航操作的方法。查看 NavEventReaderExt 特性(以及 NavEventReader 结构方法)以了解您可以做什么。

use bevy::{app::AppExit, prelude::*};
use bevy_ui_navigation::prelude::*;

#[derive(Component)]
enum MenuButton {
    StartGame,
    ToggleFullscreen,
    ExitGame,
    Counter(i32),
    //.. etc.
}

fn handle_nav_events(
    mut buttons: Query<&mut MenuButton>,
    mut events: EventReader<NavEvent>,
    mut exit: EventWriter<AppExit>
) {
    // Note: we have a closure here because the `buttons` query is mutable.
    // for immutable queries, you can use `.activated_in_query` which returns an iterator.
    // Do something when player activates (click, press "A" etc.) a `Focusable` button.
    events.nav_iter().activated_in_query_foreach_mut(&mut buttons, |mut button| match &mut *button {
        MenuButton::StartGame => {
            // start the game
        }
        MenuButton::ToggleFullscreen => {
            // toggle fullscreen here
        }
        MenuButton::ExitGame => {
            exit.send(AppExit);
        }
        MenuButton::Counter(count) => {
            *count += 1;
        }
        //.. etc.
    })
}

焦点导航在整个 UI 树中工作,无论您将可聚焦实体放在何处或如何。您只需朝您想要的方向移动,就可以到达那里。

任何 Entity 都可以通过添加 Focusable 组件来转换为可聚焦实体。要做到这一点,只需

# use bevy::prelude::*;
# use bevy_ui_navigation::prelude::Focusable;
fn system(mut cmds: Commands, my_entity: Entity) {
    cmds.entity(my_entity).insert(Focusable::default());
}

就是这样!现在 my_entity 是导航树的一部分。玩家可以使用控制器像选择其他任何 Focusable 元素一样选择它。

您可能希望将聚焦按钮以不同于其他按钮的方式渲染,这可以通过以下方式使用 Changed<Focusable> 查询参数实现

use bevy::prelude::*;
use bevy_ui_navigation::prelude::{FocusState, Focusable};

fn button_system(
    mut focusables: Query<(&Focusable, &mut BackgroundColor), Changed<Focusable>>,
) {
    for (focus, mut color) in focusables.iter_mut() {
        let new_color = if matches!(focus.state(), FocusState::Focused) {
            Color::RED
        } else {
            Color::BLACK
        };
        *color = new_color.into();
    }
}

快速反馈

您希望交互反馈是快速的。这意味着交互反馈应该与焦点更改在同一个帧中运行。为了使这每帧发生,您应该在您的应用中使用 NavRequestSystem 标签将 button_system 添加到您的应用中

use bevy::prelude::*;
use bevy_ui_navigation::prelude::{NavRequestSystem, NavRequest, NavigationPlugin};

fn custom_mouse_input(mut events: EventWriter<NavRequest>) {
    // handle input and events.send(NavRequest::FooBar)
}

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, NavigationPlugin::new()))
        // ...
        .add_systems(Update, (
            // Add input systems before the focus update system
            custom_mouse_input.before(NavRequestSystem),
            // Add the button color update system after the focus update system
            button_system.after(NavRequestSystem),
        ))
        // ...
        .run();
}
// Implementation from earlier
fn button_system() {}

更复杂的使用案例

锁定

如果您需要暂时抑制导航算法,可以将 Focusable 声明为 Focusable::lock

例如,如果您想实现具有自己控制的自定义小部件,或者您想在游戏中禁用菜单导航,这将非常有用。要恢复导航系统,您需要发送 NavRequest::Free

NavRequest::FocusOn

您不能直接操作哪个实体被聚焦,因为我们需要在后台跟踪很多事务以使导航按预期工作。但是,您可以使用 NavRequest::FocusOn 将聚焦元素设置为任何任意的 Focusable 实体。

use bevy::prelude::*;
use bevy_ui_navigation::prelude::NavRequest;

fn set_focus_to_arbitrary_focusable(
    entity: Entity,
    mut requests: EventWriter<NavRequest>,
) {
    requests.send(NavRequest::FocusOn(entity));
}

设置第一个聚焦元素

您可能希望能够选择哪个元素首先获得焦点。默认情况下,系统会选择它找到的第一个Focusable。要改变这种行为,使用FocusableFocusable::prioritized方法创建一个优先级更高的Focusable

MenuBuilder

假设您有一个更复杂的游戏,其中包含菜单、子菜单和子子菜单等。例如,在您的日常2021年AAA游戏中,要更改抗锯齿,您需要通过几个菜单进行操作

game menu → options menu → graphics menu → custom graphics menu → AA

在这种情况下,您需要能够指定前一个菜单中的哪个按钮可以跳转到下一个菜单(例如,您需要按游戏菜单中的“选项”按钮才能访问选项菜单)。

为此,您需要使用MenuBuilder

MenuBuilder的高级使用方法如下

  1. 首先,您需要使用MenuBuilder::Root创建一个“根”菜单。
  2. 您需要在ECS中创建一个带有Focusable组件的“选项”按钮。要将按钮链接到您的选项菜单,您需要执行以下操作之一
    • 除了将Focusable组件添加到您的选项按钮外,还要添加一个Name("opt_btn_name")组件。
    • 预先创建选项按钮并将它的Entity id(例如:let opt_btn = commands.spawn(FocusableButtonBundle).id();)存储起来
  3. 到包含所有选项菜单Focusable实体的NodeBundle中,您需要添加以下组件

在代码中,这看起来像这样

use bevy::prelude::*;
use bevy_ui_navigation::prelude::{Focusable, MenuSetting, MenuBuilder};
use bevy_ui_navigation::components::FocusableButtonBundle;

struct SaveFile;
impl SaveFile {
    fn bundle(&self) -> impl Bundle {
        // UI bundle to show this in game
        NodeBundle::default()
    }
}
fn spawn_menu(mut cmds: Commands, save_files: Vec<SaveFile>) {
    let menu_node = NodeBundle {
        style: Style { flex_direction: FlexDirection::Column, ..Default::default()},
        ..Default::default()
    };
    let button = FocusableButtonBundle::from(ButtonBundle {
        background_color: Color::rgb(1.0, 0.3, 1.0).into(),
        ..Default::default()
    });
    let mut spawn = |bundle: &FocusableButtonBundle, name: &'static str| {
          cmds.spawn(bundle.clone()).insert(Name::new(name)).id()
    };
    let options = spawn(&button, "options");
    let graphics_option = spawn(&button, "graphics");
    let audio_options = spawn(&button, "audio");
    let input_options = spawn(&button, "input");
    let game = spawn(&button, "game");
    let quit = spawn(&button, "quit");
    let load = spawn(&button, "load");

    // Spawn the game menu
    cmds.spawn(menu_node.clone())
        // Root Menu                 vvvvvvvvvvvvvvvvv
        .insert((MenuSetting::new(), MenuBuilder::Root))
        .push_children(&[options, game, quit, load]);

    // Spawn the load menu
    cmds.spawn(menu_node.clone())
        // Sub menu accessible through the load button
        //                           vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
        .insert((MenuSetting::new(), MenuBuilder::EntityParent(load)))
        .with_children(|cmds| {
            // can only access the save file UI nodes from the load menu
            for file in save_files.iter() {
                cmds.spawn(file.bundle()).insert(Focusable::default());
            }
        });

    // Spawn the options menu
    cmds.spawn(menu_node)
        // Sub menu accessible through the "options" button
        //                           vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
        .insert((MenuSetting::new(), MenuBuilder::from_named("options")))
        .push_children(&[graphics_option, audio_options, input_options]);
}

使用此功能,您的游戏菜单将与选项菜单隔离,您只能通过发送 NavRequest::Action 来访问它,当 options_button 被聚焦时,或者通过发送一个 NavRequest::FocusOn(entity),其中 entitygraphics_optionaudio_optionsinput_options 中的任何一个。

注意,如果您使用的是 systems 模块 提供的默认输入系统之一,您无需手动发送 NavRequest

具体来说,Focusable 实体之间的导航将被限制在同一个 MenuSetting 的子 Focusable 中。这创建了一个自包含的菜单。

MenuSetting 类型

要定义一个菜单,您需要 MenuBuilderMenuSetting 组件。

MenuSetting 让您可以精细控制菜单内的导航方式

  • MenuSetting::new().wrapping() 启用循环导航,在屏幕边缘超出时“循环”到另一边。
  • MenuSetting::new().scope() 创建一个“范围”菜单,可以捕获即使聚焦实体在可从该菜单访问的另一个子菜单中时也会触发的 NavRequest::ScopeMove 请求。这就像您期望的选项卡菜单的行为。

有关详细信息,请参阅 MenuSetting 文档或“终极”菜单导航示例 "ultimate" menu navigation example

标记

如果您需要知道 NavEvent::FocusChanged 的来源是哪个菜单,您可以在 mark 模块中使用 NavMarker

"marking.rs" 示例 中有一个使用演示。

使用键盘回车键(Enter)进行菜单操作

触发菜单操作的默认 InputMapping 键是空格键。要使用回车键,更改 key_action 属性。

否则,如果您没有使用默认输入处理,请添加此系统

use bevy::prelude::*;
use bevy_ui_navigation::prelude::{NavRequest, NavRequestSystem};

fn main() {
    App::new()
        // ...
        .add_systems(Update, (
            return_trigger_action.before(NavRequestSystem),
        ));
}

fn return_trigger_action(mut requests: EventWriter<NavRequest>, input: Res<Input<KeyCode>>) {
    if input.just_pressed(KeyCode::Return) {
        requests.send(NavRequest::Action);
    }
}

变更日志

请查看变更日志:https://github.com/nicopap/ui-navigation/blob/master/CHANGELOG.md

版本矩阵

bevy 支持的最新版本
0.12 0.33.1
0.11 0.32.0
0.10 0.24.1
0.9 0.23.1
0.8 0.21.0
0.7 0.18.0
0.6 0.14.0

许可证

版权所有 © 2022 Nicola Papale

本软件根据您的选择,许可为MIT或Apache 2.0。有关详细信息,请参阅许可证目录。

依赖关系

~22–60MB
~1M SLoC