diff --git a/example/Cargo.toml b/example/Cargo.toml index dd88843..5362d56 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -18,6 +18,10 @@ path = "src/sprites.rs" name = "dynamic" path = "src/dynamic.rs" +[[bin]] +name = "split_screen" +path = "src/split_screen.rs" + [[bin]] name = "material" path = "src/material.rs" diff --git a/example/src/dynamic.rs b/example/src/dynamic.rs index bfa16f4..312120e 100644 --- a/example/src/dynamic.rs +++ b/example/src/dynamic.rs @@ -3,7 +3,9 @@ /// how to update effect behavior dynamiclly /// ---------------------------------------------- use bevy::{ - core_pipeline::bloom::Bloom, diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}, image::ImageSamplerDescriptor, + core_pipeline::bloom::Bloom, + diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}, + image::ImageSamplerDescriptor, prelude::*, }; use bevy_enoki::{prelude::*, EnokiPlugin}; diff --git a/example/src/split_screen.rs b/example/src/split_screen.rs new file mode 100644 index 0000000..08b4039 --- /dev/null +++ b/example/src/split_screen.rs @@ -0,0 +1,341 @@ +/// ---------------------------------------------- +/// like dynamic example, but with 1, 2, or 4 cameras in split screen. +/// ---------------------------------------------- +use bevy::{ + core_pipeline::bloom::Bloom, + diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}, + image::ImageSamplerDescriptor, + input::common_conditions::input_just_pressed, + prelude::*, + render::camera::Viewport, + window::WindowResized, +}; +use bevy_enoki::{prelude::*, EnokiPlugin}; +use std::time::Duration; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(ImagePlugin { + default_sampler: ImageSamplerDescriptor::nearest(), + })) + .add_plugins(FrameTimeDiagnosticsPlugin::default()) + .add_plugins(EnokiPlugin) + .init_resource::() + .add_systems(Startup, (setup_cameras, setup)) + .add_systems(Update, (show_fps, change_dynamic, move_camera)) + .add_systems( + Update, + ( + (toggle_camera_layout, setup_cameras) + .chain() + .run_if(input_just_pressed(KeyCode::F1)), + set_camera_viewports + .run_if(on_event::.or(resource_changed::)) + .after(setup_cameras), + ), + ) + .run(); +} + +#[derive(Component)] +pub struct FpsText; + +#[derive(Deref, Component, DerefMut)] +pub struct ChangeTimer(Timer); + +#[derive(Deref, Component, DerefMut)] +pub struct Pcindex(f32); + +#[derive(Deref, Resource, DerefMut)] +pub struct ParticleMaterialAsset(Handle); + +#[derive(Component, Default, Debug, Reflect)] +struct CameraPositioning { + offset_right: bool, + offset_down: bool, + half_width: bool, + half_height: bool, +} + +#[derive(Component)] +struct CamMarker1; + +#[derive(Component)] +struct CamMarker2; + +#[derive(Component)] +struct CamMarker3; + +#[derive(Component)] +struct CamMarker4; + +// TODO with Single as default, crashes when changing to VerticalSplit. +// (and if 2-player vertical split as default, crashes when changing to quad) +#[derive(Resource, Debug, Default, Clone, PartialEq, Eq, Hash, Reflect)] +pub enum CameraLayout { + /// Single player game + Single, + /// 2 player vertical split game + VerticalSplit, + /// 4 player split + #[default] + QuadSplit, +} + +fn toggle_camera_layout(mut cam_layout: ResMut) { + info!("Toggling camera layout"); + *cam_layout = match *cam_layout { + CameraLayout::Single => CameraLayout::VerticalSplit, + CameraLayout::VerticalSplit => CameraLayout::QuadSplit, + CameraLayout::QuadSplit => CameraLayout::Single, + }; +} + +fn camera_bundle(order: isize) -> impl Bundle { + ( + Camera2d, + Camera { + clear_color: ClearColorConfig::Custom(Color::BLACK), + hdr: true, + // set order to render cameras with different priorities, to prevent ambiguities + order, + ..default() + }, + Bloom { + intensity: 0.1, + ..default() + }, + ) +} + +// despawn all cameras and respawn the correct number based on the CameraLayout +fn setup_cameras( + mut cmd: Commands, + q_cameras: Query< + Entity, + Or<( + With, + With, + With, + With, + )>, + >, + cam_layout: Res, +) { + // despawn all existing cameras + info!("Despawning {} cameras", q_cameras.iter().count()); + q_cameras.iter().for_each(|e| cmd.entity(e).despawn()); + info!("Spawning cameras for {:?}", *cam_layout); + // spawn the correct number of cameras for the number of local players + match *cam_layout { + CameraLayout::Single => { + info!("Spawning 1 camera"); + cmd.spawn((camera_bundle(1), CameraPositioning::default(), CamMarker1)); + } + CameraLayout::VerticalSplit => { + info!("Spawning 2 cameras"); + cmd.spawn(( + camera_bundle(1), + CameraPositioning { + half_width: true, + ..default() + }, + CamMarker1, + )); + cmd.spawn(( + camera_bundle(2), + CameraPositioning { + half_width: true, + offset_right: true, + ..default() + }, + CamMarker2, + )); + } + CameraLayout::QuadSplit => { + info!("Spawning 4 cameras"); + cmd.spawn(( + camera_bundle(1), + CameraPositioning { + half_width: true, + half_height: true, + ..default() + }, + CamMarker1, + )); + cmd.spawn(( + camera_bundle(2), + CameraPositioning { + offset_right: true, + half_width: true, + half_height: true, + ..default() + }, + CamMarker2, + )); + cmd.spawn(( + camera_bundle(3), + CameraPositioning { + half_width: true, + half_height: true, + offset_down: true, + ..default() + }, + CamMarker3, + )); + cmd.spawn(( + camera_bundle(4), + CameraPositioning { + half_width: true, + half_height: true, + offset_right: true, + offset_down: true, + ..default() + }, + CamMarker4, + )); + } + } +} + +// We trigger this if a WindowResized event is received, or if the CameraLayout changes. +fn set_camera_viewports( + mut resize_events: EventReader, + q_windows: Query<(Entity, &Window)>, + mut q_cam: Query<(&CameraPositioning, &mut Camera)>, +) { + info!("Setting camera viewports"); + // we want to process window entities that were resized, or if none were resized, do them all. + let mut windows_to_process = resize_events.read().map(|e| e.window).collect::>(); + if windows_to_process.is_empty() { + windows_to_process.extend(q_windows.iter().map(|t| t.0)); + } + + // We need to dynamically resize the camera's viewports whenever the window size changes + // so that each camera takes up half the screen (or whatever the layout is) + // A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup. + for window_entity in windows_to_process.iter() { + let window = q_windows.get(*window_entity).unwrap().1; + let size = window.physical_size(); + let half_x = size.x / 2; + let half_y = size.y / 2; + // info!( + // "Window physical size: {:?}, half_x: {:?}, half_y: {:?}", + // size, half_x, half_y + // ); + for (campos, mut camera) in &mut q_cam { + let mut physical_position = UVec2::ZERO; + let mut physical_size = size; + if campos.offset_right { + physical_position.x = half_x; + } + if campos.offset_down { + physical_position.y = half_y; + } + if campos.half_width { + physical_size.x = half_x; + } + if campos.half_height { + physical_size.y = half_y; + } + let viewport = Viewport { + physical_position, + physical_size, + ..default() + }; + info!( + "Setting viewport for camera {:?}: {:?}", + camera.order, viewport + ); + camera.viewport = Some(viewport); + } + } +} + +fn setup( + mut cmd: Commands, + mut materials: ResMut>, + server: Res, +) { + cmd.spawn(( + ChangeTimer(Timer::new(Duration::from_millis(300), TimerMode::Repeating)), + Pcindex(0.), + )); + + cmd.spawn(( + Text::default(), + TextFont { + font_size: 24., + ..default() + }, + FpsText, + )); + + let material_handle = materials.add(SpriteParticle2dMaterial::from_texture( + server.load("enoki.png"), + )); + + cmd.spawn(( + ParticleSpawner(material_handle), + ParticleEffectHandle(server.load("base.particle.ron")), + )); +} + +fn change_dynamic( + mut elapsed: Local, + mut query: Query<&mut ParticleEffectInstance>, + time: Res