I am building a UI using Bevy 0.19's Feathers/BSN APIs.
There is a persistent Camera3d used for rendering the game world, as well as a separate persistent Camera2d used exclusively for UI rendering.
The Camera2d is created once and remains alive while UI screens are switched using a Bevy State. Each UI screen is spawned on OnEnter and despawned on OnExit.
use bevy::{
feathers::{
FeathersPlugins,
controls::{ButtonVariant, FeathersButton, FeathersTextInput, FeathersTextInputContainer},
dark_theme::create_dark_theme,
theme::{ThemedText, UiTheme},
},
input_focus::{AutoFocus, tab_navigation::TabGroup},
prelude::*,
text::{EditableText, TextEditChange},
ui_widgets::Activate,
};
#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
enum UiScreen {
#[default]
MainMenu,
Connect,
}
#[derive(Component, Clone, Copy, Default)]
struct MainMenuRoot;
#[derive(Component, Clone, Copy, Default)]
struct ConnectMenuRoot;
#[derive(Component, Clone, Copy, Default)]
struct ServerAddressInput;
#[derive(Component, Clone, Copy, Default)]
struct LoginInput;
#[derive(Component, Clone, Copy, Default)]
struct PasswordInput;
#[derive(Resource, Debug, Clone)]
pub struct ConnectionForm {
pub address: String,
pub login: String,
pub password: String,
}
impl Default for ConnectionForm {
fn default() -> Self {
Self {
address: "127.0.0.1:25565".into(),
login: String::new(),
password: String::new(),
}
}
}
pub struct UiPlugin;
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(FeathersPlugins)
.insert_resource(UiTheme(create_dark_theme()))
.init_resource::<ConnectionForm>()
.init_state::<UiScreen>()
.add_systems(Startup, ui_scene.spawn())
.add_systems(OnEnter(UiScreen::MainMenu), main_menu.spawn())
.add_systems(OnExit(UiScreen::MainMenu), despawn_main_menu)
.add_systems(OnEnter(UiScreen::Connect), connect_menu.spawn())
.add_systems(OnExit(UiScreen::Connect), despawn_connect_menu);
}
}
fn ui_scene() -> impl SceneList {
bsn_list![(
Camera2d
Camera {
order: 1,
clear_color: ClearColorConfig::None,
}
)]
}
fn main_menu() -> impl Scene {
bsn! {
(
Node {
width: percent(100),
height: percent(100),
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
row_gap: px(12),
}
MainMenuRoot
TabGroup
Children [
(
@FeathersButton {
@caption: bsn! {
Text("Start")
ThemedText
}
@variant: ButtonVariant::Primary,
}
AccessibleLabel("Start")
AutoFocus
on(|_: On<Activate>,
mut next: ResMut<NextState<UiScreen>>| {
next.set(UiScreen::Connect);
})
),
(
@FeathersButton {
@caption: bsn! {
Text("Quit")
ThemedText
}
}
AccessibleLabel("Quit")
on(|_: On<Activate>,
mut exit: MessageWriter<AppExit>| {
exit.write(AppExit::Success);
})
),
]
)
}
}
fn connect_menu() -> impl Scene {
bsn! {
(
Node {
width: percent(100),
height: percent(100),
display: Display::Flex,
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
row_gap: px(12),
}
ConnectMenuRoot
TabGroup
Children [
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(4),
width: px(320),
}
Children [
(
Text("Server address")
ThemedText
),
(
@FeathersTextInputContainer
Children [
(
@FeathersTextInput {
@visible_width: {Some(32.0)},
}
ServerAddressInput
EditableText::new("127.0.0.1:25565")
AccessibleLabel("Server address")
AutoFocus
on(update_server_address)
)
]
)
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(4),
width: px(320),
}
Children [
(
Text("Login")
ThemedText
),
(
@FeathersTextInputContainer
Children [
(
@FeathersTextInput {
@visible_width: {Some(32.0)},
}
LoginInput
EditableText::new("")
AccessibleLabel("Login")
on(update_login)
)
]
)
]
),
(
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
row_gap: px(4),
width: px(320),
}
Children [
(
Text("Password")
ThemedText
),
(
@FeathersTextInputContainer
Children [
(
@FeathersTextInput {
@visible_width: {Some(32.0)},
}
PasswordInput
EditableText::new("")
AccessibleLabel("Password")
on(update_password)
)
]
)
]
),
(
@FeathersButton {
@caption: bsn! {
Text("Connect")
ThemedText
}
@variant: ButtonVariant::Primary,
}
AccessibleLabel("Connect")
on(|_: On<Activate>,
form: Res<ConnectionForm>| {
info!(
address = %form.address,
login = %form.login,
"Connect requested"
);
})
),
(
@FeathersButton {
@caption: bsn! {
Text("Back")
ThemedText
}
}
AccessibleLabel("Back")
on(|_: On<Activate>,
mut next: ResMut<NextState<UiScreen>>| {
next.set(UiScreen::MainMenu);
})
),
]
)
}
}
fn update_server_address(
_change: On<TextEditChange>,
input: Single<&EditableText, With<ServerAddressInput>>,
mut form: ResMut<ConnectionForm>,
) {
form.address = input.value().to_string();
}
fn update_login(
_change: On<TextEditChange>,
input: Single<&EditableText, With<LoginInput>>,
mut form: ResMut<ConnectionForm>,
) {
form.login = input.value().to_string();
}
fn update_password(
_change: On<TextEditChange>,
input: Single<&EditableText, With<PasswordInput>>,
mut form: ResMut<ConnectionForm>,
) {
form.password = input.value().to_string();
}
fn despawn_main_menu(mut commands: Commands, roots: Query<Entity, With<MainMenuRoot>>) {
for entity in &roots {
commands.entity(entity).despawn();
}
}
fn despawn_connect_menu(mut commands: Commands, roots: Query<Entity, With<ConnectMenuRoot>>) {
for entity in &roots {
commands.entity(entity).despawn();
}
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, UiPlugin))
.add_systems(Startup, scene.spawn())
.run();
}
fn scene() -> impl SceneList {
bsn_list! [
(
#CircularBase
Mesh3d(asset_value(Circle::new(4.0)))
MeshMaterial3d::<StandardMaterial>(asset_value(Color::WHITE))
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2))
),
(
#Cube
Mesh3d(asset_value(Cuboid::new(1.0, 1.0, 1.0)))
MeshMaterial3d::<StandardMaterial>(asset_value(Color::srgb_u8(124, 144, 255)))
Transform::from_xyz(0.0, 0.5, 0.0)
),
(
PointLight {
shadow_maps_enabled: true,
}
Transform::from_xyz(4.0, 8.0, 4.0)
),
(
Camera3d
Msaa::Off
template_value(Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
)
]
}
After the MainMenuRoot hierarchy is despawned, the next frame should contain only the currently existing ConnectMenuRoot UI.
The new UI is rendered correctly, but the old UI appears to remain on screen as a visual "ghost".
Bevy version and features
Bevy version: 0.19.0
Bevy is used with the default features.
The application uses Bevy's feathers feature/plugins.
[Optional] Relevant system information
What you did
I am building a UI using Bevy 0.19's Feathers/BSN APIs.
There is a persistent Camera3d used for rendering the game world, as well as a separate persistent Camera2d used exclusively for UI rendering.
The Camera2d is created once and remains alive while UI screens are switched using a Bevy State. Each UI screen is spawned on OnEnter and despawned on OnExit.
What went wrong
After the MainMenuRoot hierarchy is despawned, the next frame should contain only the currently existing ConnectMenuRoot UI.
The new UI is rendered correctly, but the old UI appears to remain on screen as a visual "ghost".
Additional information
The issue is caused by
Msaa::OffonCamera3d.Changing size of window clears "ghost" - so I guess redraw is not triggered.
Removing additional camera also fixes it...perhaps this is not a bug, just skill issue. Up to you.