mirror of
https://github.com/bevyengine/bevy
synced 2024-11-30 00:20:20 +00:00
7989cb2650
# Objective - Make `Time` API more consistent. - Support time accel/decel/pause. ## Solution This is just the `Time` half of #3002. I was told that part isn't controversial. - Give the "delta time" and "total elapsed time" methods `f32`, `f64`, and `Duration` variants with consistent naming. - Implement accelerating / decelerating the passage of time. - Implement stopping time. --- ## Changelog - Changed `time_since_startup` to `elapsed` because `time.time_*` is just silly. - Added `relative_speed` and `set_relative_speed` methods. - Added `is_paused`, `pause`, `unpause` , and methods. (I'd prefer `resume`, but `unpause` matches `Timer` API.) - Added `raw_*` variants of the "delta time" and "total elapsed time" methods. - Added `first_update` method because there's a non-zero duration between startup and the first update. ## Migration Guide - `time.time_since_startup()` -> `time.elapsed()` - `time.seconds_since_startup()` -> `time.elapsed_seconds_f64()` - `time.seconds_since_startup_wrapped_f32()` -> `time.elapsed_seconds_wrapped()` If you aren't sure which to use, most systems should continue to use "scaled" time (e.g. `time.delta_seconds()`). The realtime "unscaled" time measurements (e.g. `time.raw_delta_seconds()`) are mostly for debugging and profiling.
400 lines
13 KiB
Rust
400 lines
13 KiB
Rust
//! Eat the cakes. Eat them all. An example 3D game.
|
|
|
|
use std::f32::consts::PI;
|
|
|
|
use bevy::{ecs::schedule::SystemSet, prelude::*};
|
|
use rand::Rng;
|
|
|
|
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
|
|
enum GameState {
|
|
Playing,
|
|
GameOver,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct BonusSpawnTimer(Timer);
|
|
|
|
fn main() {
|
|
App::new()
|
|
.init_resource::<Game>()
|
|
.insert_resource(BonusSpawnTimer(Timer::from_seconds(
|
|
5.0,
|
|
TimerMode::Repeating,
|
|
)))
|
|
.add_plugins(DefaultPlugins)
|
|
.add_state(GameState::Playing)
|
|
.add_startup_system(setup_cameras)
|
|
.add_system_set(SystemSet::on_enter(GameState::Playing).with_system(setup))
|
|
.add_system_set(
|
|
SystemSet::on_update(GameState::Playing)
|
|
.with_system(move_player)
|
|
.with_system(focus_camera)
|
|
.with_system(rotate_bonus)
|
|
.with_system(scoreboard_system)
|
|
.with_system(spawn_bonus),
|
|
)
|
|
.add_system_set(SystemSet::on_exit(GameState::Playing).with_system(teardown))
|
|
.add_system_set(SystemSet::on_enter(GameState::GameOver).with_system(display_score))
|
|
.add_system_set(SystemSet::on_update(GameState::GameOver).with_system(gameover_keyboard))
|
|
.add_system_set(SystemSet::on_exit(GameState::GameOver).with_system(teardown))
|
|
.add_system(bevy::window::close_on_esc)
|
|
.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: 3000.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: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
|
font_size: 40.0,
|
|
color: Color::rgb(0.5, 0.5, 1.0),
|
|
},
|
|
)
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
position: UiRect {
|
|
top: Val::Px(5.0),
|
|
left: Val::Px(5.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
}),
|
|
);
|
|
}
|
|
|
|
// remove all entities that are not a camera
|
|
fn teardown(mut commands: Commands, entities: Query<Entity, Without<Camera>>) {
|
|
for entity in &entities {
|
|
commands.entity(entity).despawn_recursive();
|
|
}
|
|
}
|
|
|
|
// control the game character
|
|
fn move_player(
|
|
mut commands: Commands,
|
|
keyboard_input: Res<Input<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::Up) {
|
|
if game.player.i < BOARD_SIZE_I - 1 {
|
|
game.player.i += 1;
|
|
}
|
|
rotation = -PI / 2.;
|
|
moved = true;
|
|
}
|
|
if keyboard_input.pressed(KeyCode::Down) {
|
|
if game.player.i > 0 {
|
|
game.player.i -= 1;
|
|
}
|
|
rotation = PI / 2.;
|
|
moved = true;
|
|
}
|
|
if keyboard_input.pressed(KeyCode::Right) {
|
|
if game.player.j < BOARD_SIZE_J - 1 {
|
|
game.player.j += 1;
|
|
}
|
|
rotation = PI;
|
|
moved = true;
|
|
}
|
|
if keyboard_input.pressed(KeyCode::Left) {
|
|
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 state: ResMut<State<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 {
|
|
// We don't particularly care if this operation fails
|
|
let _ = state.overwrite_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: 1000.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 state: ResMut<State<GameState>>, keyboard_input: Res<Input<KeyCode>>) {
|
|
if keyboard_input.just_pressed(KeyCode::Space) {
|
|
state.set(GameState::Playing).unwrap();
|
|
}
|
|
}
|
|
|
|
// display the number of cake eaten before losing
|
|
fn display_score(mut commands: Commands, asset_server: Res<AssetServer>, game: Res<Game>) {
|
|
commands
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
margin: UiRect::all(Val::Auto),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent.spawn(TextBundle::from_section(
|
|
format!("Cake eaten: {}", game.cake_eaten),
|
|
TextStyle {
|
|
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
|
font_size: 80.0,
|
|
color: Color::rgb(0.5, 0.5, 1.0),
|
|
},
|
|
));
|
|
});
|
|
}
|