bevy 0.20.0

A refreshingly simple data-driven game engine and app framework
Documentation
//! Renders four cameras to the same window to accomplish "split screen".

use std::f32::consts::PI;

use bevy::{
    camera::Viewport,
    feathers::{
        controls::FeathersButton, dark_theme::create_dark_theme, display::caption, theme::UiTheme,
        FeathersPlugins,
    },
    light::CascadeShadowConfigBuilder,
    prelude::*,
    ui_widgets::Activate,
    window::WindowResized,
};

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, FeathersPlugins))
        .insert_resource(UiTheme(create_dark_theme()))
        .add_systems(Startup, setup)
        .add_systems(Update, set_camera_viewports)
        .add_observer(on_activate_rotate_camera)
        .run();
}

/// set up a simple 3D scene
fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // plane
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
    ));

    commands.spawn(WorldAssetRoot(
        asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
    ));

    // Light
    commands.spawn((
        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
        DirectionalLight {
            shadow_maps_enabled: true,
            ..default()
        },
        CascadeShadowConfigBuilder {
            num_cascades: if cfg!(all(
                feature = "webgl2",
                target_arch = "wasm32",
                not(feature = "webgpu")
            )) {
                // Limited to 1 cascade in WebGL
                1
            } else {
                2
            },
            first_cascade_far_bound: 200.0,
            maximum_distance: 280.0,
            ..default()
        }
        .build(),
    ));

    // Cameras and their dedicated UI
    for (index, (camera_name, camera_pos)) in [
        ("Player 1", Vec3::new(0.0, 200.0, -150.0)),
        ("Player 2", Vec3::new(150.0, 150., 50.0)),
        ("Player 3", Vec3::new(100.0, 150., -150.0)),
        ("Player 4", Vec3::new(-100.0, 80., 150.0)),
    ]
    .iter()
    .enumerate()
    {
        let camera = commands
            .spawn((
                Camera3d::default(),
                Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y),
                Camera {
                    // Renders cameras with different priorities to prevent ambiguities
                    order: index as isize,
                    ..default()
                },
                CameraPosition {
                    pos: UVec2::new((index % 2) as u32, (index / 2) as u32),
                },
            ))
            .id();

        // Set up UI
        let buttons_entity = commands.spawn_scene(buttons_panel()).id();
        commands
            .spawn((
                UiTargetCamera(camera),
                Node {
                    width: percent(100),
                    height: percent(100),
                    ..default()
                },
                children![(
                    Text::new(*camera_name),
                    Node {
                        margin: px(12).all(),
                        ..default()
                    },
                ),],
            ))
            .add_child(buttons_entity);
    }

    fn buttons_panel() -> impl Scene {
        bsn! {
            Node {
                position_type: PositionType::Absolute,
                width: percent(100),
                height: percent(100),
                display: Display::Flex,
                flex_direction: FlexDirection::Row,
                justify_content: JustifyContent::SpaceBetween,
                align_items: AlignItems::Center,
                padding: UiRect::all(px(20)),
            }
            Children [
                @rotate_button("<", Direction::Left)
                --
                @rotate_button(">", Direction::Right)
            ]
        }
    }

    fn rotate_button(text_caption: &'static str, direction: Direction) -> impl Scene {
        bsn! {
            RotateCamera(direction)
            @FeathersButton {
                @caption: bsn! { @caption(text_caption) },
            }
            Node {
                width: px(40),
            }
        }
    }
}

#[derive(Component)]
struct CameraPosition {
    pos: UVec2,
}

#[derive(Component, Clone, Default, PartialEq)]
struct RotateCamera(Direction);

#[derive(Clone, Default, PartialEq)]
enum Direction {
    #[default]
    Left,
    Right,
}

fn set_camera_viewports(
    windows: Query<&Window>,
    mut window_resized_reader: MessageReader<WindowResized>,
    mut query: Query<(&CameraPosition, &mut Camera)>,
) {
    // We need to dynamically resize the camera's viewports whenever the window size changes
    // so then each camera always takes up half the screen.
    // A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup.
    for window_resized in window_resized_reader.read() {
        let window = windows.get(window_resized.window).unwrap();
        let size = window.physical_size() / 2;

        for (camera_position, mut camera) in &mut query {
            camera.viewport = Some(Viewport {
                physical_position: camera_position.pos * size,
                physical_size: size,
                ..default()
            });
        }
    }
}

fn on_activate_rotate_camera(
    event: On<Activate>,
    button_query: Query<(&ComputedUiTargetCamera, &RotateCamera), With<FeathersButton>>,
    mut camera_query: Query<&mut Transform, With<Camera>>,
) {
    let Ok((computed_target, RotateCamera(direction))) = button_query.get(event.entity) else {
        return;
    };

    // Since TargetCamera propagates to the children, we can use it to find
    // which side of the screen the button is on.
    if let Some(mut camera_transform) = computed_target
        .get()
        .and_then(|camera| camera_query.get_mut(camera).ok())
    {
        let angle = match direction {
            Direction::Left => -0.1,
            Direction::Right => 0.1,
        };
        camera_transform.rotate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, angle));
    }
}