Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Splash screen allows exiting early (with escape key) #257

Merged
merged 12 commits into from
Aug 11, 2024
143 changes: 102 additions & 41 deletions src/screens/splash.rs
benfrankel marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
//! A splash screen that plays briefly at startup.

use bevy::{
prelude::*,
render::texture::{ImageLoaderSettings, ImageSampler},
};
use std::collections::VecDeque;

use bevy::{input::common_conditions::input_just_pressed, prelude::*};

use super::Screen;
use crate::{theme::prelude::*, AppSet};

pub(super) fn plugin(app: &mut App) {
// Spawn splash screen.
app.insert_resource(ClearColor(SPLASH_BACKGROUND_COLOR));

app.register_type::<SplashScreenContainer>();
app.register_type::<SplashScreenImageList>();
app.init_resource::<SplashScreenImageList>();

app.add_systems(OnEnter(Screen::Splash), spawn_splash);

// Animate splash screen.
Expand All @@ -35,48 +39,82 @@ pub(super) fn plugin(app: &mut App) {
)
.run_if(in_state(Screen::Splash)),
);

// Exit the splash screen early if the player hits escape.
app.add_systems(
Update,
exit_splash_screen
.run_if(input_just_pressed(KeyCode::Escape).and_then(in_state(Screen::Splash))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think that this reads better when written as two separate run_if because of the line break, but I'm not blocking on that.

Copy link
Collaborator

@benfrankel benfrankel Aug 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO that's confusing because you have to know whether that yields an and or an or behavior. Would be neat if run conditions could use && somehow to improve readability.

);
}

const SPLASH_BACKGROUND_COLOR: Color = Color::srgb(0.157, 0.157, 0.157);
const SPLASH_DURATION_SECS: f32 = 1.8;
const SPLASH_FADE_DURATION_SECS: f32 = 0.6;

fn spawn_splash(mut commands: Commands, asset_server: Res<AssetServer>) {
commands
.ui_root()
.insert((
Name::new("Splash screen"),
#[derive(Component, Reflect)]
#[reflect(Component)]
struct SplashScreenContainer;
will-hart marked this conversation as resolved.
Show resolved Hide resolved

#[derive(Resource, Reflect)]
#[reflect(Resource)]
struct SplashScreenImageList(VecDeque<Handle<Image>>);

impl FromWorld for SplashScreenImageList {
fn from_world(world: &mut World) -> Self {
let asset_server = world.resource::<AssetServer>();

// To show your own splash images, replace or add images here.
Self(VecDeque::from_iter(
[asset_server.load("images/splash.png")],
will-hart marked this conversation as resolved.
Show resolved Hide resolved
))
}
}

fn exit_splash_screen(mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Loading);
}

fn splash_image_bundle(image: Handle<Image>) -> impl Bundle {
let image = image.into();

(
Name::new("Splash image"),
ImageBundle {
style: Style {
margin: UiRect::all(Val::Auto),
width: Val::Percent(70.0),
..default()
},
image,
background_color: BackgroundColor(Color::srgba(0., 0., 0., 0.)),
..default()
},
UiImageFadeInOut {
total_duration: SPLASH_DURATION_SECS,
fade_duration: SPLASH_FADE_DURATION_SECS,
t: 0.0,
},
)
}

fn spawn_splash(
mut commands: Commands,
splash_images: Res<SplashScreenImageList>,
mut next_screen: ResMut<NextState<Screen>>,
) {
if splash_images.0.is_empty() {
will-hart marked this conversation as resolved.
Show resolved Hide resolved
// If there are no splash images, go directly to the loading screen.
next_screen.set(Screen::Loading);
} else {
// Spawn the UI root but wait for the first timer tick to show an image.
commands.ui_root().insert((
Name::new("Splash screen container"),
BackgroundColor(SPLASH_BACKGROUND_COLOR),
StateScoped(Screen::Splash),
))
.with_children(|children| {
children.spawn((
Name::new("Splash image"),
ImageBundle {
style: Style {
margin: UiRect::all(Val::Auto),
width: Val::Percent(70.0),
..default()
},
image: UiImage::new(asset_server.load_with_settings(
// This should be an embedded asset for instant loading, but that is
// currently [broken on Windows Wasm builds](https://github.com/bevyengine/bevy/issues/14246).
"images/splash.png",
|settings: &mut ImageLoaderSettings| {
// Make an exception for the splash image in case
// `ImagePlugin::default_nearest()` is used for pixel art.
settings.sampler = ImageSampler::linear();
},
)),
..default()
},
UiImageFadeInOut {
total_duration: SPLASH_DURATION_SECS,
fade_duration: SPLASH_FADE_DURATION_SECS,
t: 0.0,
},
));
});
SplashScreenContainer,
));
}
}

#[derive(Component, Reflect)]
Expand Down Expand Up @@ -135,8 +173,31 @@ fn tick_splash_timer(time: Res<Time>, mut timer: ResMut<SplashTimer>) {
timer.0.tick(time.delta());
}

fn check_splash_timer(timer: ResMut<SplashTimer>, mut next_screen: ResMut<NextState<Screen>>) {
if timer.0.just_finished() {
next_screen.set(Screen::Loading);
fn check_splash_timer(
mut commands: Commands,
mut timer: ResMut<SplashTimer>,
mut next_screen: ResMut<NextState<Screen>>,
mut splash_images: ResMut<SplashScreenImageList>,
containers: Query<Entity, With<SplashScreenContainer>>,
) {
if !timer.0.just_finished() {
return;
}

// Try to get the next splash image. If there isn't one, we move on to the next screen.
let Some(next_splash_image) = splash_images.0.pop_front() else {
// There are no more splash screens, exit to the loading screen.
will-hart marked this conversation as resolved.
Show resolved Hide resolved
next_screen.set(Screen::Loading);
return;
};

let container = containers.single();
commands
.entity(container)
.despawn_descendants()
.with_children(|parent| {
parent.spawn(splash_image_bundle(next_splash_image));
});

timer.0.reset();
benfrankel marked this conversation as resolved.
Show resolved Hide resolved
}