Skip to main content

split_screen/
split_screen.rs

1//! Renders four cameras to the same window to accomplish "split screen".
2
3use std::f32::consts::PI;
4
5use bevy::{
6    camera::Viewport,
7    feathers::{
8        controls::FeathersButton, dark_theme::create_dark_theme, display::caption, theme::UiTheme,
9        FeathersPlugins,
10    },
11    light::CascadeShadowConfigBuilder,
12    prelude::*,
13    ui_widgets::Activate,
14    window::WindowResized,
15};
16
17fn main() {
18    App::new()
19        .add_plugins((DefaultPlugins, FeathersPlugins))
20        .insert_resource(UiTheme(create_dark_theme()))
21        .add_systems(Startup, setup)
22        .add_systems(Update, set_camera_viewports)
23        .add_observer(on_activate_rotate_camera)
24        .run();
25}
26
27/// set up a simple 3D scene
28fn setup(
29    mut commands: Commands,
30    asset_server: Res<AssetServer>,
31    mut meshes: ResMut<Assets<Mesh>>,
32    mut materials: ResMut<Assets<StandardMaterial>>,
33) {
34    // plane
35    commands.spawn((
36        Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))),
37        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
38    ));
39
40    commands.spawn(WorldAssetRoot(
41        asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
42    ));
43
44    // Light
45    commands.spawn((
46        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
47        DirectionalLight {
48            shadow_maps_enabled: true,
49            ..default()
50        },
51        CascadeShadowConfigBuilder {
52            num_cascades: if cfg!(all(
53                feature = "webgl2",
54                target_arch = "wasm32",
55                not(feature = "webgpu")
56            )) {
57                // Limited to 1 cascade in WebGL
58                1
59            } else {
60                2
61            },
62            first_cascade_far_bound: 200.0,
63            maximum_distance: 280.0,
64            ..default()
65        }
66        .build(),
67    ));
68
69    // Cameras and their dedicated UI
70    for (index, (camera_name, camera_pos)) in [
71        ("Player 1", Vec3::new(0.0, 200.0, -150.0)),
72        ("Player 2", Vec3::new(150.0, 150., 50.0)),
73        ("Player 3", Vec3::new(100.0, 150., -150.0)),
74        ("Player 4", Vec3::new(-100.0, 80., 150.0)),
75    ]
76    .iter()
77    .enumerate()
78    {
79        let camera = commands
80            .spawn((
81                Camera3d::default(),
82                Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y),
83                Camera {
84                    // Renders cameras with different priorities to prevent ambiguities
85                    order: index as isize,
86                    ..default()
87                },
88                CameraPosition {
89                    pos: UVec2::new((index % 2) as u32, (index / 2) as u32),
90                },
91            ))
92            .id();
93
94        // Set up UI
95        let buttons_entity = commands.spawn_scene(buttons_panel()).id();
96        commands
97            .spawn((
98                UiTargetCamera(camera),
99                Node {
100                    width: percent(100),
101                    height: percent(100),
102                    ..default()
103                },
104                children![(
105                    Text::new(*camera_name),
106                    Node {
107                        margin: px(12).all(),
108                        ..default()
109                    },
110                ),],
111            ))
112            .add_child(buttons_entity);
113    }
114
115    fn buttons_panel() -> impl Scene {
116        bsn! {
117            Node {
118                position_type: PositionType::Absolute,
119                width: percent(100),
120                height: percent(100),
121                display: Display::Flex,
122                flex_direction: FlexDirection::Row,
123                justify_content: JustifyContent::SpaceBetween,
124                align_items: AlignItems::Center,
125                padding: UiRect::all(px(20)),
126            }
127            Children [
128                @rotate_button("<", Direction::Left)
129                --
130                @rotate_button(">", Direction::Right)
131            ]
132        }
133    }
134
135    fn rotate_button(text_caption: &'static str, direction: Direction) -> impl Scene {
136        bsn! {
137            RotateCamera(direction)
138            @FeathersButton {
139                @caption: bsn! { @caption(text_caption) },
140            }
141            Node {
142                width: px(40),
143            }
144        }
145    }
146}
147
148#[derive(Component)]
149struct CameraPosition {
150    pos: UVec2,
151}
152
153#[derive(Component, Clone, Default, PartialEq)]
154struct RotateCamera(Direction);
155
156#[derive(Clone, Default, PartialEq)]
157enum Direction {
158    #[default]
159    Left,
160    Right,
161}
162
163fn set_camera_viewports(
164    windows: Query<&Window>,
165    mut window_resized_reader: MessageReader<WindowResized>,
166    mut query: Query<(&CameraPosition, &mut Camera)>,
167) {
168    // We need to dynamically resize the camera's viewports whenever the window size changes
169    // so then each camera always takes up half the screen.
170    // A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup.
171    for window_resized in window_resized_reader.read() {
172        let window = windows.get(window_resized.window).unwrap();
173        let size = window.physical_size() / 2;
174
175        for (camera_position, mut camera) in &mut query {
176            camera.viewport = Some(Viewport {
177                physical_position: camera_position.pos * size,
178                physical_size: size,
179                ..default()
180            });
181        }
182    }
183}
184
185fn on_activate_rotate_camera(
186    event: On<Activate>,
187    button_query: Query<(&ComputedUiTargetCamera, &RotateCamera), With<FeathersButton>>,
188    mut camera_query: Query<&mut Transform, With<Camera>>,
189) {
190    let Ok((computed_target, RotateCamera(direction))) = button_query.get(event.entity) else {
191        return;
192    };
193
194    // Since TargetCamera propagates to the children, we can use it to find
195    // which side of the screen the button is on.
196    if let Some(mut camera_transform) = computed_target
197        .get()
198        .and_then(|camera| camera_query.get_mut(camera).ok())
199    {
200        let angle = match direction {
201            Direction::Left => -0.1,
202            Direction::Right => 0.1,
203        };
204        camera_transform.rotate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, angle));
205    }
206}