Path: blob/main/examples/showcase/alien_cake_addict.rs
30632 views
//! Eat the cakes. Eat them all. An example 3D game.12use std::f32::consts::PI;34use bevy::prelude::*;56use chacha20::ChaCha8Rng;7use rand::{RngExt, SeedableRng};89#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]10enum GameState {11#[default]12Playing,13GameOver,14}1516#[derive(Resource)]17struct BonusSpawnTimer(Timer);1819fn main() {20App::new()21.add_plugins(DefaultPlugins)22.init_resource::<Game>()23.insert_resource(BonusSpawnTimer(Timer::from_seconds(245.0,25TimerMode::Repeating,26)))27.init_state::<GameState>()28.add_systems(Startup, setup_cameras)29.add_systems(OnEnter(GameState::Playing), setup)30.add_systems(31Update,32(33move_player,34focus_camera,35rotate_bonus,36scoreboard_system,37spawn_bonus,38)39.run_if(in_state(GameState::Playing)),40)41.add_systems(OnEnter(GameState::GameOver), display_score)42.add_systems(43Update,44game_over_keyboard.run_if(in_state(GameState::GameOver)),45)46.run();47}4849struct Cell {50height: f32,51}5253#[derive(Default)]54struct Player {55entity: Option<Entity>,56i: usize,57j: usize,58move_cooldown: Timer,59}6061#[derive(Default)]62struct Bonus {63entity: Option<Entity>,64i: usize,65j: usize,66handle: Handle<WorldAsset>,67}6869#[derive(Resource, Default)]70struct Game {71board: Vec<Vec<Cell>>,72player: Player,73bonus: Bonus,74score: i32,75cake_eaten: u32,76camera_should_focus: Vec3,77camera_is_focus: Vec3,78}7980#[derive(Resource, Deref, DerefMut)]81struct Random(ChaCha8Rng);8283const BOARD_SIZE_I: usize = 14;84const BOARD_SIZE_J: usize = 21;8586const RESET_FOCUS: [f32; 3] = [87BOARD_SIZE_I as f32 / 2.0,880.0,89BOARD_SIZE_J as f32 / 2.0 - 0.5,90];9192fn setup_cameras(mut commands: Commands, mut game: ResMut<Game>) {93game.camera_should_focus = Vec3::from(RESET_FOCUS);94game.camera_is_focus = game.camera_should_focus;95commands.spawn((96Camera3d::default(),97Transform::from_xyz(98-(BOARD_SIZE_I as f32 / 2.0),992.0 * BOARD_SIZE_J as f32 / 3.0,100BOARD_SIZE_J as f32 / 2.0 - 0.5,101)102.looking_at(game.camera_is_focus, Vec3::Y),103));104}105106fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMut<Game>) {107let mut rng = if std::env::var("GITHUB_ACTIONS") == Ok("true".to_string()) {108// We're seeding the PRNG here to make this example deterministic for testing purposes.109// This isn't strictly required in practical use unless you need your app to be deterministic.110ChaCha8Rng::seed_from_u64(19878367467713)111} else {112rand::make_rng()113};114115// reset the game state116game.cake_eaten = 0;117game.score = 0;118game.player.i = BOARD_SIZE_I / 2;119game.player.j = BOARD_SIZE_J / 2;120game.player.move_cooldown = Timer::from_seconds(0.3, TimerMode::Once);121122commands.spawn((123DespawnOnExit(GameState::Playing),124PointLight {125intensity: 2_000_000.0,126shadow_maps_enabled: true,127range: 30.0,128..default()129},130Transform::from_xyz(4.0, 10.0, 4.0),131));132133// spawn the game board134let cell_scene =135asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/tile.glb"));136game.board = (0..BOARD_SIZE_J)137.map(|j| {138(0..BOARD_SIZE_I)139.map(|i| {140let height = rng.random_range(-0.1..0.1);141commands.spawn((142DespawnOnExit(GameState::Playing),143Transform::from_xyz(i as f32, height - 0.2, j as f32),144WorldAssetRoot(cell_scene.clone()),145));146Cell { height }147})148.collect()149})150.collect();151152// spawn the game character153game.player.entity = Some(154commands155.spawn((156DespawnOnExit(GameState::Playing),157Transform {158translation: Vec3::new(159game.player.i as f32,160game.board[game.player.j][game.player.i].height,161game.player.j as f32,162),163rotation: Quat::from_rotation_y(-PI / 2.),164..default()165},166WorldAssetRoot(167asset_server168.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/alien.glb")),169),170))171.id(),172);173174// load the scene for the cake175game.bonus.handle =176asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/cakeBirthday.glb"));177178// scoreboard179commands.spawn((180DespawnOnExit(GameState::Playing),181Text::new("Score:"),182TextFont {183font_size: FontSize::Px(33.0),184..default()185},186TextColor(Color::srgb(0.5, 0.5, 1.0)),187Node {188position_type: PositionType::Absolute,189top: px(5),190left: px(5),191..default()192},193));194195commands.insert_resource(Random(rng));196}197198// control the game character199fn move_player(200mut commands: Commands,201keyboard_input: Res<ButtonInput<KeyCode>>,202mut game: ResMut<Game>,203mut transforms: Query<&mut Transform>,204time: Res<Time>,205) {206if game.player.move_cooldown.tick(time.delta()).is_finished() {207let mut moved = false;208let mut rotation = 0.0;209210if keyboard_input.pressed(KeyCode::ArrowUp) {211if game.player.i < BOARD_SIZE_I - 1 {212game.player.i += 1;213}214rotation = -PI / 2.;215moved = true;216}217if keyboard_input.pressed(KeyCode::ArrowDown) {218if game.player.i > 0 {219game.player.i -= 1;220}221rotation = PI / 2.;222moved = true;223}224if keyboard_input.pressed(KeyCode::ArrowRight) {225if game.player.j < BOARD_SIZE_J - 1 {226game.player.j += 1;227}228rotation = PI;229moved = true;230}231if keyboard_input.pressed(KeyCode::ArrowLeft) {232if game.player.j > 0 {233game.player.j -= 1;234}235rotation = 0.0;236moved = true;237}238239// move on the board240if moved {241game.player.move_cooldown.reset();242*transforms.get_mut(game.player.entity.unwrap()).unwrap() = Transform {243translation: Vec3::new(244game.player.i as f32,245game.board[game.player.j][game.player.i].height,246game.player.j as f32,247),248rotation: Quat::from_rotation_y(rotation),249..default()250};251}252}253254// eat the cake!255if let Some(entity) = game.bonus.entity256&& game.player.i == game.bonus.i257&& game.player.j == game.bonus.j258{259game.score += 2;260game.cake_eaten += 1;261commands.entity(entity).despawn();262game.bonus.entity = None;263}264}265266// change the focus of the camera267fn focus_camera(268time: Res<Time>,269mut game: ResMut<Game>,270mut transforms: ParamSet<(Query<&mut Transform, With<Camera3d>>, Query<&Transform>)>,271) {272const SPEED: f32 = 2.0;273// if there is both a player and a bonus, target the mid-point of them274if let (Some(player_entity), Some(bonus_entity)) = (game.player.entity, game.bonus.entity) {275let transform_query = transforms.p1();276if let (Ok(player_transform), Ok(bonus_transform)) = (277transform_query.get(player_entity),278transform_query.get(bonus_entity),279) {280game.camera_should_focus = player_transform281.translation282.lerp(bonus_transform.translation, 0.5);283}284// otherwise, if there is only a player, target the player285} else if let Some(player_entity) = game.player.entity {286if let Ok(player_transform) = transforms.p1().get(player_entity) {287game.camera_should_focus = player_transform.translation;288}289// otherwise, target the middle290} else {291game.camera_should_focus = Vec3::from(RESET_FOCUS);292}293// calculate the camera motion based on the difference between where the camera is looking294// and where it should be looking; the greater the distance, the faster the motion;295// smooth out the camera movement using the frame time296let mut camera_motion = game.camera_should_focus - game.camera_is_focus;297if camera_motion.length() > 0.2 {298camera_motion *= SPEED * time.delta_secs();299// set the new camera's actual focus300game.camera_is_focus += camera_motion;301}302// look at that new camera's actual focus303for mut transform in transforms.p0().iter_mut() {304*transform = transform.looking_at(game.camera_is_focus, Vec3::Y);305}306}307308// despawn the bonus if there is one, then spawn a new one at a random location309fn spawn_bonus(310time: Res<Time>,311mut timer: ResMut<BonusSpawnTimer>,312mut next_state: ResMut<NextState<GameState>>,313mut commands: Commands,314mut game: ResMut<Game>,315mut rng: ResMut<Random>,316) {317// make sure we wait enough time before spawning the next cake318if !timer.0.tick(time.delta()).is_finished() {319return;320}321322if let Some(entity) = game.bonus.entity {323game.score -= 3;324commands.entity(entity).despawn();325game.bonus.entity = None;326if game.score <= -5 {327next_state.set(GameState::GameOver);328return;329}330}331332// ensure bonus doesn't spawn on the player333loop {334game.bonus.i = rng.random_range(0..BOARD_SIZE_I);335game.bonus.j = rng.random_range(0..BOARD_SIZE_J);336if game.bonus.i != game.player.i || game.bonus.j != game.player.j {337break;338}339}340game.bonus.entity = Some(341commands342.spawn((343DespawnOnExit(GameState::Playing),344Transform::from_xyz(345game.bonus.i as f32,346game.board[game.bonus.j][game.bonus.i].height + 0.2,347game.bonus.j as f32,348),349WorldAssetRoot(game.bonus.handle.clone()),350children![(351PointLight {352color: Color::srgb(1.0, 1.0, 0.0),353intensity: 500_000.0,354range: 10.0,355..default()356},357Transform::from_xyz(0.0, 2.0, 0.0),358)],359))360.id(),361);362}363364// let the cake turn on itself365fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Transform>) {366if let Some(entity) = game.bonus.entity367&& let Ok(mut cake_transform) = transforms.get_mut(entity)368{369cake_transform.rotate_y(time.delta_secs());370cake_transform.scale =371Vec3::splat(1.0 + (game.score as f32 / 10.0 * ops::sin(time.elapsed_secs())).abs());372}373}374375// update the score displayed during the game376fn scoreboard_system(game: Res<Game>, mut display: Single<&mut Text>) {377display.0 = format!("Sugar Rush: {}", game.score);378}379380// restart the game when pressing spacebar381fn game_over_keyboard(382mut next_state: ResMut<NextState<GameState>>,383keyboard_input: Res<ButtonInput<KeyCode>>,384) {385if keyboard_input.just_pressed(KeyCode::Space) {386next_state.set(GameState::Playing);387}388}389390// display the number of cake eaten before losing391fn display_score(mut commands: Commands, game: Res<Game>) {392commands.spawn((393DespawnOnExit(GameState::GameOver),394Node {395width: percent(100),396align_items: AlignItems::Center,397justify_content: JustifyContent::Center,398..default()399},400children![(401Text::new(format!("Cake eaten: {}", game.cake_eaten)),402TextFont {403font_size: FontSize::Px(67.0),404..default()405},406TextColor(Color::srgb(0.5, 0.5, 1.0)),407)],408));409}410411412