diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs
index 2f89f6df9ca85..3d19b7fb5a568 100644
--- a/crates/bevy_ui/src/lib.rs
+++ b/crates/bevy_ui/src/lib.rs
@@ -1,11 +1,13 @@
//! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games
//! # Basic usage
//! Spawn [`entity::UiCameraBundle`] and spawn UI elements with [`entity::ButtonBundle`], [`entity::ImageBundle`], [`entity::TextBundle`] and [`entity::NodeBundle`]
+//! When UI elements are present during startup, the [`entity::UiCameraBundle`] will be added automatically.
//! This UI is laid out with the Flexbox paradigm (see ) except the vertical axis is inverted
mod flex;
mod focus;
mod margins;
mod render;
+mod startup;
mod ui_node;
pub mod entity;
@@ -72,6 +74,10 @@ impl Plugin for UiPlugin {
.register_type::()
.register_type::()
.register_type::()
+ .add_startup_system_to_stage(
+ StartupStage::PostStartup,
+ startup::add_default_ui_cam_if_needed,
+ )
.add_system_to_stage(
CoreStage::PreUpdate,
ui_focus_system.label(UiSystem::Focus).after(InputSystem),
diff --git a/crates/bevy_ui/src/startup.rs b/crates/bevy_ui/src/startup.rs
new file mode 100644
index 0000000000000..ce0d02c788369
--- /dev/null
+++ b/crates/bevy_ui/src/startup.rs
@@ -0,0 +1,96 @@
+use crate::prelude::UiCameraBundle;
+use crate::ui_node::Node;
+use crate::CAMERA_UI;
+use bevy_ecs::prelude::{Commands, Entity, Query, With};
+use bevy_render::prelude::{Camera, OrthographicProjection};
+
+pub fn add_default_ui_cam_if_needed(
+ mut commands: Commands,
+ node_query: Query>,
+ ui_cam_query: Query<&Camera, With>,
+) {
+ let world_contains_nodes = node_query.iter().next().is_some();
+ let world_contains_ui_cam = ui_cam_query
+ .iter()
+ .any(|cam| cam.name.as_deref() == Some(CAMERA_UI));
+
+ if world_contains_nodes && !world_contains_ui_cam {
+ commands.spawn_bundle(UiCameraBundle::default());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::entity::{NodeBundle, UiCameraBundle};
+ use crate::startup::add_default_ui_cam_if_needed;
+ use bevy_ecs::prelude::{Stage, SystemStage, World};
+ use bevy_render::prelude::{Camera, OrthographicProjection};
+
+ #[test]
+ fn no_ui_camera_added_when_no_ui_nodes_exist() {
+ let mut world = World::default();
+
+ let mut startup_stage = SystemStage::parallel();
+ startup_stage.add_system(add_default_ui_cam_if_needed);
+
+ startup_stage.run(&mut world);
+
+ assert_eq!(
+ world
+ .query::<(&Camera, &OrthographicProjection)>()
+ .iter(&world)
+ .len(),
+ 0
+ );
+ }
+
+ #[test]
+ fn ui_camera_added_when_ui_nodes_exist() {
+ let mut world = World::default();
+
+ world.spawn().insert_bundle(NodeBundle::default());
+
+ let mut startup_stage = SystemStage::parallel();
+ startup_stage.add_system(add_default_ui_cam_if_needed);
+
+ startup_stage.run(&mut world);
+
+ assert_eq!(
+ world
+ .query::<(&Camera, &OrthographicProjection)>()
+ .iter(&world)
+ .len(),
+ 1
+ );
+ }
+
+ #[test]
+ fn no_duplicate_ui_camera_added_when_one_is_already_present() {
+ let mut world = World::default();
+
+ let cam_id = world.spawn().insert_bundle(UiCameraBundle::default()).id();
+
+ assert_eq!(
+ world
+ .query::<(&Camera, &OrthographicProjection)>()
+ .iter(&world)
+ .len(),
+ 1
+ );
+
+ let mut startup_stage = SystemStage::parallel();
+ startup_stage.add_system(add_default_ui_cam_if_needed);
+
+ startup_stage.run(&mut world);
+
+ assert_eq!(
+ world
+ .query::<(&Camera, &OrthographicProjection)>()
+ .iter(&world)
+ .len(),
+ 1
+ );
+
+ assert!(world.get::(cam_id).is_some());
+ }
+}
diff --git a/examples/2d/contributors.rs b/examples/2d/contributors.rs
index cefce614588a7..17089baa7b297 100644
--- a/examples/2d/contributors.rs
+++ b/examples/2d/contributors.rs
@@ -118,7 +118,6 @@ fn setup_contributor_selection(mut commands: Commands, asset_server: Res) {
commands.spawn_bundle(OrthographicCameraBundle::new_2d());
- commands.spawn_bundle(UiCameraBundle::default());
commands.spawn_bundle((SelectTimer, Timer::from_seconds(SHOWCASE_TIMER_SECS, true)));
diff --git a/examples/game/breakout.rs b/examples/game/breakout.rs
index 1bee7cb8df37c..38d690b0f1299 100644
--- a/examples/game/breakout.rs
+++ b/examples/game/breakout.rs
@@ -50,7 +50,6 @@ fn setup(mut commands: Commands, asset_server: Res) {
// cameras
commands.spawn_bundle(OrthographicCameraBundle::new_2d());
- commands.spawn_bundle(UiCameraBundle::default());
// paddle
commands
.spawn_bundle(SpriteBundle {
diff --git a/examples/scene/scene.rs b/examples/scene/scene.rs
index b8f821489451a..078e8d2c309e8 100644
--- a/examples/scene/scene.rs
+++ b/examples/scene/scene.rs
@@ -103,7 +103,6 @@ fn save_scene_system(world: &mut World) {
// This is only necessary for the info message in the UI. See examples/ui/text.rs for a standalone
// text example.
fn infotext_system(mut commands: Commands, asset_server: Res) {
- commands.spawn_bundle(UiCameraBundle::default());
commands.spawn_bundle(TextBundle {
style: Style {
align_self: AlignSelf::FlexEnd,
diff --git a/examples/tools/bevymark.rs b/examples/tools/bevymark.rs
index 290fdf42594aa..c67b65928eb54 100644
--- a/examples/tools/bevymark.rs
+++ b/examples/tools/bevymark.rs
@@ -83,7 +83,6 @@ fn setup(mut commands: Commands, asset_server: Res) {
let texture = asset_server.load("branding/icon.png");
commands.spawn_bundle(OrthographicCameraBundle::new_2d());
- commands.spawn_bundle(UiCameraBundle::default());
commands.spawn_bundle(TextBundle {
text: Text {
sections: vec![
diff --git a/examples/ui/button.rs b/examples/ui/button.rs
index 48e67b555ebf9..bdea65e6f9323 100644
--- a/examples/ui/button.rs
+++ b/examples/ui/button.rs
@@ -41,8 +41,6 @@ fn button_system(
}
fn setup(mut commands: Commands, asset_server: Res) {
- // ui camera
- commands.spawn_bundle(UiCameraBundle::default());
commands
.spawn_bundle(ButtonBundle {
style: Style {
diff --git a/examples/ui/font_atlas_debug.rs b/examples/ui/font_atlas_debug.rs
index aef0075bca909..083c8c68795cc 100644
--- a/examples/ui/font_atlas_debug.rs
+++ b/examples/ui/font_atlas_debug.rs
@@ -79,7 +79,6 @@ fn text_update_system(mut state: ResMut, time: Res