mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 20:53:53 +00:00
dd619a1087
# Objective After adding configurable exposure, we set the default ev100 value to `7` (indoor). This brought us out of sync with Blender's configuration and defaults. This PR changes the default to `9.7` (bright indoor or very overcast outdoors), as I calibrated in #11577. This feels like a very reasonable default. The other changes generally center around tweaking Bevy's lighting defaults and examples to play nicely with this number, alongside a few other tweaks and improvements. Note that for artistic reasons I have reverted some examples, which changed to directional lights in #11581, back to point lights. Fixes #11577 --- ## Changelog - Changed `Exposure::ev100` from `7` to `9.7` to better match Blender - Renamed `ExposureSettings` to `Exposure` - `Camera3dBundle` now includes `Exposure` for discoverability - Bumped `FULL_DAYLIGHT ` and `DIRECT_SUNLIGHT` to represent the middle-to-top of those ranges instead of near the bottom - Added new `AMBIENT_DAYLIGHT` constant and set that as the new `DirectionalLight` default illuminance. - `PointLight` and `SpotLight` now have a default `intensity` of 1,000,000 lumens. This makes them actually useful in the context of the new "semi-outdoor" exposure and puts them in the "cinema lighting" category instead of the "common household light" category. They are also reasonably close to the Blender default. - `AmbientLight` default has been bumped from `20` to `80`. ## Migration Guide - The increased `Exposure::ev100` means that all existing 3D lighting will need to be adjusted to match (DirectionalLights, PointLights, SpotLights, EnvironmentMapLights, etc). Or alternatively, you can adjust the `Exposure::ev100` on your cameras to work nicely with your current lighting values. If you are currently relying on default intensity values, you might need to change the intensity to achieve the same effect. Note that in Bevy 0.12, point/spot lights had a different hard coded ev100 value than directional lights. In Bevy 0.13, they use the same ev100, so if you have both in your scene, the _scale_ between these light types has changed and you will likely need to adjust one or both of them.
408 lines
12 KiB
Rust
408 lines
12 KiB
Rust
//! Eat the cakes. Eat them all. An example 3D game.
|
|
|
|
use std::f32::consts::PI;
|
|
|
|
use bevy::prelude::*;
|
|
use rand::Rng;
|
|
|
|
#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)]
|
|
enum GameState {
|
|
#[default]
|
|
Playing,
|
|
GameOver,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct BonusSpawnTimer(Timer);
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.init_resource::<Game>()
|
|
.insert_resource(BonusSpawnTimer(Timer::from_seconds(
|
|
5.0,
|
|
TimerMode::Repeating,
|
|
)))
|
|
.init_state::<GameState>()
|
|
.add_systems(Startup, setup_cameras)
|
|
.add_systems(OnEnter(GameState::Playing), setup)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
move_player,
|
|
focus_camera,
|
|
rotate_bonus,
|
|
scoreboard_system,
|
|
spawn_bonus,
|
|
)
|
|
.run_if(in_state(GameState::Playing)),
|
|
)
|
|
.add_systems(OnExit(GameState::Playing), teardown)
|
|
.add_systems(OnEnter(GameState::GameOver), display_score)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
gameover_keyboard.run_if(in_state(GameState::GameOver)),
|
|
bevy::window::close_on_esc,
|
|
),
|
|
)
|
|
.add_systems(OnExit(GameState::GameOver), teardown)
|
|
.run();
|
|
}
|
|
|
|
struct Cell {
|
|
height: f32,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Player {
|
|
entity: Option<Entity>,
|
|
i: usize,
|
|
j: usize,
|
|
move_cooldown: Timer,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Bonus {
|
|
entity: Option<Entity>,
|
|
i: usize,
|
|
j: usize,
|
|
handle: Handle<Scene>,
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
struct Game {
|
|
board: Vec<Vec<Cell>>,
|
|
player: Player,
|
|
bonus: Bonus,
|
|
score: i32,
|
|
cake_eaten: u32,
|
|
camera_should_focus: Vec3,
|
|
camera_is_focus: Vec3,
|
|
}
|
|
|
|
const BOARD_SIZE_I: usize = 14;
|
|
const BOARD_SIZE_J: usize = 21;
|
|
|
|
const RESET_FOCUS: [f32; 3] = [
|
|
BOARD_SIZE_I as f32 / 2.0,
|
|
0.0,
|
|
BOARD_SIZE_J as f32 / 2.0 - 0.5,
|
|
];
|
|
|
|
fn setup_cameras(mut commands: Commands, mut game: ResMut<Game>) {
|
|
game.camera_should_focus = Vec3::from(RESET_FOCUS);
|
|
game.camera_is_focus = game.camera_should_focus;
|
|
commands.spawn(Camera3dBundle {
|
|
transform: Transform::from_xyz(
|
|
-(BOARD_SIZE_I as f32 / 2.0),
|
|
2.0 * BOARD_SIZE_J as f32 / 3.0,
|
|
BOARD_SIZE_J as f32 / 2.0 - 0.5,
|
|
)
|
|
.looking_at(game.camera_is_focus, Vec3::Y),
|
|
..default()
|
|
});
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMut<Game>) {
|
|
// reset the game state
|
|
game.cake_eaten = 0;
|
|
game.score = 0;
|
|
game.player.i = BOARD_SIZE_I / 2;
|
|
game.player.j = BOARD_SIZE_J / 2;
|
|
game.player.move_cooldown = Timer::from_seconds(0.3, TimerMode::Once);
|
|
|
|
commands.spawn(PointLightBundle {
|
|
transform: Transform::from_xyz(4.0, 10.0, 4.0),
|
|
point_light: PointLight {
|
|
intensity: 2_000_000.0,
|
|
shadows_enabled: true,
|
|
range: 30.0,
|
|
..default()
|
|
},
|
|
..default()
|
|
});
|
|
|
|
// spawn the game board
|
|
let cell_scene = asset_server.load("models/AlienCake/tile.glb#Scene0");
|
|
game.board = (0..BOARD_SIZE_J)
|
|
.map(|j| {
|
|
(0..BOARD_SIZE_I)
|
|
.map(|i| {
|
|
let height = rand::thread_rng().gen_range(-0.1..0.1);
|
|
commands.spawn(SceneBundle {
|
|
transform: Transform::from_xyz(i as f32, height - 0.2, j as f32),
|
|
scene: cell_scene.clone(),
|
|
..default()
|
|
});
|
|
Cell { height }
|
|
})
|
|
.collect()
|
|
})
|
|
.collect();
|
|
|
|
// spawn the game character
|
|
game.player.entity = Some(
|
|
commands
|
|
.spawn(SceneBundle {
|
|
transform: Transform {
|
|
translation: Vec3::new(
|
|
game.player.i as f32,
|
|
game.board[game.player.j][game.player.i].height,
|
|
game.player.j as f32,
|
|
),
|
|
rotation: Quat::from_rotation_y(-PI / 2.),
|
|
..default()
|
|
},
|
|
scene: asset_server.load("models/AlienCake/alien.glb#Scene0"),
|
|
..default()
|
|
})
|
|
.id(),
|
|
);
|
|
|
|
// load the scene for the cake
|
|
game.bonus.handle = asset_server.load("models/AlienCake/cakeBirthday.glb#Scene0");
|
|
|
|
// scoreboard
|
|
commands.spawn(
|
|
TextBundle::from_section(
|
|
"Score:",
|
|
TextStyle {
|
|
font_size: 40.0,
|
|
color: Color::rgb(0.5, 0.5, 1.0),
|
|
..default()
|
|
},
|
|
)
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(5.0),
|
|
left: Val::Px(5.0),
|
|
..default()
|
|
}),
|
|
);
|
|
}
|
|
|
|
// remove all entities that are not a camera or window
|
|
fn teardown(mut commands: Commands, entities: Query<Entity, (Without<Camera>, Without<Window>)>) {
|
|
for entity in &entities {
|
|
commands.entity(entity).despawn();
|
|
}
|
|
}
|
|
|
|
// control the game character
|
|
fn move_player(
|
|
mut commands: Commands,
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mut game: ResMut<Game>,
|
|
mut transforms: Query<&mut Transform>,
|
|
time: Res<Time>,
|
|
) {
|
|
if game.player.move_cooldown.tick(time.delta()).finished() {
|
|
let mut moved = false;
|
|
let mut rotation = 0.0;
|
|
|
|
if keyboard_input.pressed(KeyCode::ArrowUp) {
|
|
if game.player.i < BOARD_SIZE_I - 1 {
|
|
game.player.i += 1;
|
|
}
|
|
rotation = -PI / 2.;
|
|
moved = true;
|
|
}
|
|
if keyboard_input.pressed(KeyCode::ArrowDown) {
|
|
if game.player.i > 0 {
|
|
game.player.i -= 1;
|
|
}
|
|
rotation = PI / 2.;
|
|
moved = true;
|
|
}
|
|
if keyboard_input.pressed(KeyCode::ArrowRight) {
|
|
if game.player.j < BOARD_SIZE_J - 1 {
|
|
game.player.j += 1;
|
|
}
|
|
rotation = PI;
|
|
moved = true;
|
|
}
|
|
if keyboard_input.pressed(KeyCode::ArrowLeft) {
|
|
if game.player.j > 0 {
|
|
game.player.j -= 1;
|
|
}
|
|
rotation = 0.0;
|
|
moved = true;
|
|
}
|
|
|
|
// move on the board
|
|
if moved {
|
|
game.player.move_cooldown.reset();
|
|
*transforms.get_mut(game.player.entity.unwrap()).unwrap() = Transform {
|
|
translation: Vec3::new(
|
|
game.player.i as f32,
|
|
game.board[game.player.j][game.player.i].height,
|
|
game.player.j as f32,
|
|
),
|
|
rotation: Quat::from_rotation_y(rotation),
|
|
..default()
|
|
};
|
|
}
|
|
}
|
|
|
|
// eat the cake!
|
|
if let Some(entity) = game.bonus.entity {
|
|
if game.player.i == game.bonus.i && game.player.j == game.bonus.j {
|
|
game.score += 2;
|
|
game.cake_eaten += 1;
|
|
commands.entity(entity).despawn_recursive();
|
|
game.bonus.entity = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
// change the focus of the camera
|
|
fn focus_camera(
|
|
time: Res<Time>,
|
|
mut game: ResMut<Game>,
|
|
mut transforms: ParamSet<(Query<&mut Transform, With<Camera3d>>, Query<&Transform>)>,
|
|
) {
|
|
const SPEED: f32 = 2.0;
|
|
// if there is both a player and a bonus, target the mid-point of them
|
|
if let (Some(player_entity), Some(bonus_entity)) = (game.player.entity, game.bonus.entity) {
|
|
let transform_query = transforms.p1();
|
|
if let (Ok(player_transform), Ok(bonus_transform)) = (
|
|
transform_query.get(player_entity),
|
|
transform_query.get(bonus_entity),
|
|
) {
|
|
game.camera_should_focus = player_transform
|
|
.translation
|
|
.lerp(bonus_transform.translation, 0.5);
|
|
}
|
|
// otherwise, if there is only a player, target the player
|
|
} else if let Some(player_entity) = game.player.entity {
|
|
if let Ok(player_transform) = transforms.p1().get(player_entity) {
|
|
game.camera_should_focus = player_transform.translation;
|
|
}
|
|
// otherwise, target the middle
|
|
} else {
|
|
game.camera_should_focus = Vec3::from(RESET_FOCUS);
|
|
}
|
|
// calculate the camera motion based on the difference between where the camera is looking
|
|
// and where it should be looking; the greater the distance, the faster the motion;
|
|
// smooth out the camera movement using the frame time
|
|
let mut camera_motion = game.camera_should_focus - game.camera_is_focus;
|
|
if camera_motion.length() > 0.2 {
|
|
camera_motion *= SPEED * time.delta_seconds();
|
|
// set the new camera's actual focus
|
|
game.camera_is_focus += camera_motion;
|
|
}
|
|
// look at that new camera's actual focus
|
|
for mut transform in transforms.p0().iter_mut() {
|
|
*transform = transform.looking_at(game.camera_is_focus, Vec3::Y);
|
|
}
|
|
}
|
|
|
|
// despawn the bonus if there is one, then spawn a new one at a random location
|
|
fn spawn_bonus(
|
|
time: Res<Time>,
|
|
mut timer: ResMut<BonusSpawnTimer>,
|
|
mut next_state: ResMut<NextState<GameState>>,
|
|
mut commands: Commands,
|
|
mut game: ResMut<Game>,
|
|
) {
|
|
// make sure we wait enough time before spawning the next cake
|
|
if !timer.0.tick(time.delta()).finished() {
|
|
return;
|
|
}
|
|
|
|
if let Some(entity) = game.bonus.entity {
|
|
game.score -= 3;
|
|
commands.entity(entity).despawn_recursive();
|
|
game.bonus.entity = None;
|
|
if game.score <= -5 {
|
|
next_state.set(GameState::GameOver);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// ensure bonus doesn't spawn on the player
|
|
loop {
|
|
game.bonus.i = rand::thread_rng().gen_range(0..BOARD_SIZE_I);
|
|
game.bonus.j = rand::thread_rng().gen_range(0..BOARD_SIZE_J);
|
|
if game.bonus.i != game.player.i || game.bonus.j != game.player.j {
|
|
break;
|
|
}
|
|
}
|
|
game.bonus.entity = Some(
|
|
commands
|
|
.spawn(SceneBundle {
|
|
transform: Transform::from_xyz(
|
|
game.bonus.i as f32,
|
|
game.board[game.bonus.j][game.bonus.i].height + 0.2,
|
|
game.bonus.j as f32,
|
|
),
|
|
scene: game.bonus.handle.clone(),
|
|
..default()
|
|
})
|
|
.with_children(|children| {
|
|
children.spawn(PointLightBundle {
|
|
point_light: PointLight {
|
|
color: Color::rgb(1.0, 1.0, 0.0),
|
|
intensity: 500_000.0,
|
|
range: 10.0,
|
|
..default()
|
|
},
|
|
transform: Transform::from_xyz(0.0, 2.0, 0.0),
|
|
..default()
|
|
});
|
|
})
|
|
.id(),
|
|
);
|
|
}
|
|
|
|
// let the cake turn on itself
|
|
fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Transform>) {
|
|
if let Some(entity) = game.bonus.entity {
|
|
if let Ok(mut cake_transform) = transforms.get_mut(entity) {
|
|
cake_transform.rotate_y(time.delta_seconds());
|
|
cake_transform.scale =
|
|
Vec3::splat(1.0 + (game.score as f32 / 10.0 * time.elapsed_seconds().sin()).abs());
|
|
}
|
|
}
|
|
}
|
|
|
|
// update the score displayed during the game
|
|
fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) {
|
|
let mut text = query.single_mut();
|
|
text.sections[0].value = format!("Sugar Rush: {}", game.score);
|
|
}
|
|
|
|
// restart the game when pressing spacebar
|
|
fn gameover_keyboard(
|
|
mut next_state: ResMut<NextState<GameState>>,
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
) {
|
|
if keyboard_input.just_pressed(KeyCode::Space) {
|
|
next_state.set(GameState::Playing);
|
|
}
|
|
}
|
|
|
|
// display the number of cake eaten before losing
|
|
fn display_score(mut commands: Commands, game: Res<Game>) {
|
|
commands
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(100.),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent.spawn(TextBundle::from_section(
|
|
format!("Cake eaten: {}", game.cake_eaten),
|
|
TextStyle {
|
|
font_size: 80.0,
|
|
color: Color::rgb(0.5, 0.5, 1.0),
|
|
..default()
|
|
},
|
|
));
|
|
});
|
|
}
|