mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
535cf401cc
Part 2 of #23619 In **Bevy 0.19** we are landing a subset of Bevy's Next Generation Scene system (often known as BSN), which now lives in the `bevy_scene` / `bevy::scene` crate. However the old `bevy_scene` system still needs to stick around for a bit longer, as it provides some features that Bevy's Next Generation Scene system doesn't (yet!): 1. It is not _yet_ possible to write a World _to_ BSN, so the old system is still necessary for "round trip World serialization". 2. The GLTF scene loader has not yet been ported to BSN, so the old system is still necessary to spawn GLTF scenes in Bevy. For this reason, we have renamed the old `bevy_scene` crate to `bevy_world_serialization`. If you were referencing `bevy_scene::*` or `bevy::scene::*` types, rename those paths to `bevy_world_serialization::*` and `bevy::world_serialization::*` respectively. Additionally, to avoid confusion / conflicts with the new scene system, all "scene" terminology / types have been reframed as "world serialization": - `Scene` -> `WorldAsset` (as this was always just a World wrapper) - `SceneRoot` -> `WorldAssetRoot` - `DynamicScene` -> `DynamicWorld` - `DynamicScene::from_scene` -> `DynamicWorld::from_world_asset` - `DynamicSceneBuilder` -> `DynamicWorldBuilder` - `DynamicSceneRoot` -> `DynamicWorldRoot` - `SceneInstanceReady` -> `WorldInstanceReady` - `SceneLoader` -> `WorldAssetLoader` - `ScenePlugin` -> `WorldSerializationPlugin` - `SceneRootTemplate` -> `WorldAssetRootTemplate` - `SceneSpawner` -> `WorldInstanceSpawner` - `SceneFilter` -> `WorldFilter` - `SceneLoaderError` -> `WorldAssetLoaderError` - `SceneSpawnError` -> `WorldInstanceSpawnError` Note that I went with `bevy_world_serialization` over `bevy_ecs_serialization`, as that is what all of the internal features described themselves as. I think it is both more specific and does a better job of making itself decoupled from `bevy_ecs` proper.
411 lines
12 KiB
Rust
411 lines
12 KiB
Rust
//! Eat the cakes. Eat them all. An example 3D game.
|
|
|
|
use std::f32::consts::PI;
|
|
|
|
use bevy::prelude::*;
|
|
|
|
use chacha20::ChaCha8Rng;
|
|
use rand::{RngExt, SeedableRng};
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, 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(OnEnter(GameState::GameOver), display_score)
|
|
.add_systems(
|
|
Update,
|
|
game_over_keyboard.run_if(in_state(GameState::GameOver)),
|
|
)
|
|
.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<WorldAsset>,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[derive(Resource, Deref, DerefMut)]
|
|
struct Random(ChaCha8Rng);
|
|
|
|
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((
|
|
Camera3d::default(),
|
|
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),
|
|
));
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMut<Game>) {
|
|
let mut rng = if std::env::var("GITHUB_ACTIONS") == Ok("true".to_string()) {
|
|
// We're seeding the PRNG here to make this example deterministic for testing purposes.
|
|
// This isn't strictly required in practical use unless you need your app to be deterministic.
|
|
ChaCha8Rng::seed_from_u64(19878367467713)
|
|
} else {
|
|
rand::make_rng()
|
|
};
|
|
|
|
// 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((
|
|
DespawnOnExit(GameState::Playing),
|
|
PointLight {
|
|
intensity: 2_000_000.0,
|
|
shadow_maps_enabled: true,
|
|
range: 30.0,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 10.0, 4.0),
|
|
));
|
|
|
|
// spawn the game board
|
|
let cell_scene =
|
|
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/tile.glb"));
|
|
game.board = (0..BOARD_SIZE_J)
|
|
.map(|j| {
|
|
(0..BOARD_SIZE_I)
|
|
.map(|i| {
|
|
let height = rng.random_range(-0.1..0.1);
|
|
commands.spawn((
|
|
DespawnOnExit(GameState::Playing),
|
|
Transform::from_xyz(i as f32, height - 0.2, j as f32),
|
|
WorldAssetRoot(cell_scene.clone()),
|
|
));
|
|
Cell { height }
|
|
})
|
|
.collect()
|
|
})
|
|
.collect();
|
|
|
|
// spawn the game character
|
|
game.player.entity = Some(
|
|
commands
|
|
.spawn((
|
|
DespawnOnExit(GameState::Playing),
|
|
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()
|
|
},
|
|
WorldAssetRoot(
|
|
asset_server
|
|
.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/alien.glb")),
|
|
),
|
|
))
|
|
.id(),
|
|
);
|
|
|
|
// load the scene for the cake
|
|
game.bonus.handle =
|
|
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/cakeBirthday.glb"));
|
|
|
|
// scoreboard
|
|
commands.spawn((
|
|
DespawnOnExit(GameState::Playing),
|
|
Text::new("Score:"),
|
|
TextFont {
|
|
font_size: FontSize::Px(33.0),
|
|
..default()
|
|
},
|
|
TextColor(Color::srgb(0.5, 0.5, 1.0)),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: px(5),
|
|
left: px(5),
|
|
..default()
|
|
},
|
|
));
|
|
|
|
commands.insert_resource(Random(rng));
|
|
}
|
|
|
|
// 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()).is_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
|
|
&& game.player.i == game.bonus.i
|
|
&& game.player.j == game.bonus.j
|
|
{
|
|
game.score += 2;
|
|
game.cake_eaten += 1;
|
|
commands.entity(entity).despawn();
|
|
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_secs();
|
|
// 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>,
|
|
mut rng: ResMut<Random>,
|
|
) {
|
|
// make sure we wait enough time before spawning the next cake
|
|
if !timer.0.tick(time.delta()).is_finished() {
|
|
return;
|
|
}
|
|
|
|
if let Some(entity) = game.bonus.entity {
|
|
game.score -= 3;
|
|
commands.entity(entity).despawn();
|
|
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 = rng.random_range(0..BOARD_SIZE_I);
|
|
game.bonus.j = rng.random_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((
|
|
DespawnOnExit(GameState::Playing),
|
|
Transform::from_xyz(
|
|
game.bonus.i as f32,
|
|
game.board[game.bonus.j][game.bonus.i].height + 0.2,
|
|
game.bonus.j as f32,
|
|
),
|
|
WorldAssetRoot(game.bonus.handle.clone()),
|
|
children![(
|
|
PointLight {
|
|
color: Color::srgb(1.0, 1.0, 0.0),
|
|
intensity: 500_000.0,
|
|
range: 10.0,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(0.0, 2.0, 0.0),
|
|
)],
|
|
))
|
|
.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
|
|
&& let Ok(mut cake_transform) = transforms.get_mut(entity)
|
|
{
|
|
cake_transform.rotate_y(time.delta_secs());
|
|
cake_transform.scale =
|
|
Vec3::splat(1.0 + (game.score as f32 / 10.0 * ops::sin(time.elapsed_secs())).abs());
|
|
}
|
|
}
|
|
|
|
// update the score displayed during the game
|
|
fn scoreboard_system(game: Res<Game>, mut display: Single<&mut Text>) {
|
|
display.0 = format!("Sugar Rush: {}", game.score);
|
|
}
|
|
|
|
// restart the game when pressing spacebar
|
|
fn game_over_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((
|
|
DespawnOnExit(GameState::GameOver),
|
|
Node {
|
|
width: percent(100),
|
|
align_items: AlignItems::Center,
|
|
justify_content: JustifyContent::Center,
|
|
..default()
|
|
},
|
|
children![(
|
|
Text::new(format!("Cake eaten: {}", game.cake_eaten)),
|
|
TextFont {
|
|
font_size: FontSize::Px(67.0),
|
|
..default()
|
|
},
|
|
TextColor(Color::srgb(0.5, 0.5, 1.0)),
|
|
)],
|
|
));
|
|
}
|