From 653910ab781f4f8a729fc7c6d613643ddd58db83 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Tue, 1 Jul 2025 15:21:57 -0600 Subject: [PATCH 01/14] upgrade to bevy 0.13.2 --- Cargo.toml | 4 +- examples/collider.rs | 29 +-- examples/collision.rs | 2 +- examples/keyboard_events.rs | 20 +- examples/keyboard_state.rs | 16 +- examples/level_creator.rs | 69 +++-- examples/scenarios/extreme_drivers_ed.rs | 8 +- examples/scenarios/road_race.rs | 4 +- src/audio.rs | 19 +- src/game.rs | 2 +- src/keyboard.rs | 313 +++++++++++++---------- src/mouse.rs | 2 +- 12 files changed, 263 insertions(+), 225 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c44bd6b..49a0b73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ exclude = [ ] [dependencies] -bevy = { version = "0.12.1", default-features = false, features = [ +bevy = { version = "0.13.2", default-features = false, features = [ "bevy_audio", "bevy_gilrs", "bevy_gltf", @@ -34,7 +34,7 @@ bevy = { version = "0.12.1", default-features = false, features = [ "x11", "vorbis", ] } -bevy_prototype_lyon = "0.10.0" +bevy_prototype_lyon = "0.11.0" ron = "0.8" serde = { version = "1.0", features = [ "derive" ] } diff --git a/examples/collider.rs b/examples/collider.rs index c0db2c3..941ba0c 100644 --- a/examples/collider.rs +++ b/examples/collider.rs @@ -106,31 +106,31 @@ fn main() { fn game_logic(engine: &mut Engine, game_state: &mut GameState) { // Zoom levels - if engine.keyboard_state.just_pressed(KeyCode::Key1) { + if engine.keyboard_state.just_pressed(KeyCode::Digit1) { game_state.scale = 1.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key2) { + if engine.keyboard_state.just_pressed(KeyCode::Digit2) { game_state.scale = 2.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key3) { + if engine.keyboard_state.just_pressed(KeyCode::Digit3) { game_state.scale = 3.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key4) { + if engine.keyboard_state.just_pressed(KeyCode::Digit4) { game_state.scale = 4.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key5) { + if engine.keyboard_state.just_pressed(KeyCode::Digit5) { game_state.scale = 5.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key6) { + if engine.keyboard_state.just_pressed(KeyCode::Digit6) { game_state.scale = 6.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key7) { + if engine.keyboard_state.just_pressed(KeyCode::Digit7) { game_state.scale = 7.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key8) { + if engine.keyboard_state.just_pressed(KeyCode::Digit8) { game_state.scale = 8.0; } - if engine.keyboard_state.just_pressed(KeyCode::Key9) { + if engine.keyboard_state.just_pressed(KeyCode::Digit9) { game_state.scale = 9.0; } // Update scale @@ -143,7 +143,7 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { } // Delete collider if engine.keyboard_state.just_pressed(KeyCode::Delete) - || engine.keyboard_state.just_pressed(KeyCode::Back) + || engine.keyboard_state.just_pressed(KeyCode::Backspace) { sprite.collider = Collider::NoCollider; sprite.collider_dirty = true; @@ -165,7 +165,7 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { // Generate a circle collider if engine .keyboard_state - .just_pressed_any(&[KeyCode::Plus, KeyCode::Equals, KeyCode::NumpadAdd]) + .just_pressed_any(&[KeyCode::Equal, KeyCode::NumpadAdd]) { game_state.circle_radius += 0.5; } @@ -176,12 +176,11 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { game_state.circle_radius -= 0.5; } if engine.keyboard_state.just_pressed_any(&[ - KeyCode::Plus, - KeyCode::Equals, + KeyCode::Equal, KeyCode::NumpadAdd, KeyCode::Minus, KeyCode::NumpadSubtract, - KeyCode::C, + KeyCode::KeyC, ]) { sprite.collider = Collider::circle(game_state.circle_radius); sprite.collider_dirty = true; @@ -200,7 +199,7 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { } } // Write the collider file - if engine.keyboard_state.just_pressed(KeyCode::W) { + if engine.keyboard_state.just_pressed(KeyCode::KeyW) { if sprite.write_collider() { println!( "Successfully wrote the new collider file: {}", diff --git a/examples/collision.rs b/examples/collision.rs index d9c65a3..af44d44 100644 --- a/examples/collision.rs +++ b/examples/collision.rs @@ -84,7 +84,7 @@ fn logic(engine: &mut Engine, _: &mut GameState) { } // Pressing C toggles sprite collider debug lines - if engine.keyboard_state.just_pressed(KeyCode::C) { + if engine.keyboard_state.just_pressed(KeyCode::KeyC) { engine.show_colliders = !engine.show_colliders; } } diff --git a/examples/keyboard_events.rs b/examples/keyboard_events.rs index 00af312..d389079 100644 --- a/examples/keyboard_events.rs +++ b/examples/keyboard_events.rs @@ -30,22 +30,22 @@ fn logic(engine: &mut Engine, _: &mut GameState) { // Loop through any keyboard input that hasn't been processed this frame for keyboard_event in &engine.keyboard_events { if let KeyboardInput { - scan_code: _, - key_code: Some(key_code), + key_code, state: ButtonState::Pressed, window: _, + logical_key: _, } = keyboard_event { // Handle various keypresses. The extra keys are for the Dvorak keyboard layout. ;-) match key_code { - KeyCode::A | KeyCode::Left => race_car.translation.x -= 10.0, - KeyCode::D | KeyCode::Right | KeyCode::E => race_car.translation.x += 10.0, - KeyCode::O | KeyCode::Down | KeyCode::S => race_car.translation.y -= 10.0, - KeyCode::W | KeyCode::Up | KeyCode::Comma => race_car.translation.y += 10.0, - KeyCode::Z | KeyCode::Semicolon => race_car.rotation += std::f32::consts::FRAC_PI_4, - KeyCode::C | KeyCode::J => race_car.rotation -= std::f32::consts::FRAC_PI_4, - KeyCode::Plus | KeyCode::Equals => race_car.scale *= 1.1, - KeyCode::Minus | KeyCode::Underline => race_car.scale *= 0.9, + KeyCode::KeyA | KeyCode::ArrowLeft => race_car.translation.x -= 10.0, + KeyCode::KeyD | KeyCode::ArrowRight => race_car.translation.x += 10.0, + KeyCode::KeyO | KeyCode::ArrowDown => race_car.translation.y -= 10.0, + KeyCode::KeyW | KeyCode::ArrowUp => race_car.translation.y += 10.0, + KeyCode::KeyZ => race_car.rotation += std::f32::consts::FRAC_PI_4, + KeyCode::KeyC => race_car.rotation -= std::f32::consts::FRAC_PI_4, + KeyCode::Equal => race_car.scale *= 1.1, + KeyCode::Minus => race_car.scale *= 0.9, _ => {} } diff --git a/examples/keyboard_state.rs b/examples/keyboard_state.rs index e01d510..939dc85 100644 --- a/examples/keyboard_state.rs +++ b/examples/keyboard_state.rs @@ -36,32 +36,32 @@ fn logic(engine: &mut Engine, _: &mut GameState) { // Handle keyboard input let ks = &mut engine.keyboard_state; - if ks.pressed_any(&[KeyCode::W, KeyCode::Up, KeyCode::Comma]) { + if ks.pressed_any(&[KeyCode::KeyW, KeyCode::ArrowUp]) { race_car.translation.y += move_amount; } - if ks.pressed_any(&[KeyCode::A, KeyCode::Left]) { + if ks.pressed_any(&[KeyCode::KeyA, KeyCode::ArrowLeft]) { race_car.translation.x -= move_amount; } - if ks.pressed_any(&[KeyCode::S, KeyCode::Down, KeyCode::O]) { + if ks.pressed_any(&[KeyCode::KeyS, KeyCode::ArrowDown]) { race_car.translation.y -= move_amount; } - if ks.pressed_any(&[KeyCode::D, KeyCode::Right, KeyCode::E]) { + if ks.pressed_any(&[KeyCode::KeyD, KeyCode::ArrowRight]) { race_car.translation.x += move_amount; } // If you prefer a more functional style that is equivalent to the kind of logic above, // but takes closures to run if the buttons are pressed, you can call `.chain()` ks.chain() - .pressed_any(&[KeyCode::Z, KeyCode::Semicolon], |_| { + .pressed_any(&[KeyCode::KeyZ], |_| { race_car.rotation += rotation_amount; }) - .pressed_any(&[KeyCode::C, KeyCode::J], |_| { + .pressed_any(&[KeyCode::KeyC], |_| { race_car.rotation -= rotation_amount; }) - .pressed_any(&[KeyCode::Plus, KeyCode::Equals], |_| { + .pressed_any(&[KeyCode::Equal], |_| { race_car.scale *= 1.0 + scale_amount; }) - .pressed_any(&[KeyCode::Minus, KeyCode::Underline], |_| { + .pressed_any(&[KeyCode::Minus], |_| { race_car.scale *= 1.0 - scale_amount; }); diff --git a/examples/level_creator.rs b/examples/level_creator.rs index d1f30e1..5465571 100644 --- a/examples/level_creator.rs +++ b/examples/level_creator.rs @@ -95,45 +95,44 @@ fn logic(engine: &mut Engine, game_state: &mut GameState) { let mut prev_preset = false; let mut next_preset = false; for keyboard_event in &engine.keyboard_events { - if let KeyboardInput { - scan_code: _, - key_code: Some(key_code), + let KeyboardInput { + key_code, state, window: _, - } = keyboard_event - { - if *state == ButtonState::Pressed { - match key_code { - KeyCode::Z | KeyCode::Semicolon => { - print_level = true; - } - KeyCode::ShiftLeft | KeyCode::ShiftRight => { - game_state.shift_pressed = true; - } - KeyCode::R | KeyCode::P => { - reset = true; - } - KeyCode::S | KeyCode::O => { - print_status = true; - } - KeyCode::Space | KeyCode::Back => { - place_sprite = true; - } - KeyCode::Left | KeyCode::Up => { - prev_preset = true; - } - KeyCode::Right | KeyCode::Down => { - next_preset = true; - } - _ => {} + logical_key: _, + } = keyboard_event; + + if *state == ButtonState::Pressed { + match key_code { + KeyCode::KeyZ => { + print_level = true; } - } else { - match key_code { - KeyCode::ShiftLeft | KeyCode::ShiftRight => { - game_state.shift_pressed = false; - } - _ => {} + KeyCode::ShiftLeft | KeyCode::ShiftRight => { + game_state.shift_pressed = true; + } + KeyCode::KeyR => { + reset = true; + } + KeyCode::KeyS => { + print_status = true; + } + KeyCode::Space | KeyCode::Backspace => { + place_sprite = true; + } + KeyCode::ArrowLeft | KeyCode::ArrowUp => { + prev_preset = true; } + KeyCode::ArrowRight | KeyCode::ArrowDown => { + next_preset = true; + } + _ => {} + } + } else { + match key_code { + KeyCode::ShiftLeft | KeyCode::ShiftRight => { + game_state.shift_pressed = false; + } + _ => {} } } } diff --git a/examples/scenarios/extreme_drivers_ed.rs b/examples/scenarios/extreme_drivers_ed.rs index d2f8ace..2e738cd 100644 --- a/examples/scenarios/extreme_drivers_ed.rs +++ b/examples/scenarios/extreme_drivers_ed.rs @@ -861,17 +861,17 @@ fn logic(engine: &mut Engine, game_state: &mut GameState) { { use KeyCode::*; // Acceleration input - if engine.keyboard_state.pressed_any(&[W, Up, Comma]) { + if engine.keyboard_state.pressed_any(&[KeyW, ArrowUp, Comma]) { acceleration += 1.0; } - if engine.keyboard_state.pressed_any(&[S, Down, O]) { + if engine.keyboard_state.pressed_any(&[KeyS, ArrowDown]) { acceleration -= 1.0; } // Rotation/Turning input - if engine.keyboard_state.pressed_any(&[A, Left]) { + if engine.keyboard_state.pressed_any(&[KeyA, ArrowLeft]) { rotation += 1.0; } - if engine.keyboard_state.pressed_any(&[D, Right, E]) { + if engine.keyboard_state.pressed_any(&[KeyD, ArrowRight]) { rotation -= 1.0; } } diff --git a/examples/scenarios/road_race.rs b/examples/scenarios/road_race.rs index 57f330b..b10d7eb 100644 --- a/examples/scenarios/road_race.rs +++ b/examples/scenarios/road_race.rs @@ -75,13 +75,13 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { let mut direction = 0.0; if engine .keyboard_state - .pressed_any(&[KeyCode::Up, KeyCode::W, KeyCode::Comma]) + .pressed_any(&[KeyCode::ArrowUp, KeyCode::KeyW]) { direction += 1.0; } if engine .keyboard_state - .pressed_any(&[KeyCode::Down, KeyCode::S, KeyCode::O]) + .pressed_any(&[KeyCode::ArrowDown, KeyCode::KeyS]) { direction -= 1.0; } diff --git a/src/audio.rs b/src/audio.rs index a47fde3..503915d 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -8,11 +8,14 @@ //! ```rust,no_run //! # use rusty_engine::prelude::*; //! # +//! # #[derive(Resource)] +//! # struct GameState; +//! # //! # fn main() { //! # let mut game = Game::new(); //! // Inside your logic function... //! game.audio_manager.play_sfx("my_sound_effect.mp3", 1.0); -//! # game.run(()); +//! # game.run(GameState); //! # } //! ``` //! @@ -22,11 +25,14 @@ //! ```rust,no_run //! # use rusty_engine::prelude::*; //! # +//! # #[derive(Resource)] +//! # struct GameState; +//! # //! # fn main() { //! # let mut game = Game::new(); //! // Inside your logic function... //! game.audio_manager.play_music("my_game/spooky_loop.ogg", 1.0); -//! # game.run(()); +//! # game.run(GameState); //! # } //! ``` //! @@ -37,12 +43,15 @@ //! // Import the enums into scope first //! use rusty_engine::prelude::*; //! +//! # #[derive(Resource)] +//! # struct GameState; +//! # //! # fn main() { //! # let mut game = Game::new(); //! // Inside your logic function... //! game.audio_manager.play_sfx(SfxPreset::Confirmation1, 1.0); //! game.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0); -//! # game.run(()); +//! # game.run(GameState); //! # } //! ``` //! @@ -246,7 +255,7 @@ pub fn queue_managed_audio_system( source: asset_server.load(format!("audio/{}", sfx)), settings: PlaybackSettings { mode: PlaybackMode::Despawn, - volume: Volume::new_relative(volume), + volume: Volume::new(volume), ..Default::default() }, }); @@ -264,7 +273,7 @@ pub fn queue_managed_audio_system( .spawn(AudioBundle { source: asset_server.load(format!("audio/{}", music)), settings: PlaybackSettings { - volume: Volume::new_relative(volume), + volume: Volume::new(volume), mode: PlaybackMode::Loop, ..Default::default() }, diff --git a/src/game.rs b/src/game.rs index 7728f95..941edfc 100644 --- a/src/game.rs +++ b/src/game.rs @@ -225,7 +225,7 @@ pub fn add_texts(commands: &mut Commands, asset_server: &Res, engin color: Color::WHITE, }, ) - .with_alignment(TextAlignment::Center), + .with_justify(JustifyText::Center), transform, ..Default::default() }, diff --git a/src/keyboard.rs b/src/keyboard.rs index 4b37328..2991d86 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -25,7 +25,7 @@ fn sync_keyboard_events( // Populate this frame's events for event in keyboard_input_events.read() { - engine.keyboard_events.push(*event); + engine.keyboard_events.push(event.clone()); } } @@ -136,7 +136,7 @@ impl KeyboardState { /// store bevy's keyboard state for our own use fn sync_keyboard_state( - keyboard_input: Res>, + keyboard_input: Res>, mut keyboard_state: ResMut, ) { keyboard_state.last_frame = keyboard_state.this_frame.clone(); @@ -151,44 +151,166 @@ fn sync_keyboard_state( } use KeyCode::*; -const KEYCODEVARIANTS: [KeyCode; 163] = [ - Key1, - Key2, - Key3, - Key4, - Key5, - Key6, - Key7, - Key8, - Key9, - Key0, - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, +const KEYCODEVARIANTS: [KeyCode; 194] = [ + Backquote, + Backslash, + BracketLeft, + BracketRight, + Comma, + Digit0, + Digit1, + Digit2, + Digit3, + Digit4, + Digit5, + Digit6, + Digit7, + Digit8, + Digit9, + Equal, + IntlBackslash, + IntlRo, + IntlYen, + KeyA, + KeyB, + KeyC, + KeyD, + KeyE, + KeyF, + KeyG, + KeyH, + KeyI, + KeyJ, + KeyK, + KeyL, + KeyM, + KeyN, + KeyO, + KeyP, + KeyQ, + KeyR, + KeyS, + KeyT, + KeyU, + KeyV, + KeyW, + KeyX, + KeyY, + KeyZ, + Minus, + Period, + Quote, + Semicolon, + Slash, + AltLeft, + AltRight, + Backspace, + CapsLock, + ContextMenu, + ControlLeft, + ControlRight, + Enter, + SuperLeft, + SuperRight, + ShiftLeft, + ShiftRight, + Space, + Tab, + Convert, + KanaMode, + Lang1, + Lang2, + Lang3, + Lang4, + Lang5, + NonConvert, + Delete, + End, + Help, + Home, + Insert, + PageDown, + PageUp, + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + NumLock, + Numpad0, + Numpad1, + Numpad2, + Numpad3, + Numpad4, + Numpad5, + Numpad6, + Numpad7, + Numpad8, + Numpad9, + NumpadAdd, + NumpadBackspace, + NumpadClear, + NumpadClearEntry, + NumpadComma, + NumpadDecimal, + NumpadDivide, + NumpadEnter, + NumpadEqual, + NumpadHash, + NumpadMemoryAdd, + NumpadMemoryClear, + NumpadMemoryRecall, + NumpadMemoryStore, + NumpadMemorySubtract, + NumpadMultiply, + NumpadParenLeft, + NumpadParenRight, + NumpadStar, + NumpadSubtract, Escape, + Fn, + FnLock, + PrintScreen, + ScrollLock, + Pause, + BrowserBack, + BrowserFavorites, + BrowserForward, + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + Eject, + LaunchApp1, + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + Meta, + Hyper, + Turbo, + Abort, + Resume, + Suspend, + Again, + Copy, + Cut, + Find, + Open, + Paste, + Props, + Select, + Undo, + Hiragana, + Katakana, F1, F2, F3, @@ -213,106 +335,15 @@ const KEYCODEVARIANTS: [KeyCode; 163] = [ F22, F23, F24, - Snapshot, - Scroll, - Pause, - Insert, - Home, - Delete, - End, - PageDown, - PageUp, - Left, - Up, - Right, - Down, - Back, - Return, - Space, - Compose, - Caret, - Numlock, - Numpad0, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - AbntC1, - AbntC2, - NumpadAdd, - Apostrophe, - Apps, - Asterisk, - Plus, - At, - Ax, - Backslash, - Calculator, - Capital, - Colon, - Comma, - Convert, - NumpadDecimal, - NumpadDivide, - Equals, - Grave, - Kana, - Kanji, - AltLeft, - BracketLeft, - ControlLeft, - ShiftLeft, - SuperLeft, - Mail, - MediaSelect, - MediaStop, - Minus, - NumpadMultiply, - Mute, - MyComputer, - NavigateForward, - NavigateBackward, - NextTrack, - NoConvert, - NumpadComma, - NumpadEnter, - NumpadEquals, - Oem102, - Period, - PlayPause, - Power, - PrevTrack, - AltRight, - BracketRight, - ControlRight, - ShiftRight, - SuperRight, - Semicolon, - Slash, - Sleep, - Stop, - NumpadSubtract, - Sysrq, - Tab, - Underline, - Unlabeled, - VolumeDown, - VolumeUp, - Wake, - WebBack, - WebFavorites, - WebForward, - WebHome, - WebRefresh, - WebSearch, - WebStop, - Yen, - Copy, - Paste, - Cut, + F25, + F26, + F27, + F28, + F29, + F30, + F31, + F32, + F33, + F34, + F35, ]; diff --git a/src/mouse.rs b/src/mouse.rs index 614b395..0dbd666 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -214,7 +214,7 @@ fn sync_mouse_events( /// Gather the mouse state from bevy and store it for our own use fn sync_mouse_state( - mouse_button_input: Res>, + mouse_button_input: Res>, mut mouse_state: ResMut, mut mouse_motion_events: EventReader, mut cursor_moved_events: EventReader, From 60efeef27d94ba51fa823002fef3f2d58178ab3f Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Mon, 14 Jul 2025 20:43:49 -0600 Subject: [PATCH 02/14] bevy 0.14.2 and doc updates --- Cargo.toml | 4 ++-- examples/keyboard_events.rs | 10 +++++----- examples/keyboard_state.rs | 10 +++++----- examples/scenarios/extreme_drivers_ed.rs | 4 ++-- src/game.rs | 24 ++++++++++++++++++++---- tutorial/src/02-quick-start.md | 2 ++ tutorial/src/105-keyboard-state.md | 8 ++++---- tutorial/src/110-keyboard-events.md | 13 +++++++------ tutorial/src/115-mouse-state.md | 2 +- tutorial/src/120-mouse-events.md | 2 +- tutorial/src/205-music.md | 2 +- tutorial/src/450-game.md | 4 ++-- 12 files changed, 52 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 49a0b73..e67f6f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ exclude = [ ] [dependencies] -bevy = { version = "0.13.2", default-features = false, features = [ +bevy = { version = "0.14.2", default-features = false, features = [ "bevy_audio", "bevy_gilrs", "bevy_gltf", @@ -34,7 +34,7 @@ bevy = { version = "0.13.2", default-features = false, features = [ "x11", "vorbis", ] } -bevy_prototype_lyon = "0.11.0" +bevy_prototype_lyon = "0.12.0" ron = "0.8" serde = { version = "1.0", features = [ "derive" ] } diff --git a/examples/keyboard_events.rs b/examples/keyboard_events.rs index d389079..682ecb4 100644 --- a/examples/keyboard_events.rs +++ b/examples/keyboard_events.rs @@ -38,12 +38,12 @@ fn logic(engine: &mut Engine, _: &mut GameState) { { // Handle various keypresses. The extra keys are for the Dvorak keyboard layout. ;-) match key_code { + KeyCode::KeyW | KeyCode::ArrowUp | KeyCode::Comma => race_car.translation.y += 10.0, KeyCode::KeyA | KeyCode::ArrowLeft => race_car.translation.x -= 10.0, - KeyCode::KeyD | KeyCode::ArrowRight => race_car.translation.x += 10.0, - KeyCode::KeyO | KeyCode::ArrowDown => race_car.translation.y -= 10.0, - KeyCode::KeyW | KeyCode::ArrowUp => race_car.translation.y += 10.0, - KeyCode::KeyZ => race_car.rotation += std::f32::consts::FRAC_PI_4, - KeyCode::KeyC => race_car.rotation -= std::f32::consts::FRAC_PI_4, + KeyCode::KeyS | KeyCode::ArrowDown | KeyCode::KeyO => race_car.translation.y -= 10.0, + KeyCode::KeyD | KeyCode::ArrowRight | KeyCode::KeyE => race_car.translation.x += 10.0, + KeyCode::KeyZ | KeyCode::Semicolon=> race_car.rotation += std::f32::consts::FRAC_PI_4, + KeyCode::KeyC | KeyCode::KeyJ => race_car.rotation -= std::f32::consts::FRAC_PI_4, KeyCode::Equal => race_car.scale *= 1.1, KeyCode::Minus => race_car.scale *= 0.9, _ => {} diff --git a/examples/keyboard_state.rs b/examples/keyboard_state.rs index 939dc85..14910d9 100644 --- a/examples/keyboard_state.rs +++ b/examples/keyboard_state.rs @@ -36,26 +36,26 @@ fn logic(engine: &mut Engine, _: &mut GameState) { // Handle keyboard input let ks = &mut engine.keyboard_state; - if ks.pressed_any(&[KeyCode::KeyW, KeyCode::ArrowUp]) { + if ks.pressed_any(&[KeyCode::KeyW, KeyCode::ArrowUp, KeyCode::Comma]) { race_car.translation.y += move_amount; } if ks.pressed_any(&[KeyCode::KeyA, KeyCode::ArrowLeft]) { race_car.translation.x -= move_amount; } - if ks.pressed_any(&[KeyCode::KeyS, KeyCode::ArrowDown]) { + if ks.pressed_any(&[KeyCode::KeyS, KeyCode::ArrowDown, KeyCode::KeyO]) { race_car.translation.y -= move_amount; } - if ks.pressed_any(&[KeyCode::KeyD, KeyCode::ArrowRight]) { + if ks.pressed_any(&[KeyCode::KeyD, KeyCode::ArrowRight, KeyCode::KeyE]) { race_car.translation.x += move_amount; } // If you prefer a more functional style that is equivalent to the kind of logic above, // but takes closures to run if the buttons are pressed, you can call `.chain()` ks.chain() - .pressed_any(&[KeyCode::KeyZ], |_| { + .pressed_any(&[KeyCode::KeyZ, KeyCode::Semicolon], |_| { race_car.rotation += rotation_amount; }) - .pressed_any(&[KeyCode::KeyC], |_| { + .pressed_any(&[KeyCode::KeyC, KeyCode::KeyJ], |_| { race_car.rotation -= rotation_amount; }) .pressed_any(&[KeyCode::Equal], |_| { diff --git a/examples/scenarios/extreme_drivers_ed.rs b/examples/scenarios/extreme_drivers_ed.rs index 2e738cd..8f9a147 100644 --- a/examples/scenarios/extreme_drivers_ed.rs +++ b/examples/scenarios/extreme_drivers_ed.rs @@ -864,14 +864,14 @@ fn logic(engine: &mut Engine, game_state: &mut GameState) { if engine.keyboard_state.pressed_any(&[KeyW, ArrowUp, Comma]) { acceleration += 1.0; } - if engine.keyboard_state.pressed_any(&[KeyS, ArrowDown]) { + if engine.keyboard_state.pressed_any(&[KeyS, ArrowDown, KeyO]) { acceleration -= 1.0; } // Rotation/Turning input if engine.keyboard_state.pressed_any(&[KeyA, ArrowLeft]) { rotation += 1.0; } - if engine.keyboard_state.pressed_any(&[KeyD, ArrowRight]) { + if engine.keyboard_state.pressed_any(&[KeyD, ArrowRight, KeyE]) { rotation -= 1.0; } } diff --git a/src/game.rs b/src/game.rs index 941edfc..37c3995 100644 --- a/src/game.rs +++ b/src/game.rs @@ -3,7 +3,7 @@ use bevy::{ prelude::{Text as BevyText, *}, time::Time, utils::HashMap, - window::{close_on_esc, PrimaryWindow, WindowPlugin}, + window::{PrimaryWindow, WindowPlugin}, }; use bevy_prototype_lyon::prelude::*; use std::{ @@ -308,7 +308,7 @@ impl Game { self.app.insert_resource::(initial_game_state); self.app // TODO: Remove this to use the new, darker default color once the videos have been remastered - .insert_resource(ClearColor(Color::rgb(0.4, 0.4, 0.4))) + .insert_resource(ClearColor(Color::srgb(0.4, 0.4, 0.4))) // Built-ins .add_plugins( DefaultPlugins @@ -336,7 +336,7 @@ impl Game { )) //.insert_resource(ReportExecutionOrderAmbiguities) // for debugging .add_systems(Startup, setup); - self.app.world.spawn(Camera2dBundle::default()); + self.app.world_mut().spawn(Camera2dBundle::default()); let engine = std::mem::take(&mut self.engine); self.app.insert_resource(engine); let mut logic_functions = LogicFuncVec(vec![]); @@ -493,7 +493,7 @@ fn game_logic_sync( add_texts(&mut commands, &asset_server, &mut engine); if engine.should_exit { - app_exit_events.send(AppExit); + app_exit_events.send(AppExit::Success); } } @@ -515,3 +515,19 @@ impl DerefMut for Game { #[derive(Resource)] struct LogicFuncVec(Vec); + +pub fn close_on_esc( + mut commands: Commands, + focused_windows: Query<(Entity, &Window)>, + input: Res>, +) { + for (window, focus) in focused_windows.iter() { + if !focus.focused { + continue; + } + + if input.just_pressed(KeyCode::Escape) { + commands.entity(window).despawn(); + } + } +} diff --git a/tutorial/src/02-quick-start.md b/tutorial/src/02-quick-start.md index f8f712a..06279f5 100644 --- a/tutorial/src/02-quick-start.md +++ b/tutorial/src/02-quick-start.md @@ -20,6 +20,7 @@ use rusty_engine::prelude::*; struct GameState { health: i32, // add any fields you want, or leave the struct without fields } + fn main() { // Create a game let mut game = Game::new(); @@ -34,6 +35,7 @@ fn main() { // Run the game, with an initial state game.run(GameState { health: 100 }); } + // Your game logic functions can be named anything, but the first parameter is always a // `&mut Engine`, and the second parameter is a mutable reference to your custom game // state struct (`&mut GameState` in this case). The function returns a `bool`. diff --git a/tutorial/src/105-keyboard-state.md b/tutorial/src/105-keyboard-state.md index e5853d8..13bd7c4 100644 --- a/tutorial/src/105-keyboard-state.md +++ b/tutorial/src/105-keyboard-state.md @@ -31,7 +31,7 @@ if engine.keyboard_state.just_pressed(KeyCode::Escape) { Since "just pressed" and "just released" are not logical opposites, there is also a `just_released` method. This returns `true` if the key was previously in a pressed state and was just released this frame. ```rust,ignored -if engine.keyboard_state.just_released(KeyCode::W) { +if engine.keyboard_state.just_released(KeyCode::KeyW) { // do a thing when the key has just been released } ``` @@ -47,13 +47,13 @@ There is an `*_any` method for each of the three single key methods that does th Instead of passing a single `KeyCode` to these methods, you pass a slice containing all of the key codes you care about: ```rust,ignored -if engine.keyboard_state.pressed_any(&[KeyCode::W, KeyCode::Up]) { +if engine.keyboard_state.pressed_any(&[KeyCode::KeyW, KeyCode::ArrowUp]) { // player moves upward } -if engine.keyboard_state.just_pressed_any(&[KeyCode::Q, KeyCode::F1]) { +if engine.keyboard_state.just_pressed_any(&[KeyCode::KeyQ, KeyCode::F1]) { // open menu } -if engine.keyboard_state.just_released_any(&[KeyCode::Space, KeyCode::LControl]) { +if engine.keyboard_state.just_released_any(&[KeyCode::Space, KeyCode::ControlLeft]) { // re-evaluate your life choices } ``` diff --git a/tutorial/src/110-keyboard-events.md b/tutorial/src/110-keyboard-events.md index 42f45f0..f33b0d9 100644 --- a/tutorial/src/110-keyboard-events.md +++ b/tutorial/src/110-keyboard-events.md @@ -9,16 +9,17 @@ for keyboard_event in game_state.keyboard_events.drain(..) { // We're using `if let` and a pattern to destructure the KeyboardInput struct and only look at // keyboard input if the state is "Pressed". Then we match on the KeyCode and take action. if let KeyboardInput { - scan_code: _, - key_code: Some(key_code), + key_code, + logical_key, state: ButtonState::Pressed, + window: _, } = keyboard_event { match key_code { - KeyCode::W | KeyCode::Up => race_car.translation.y += 10.0, - KeyCode::A | KeyCode::Left => race_car.translation.x -= 10.0, - KeyCode::S | KeyCode::Down => race_car.translation.y -= 10.0, - KeyCode::D | KeyCode::Right => race_car.translation.x += 10.0, + KeyCode::KeyW | KeyCode::ArrowUp => race_car.translation.y += 10.0, + KeyCode::KeyA | KeyCode::ArrowLeft => race_car.translation.x -= 10.0, + KeyCode::KeyS | KeyCode::ArrowDown => race_car.translation.y -= 10.0, + KeyCode::KeyD | KeyCode::ArrowRight => race_car.translation.x += 10.0, _ => {} } } diff --git a/tutorial/src/115-mouse-state.md b/tutorial/src/115-mouse-state.md index af64301..bfea787 100644 --- a/tutorial/src/115-mouse-state.md +++ b/tutorial/src/115-mouse-state.md @@ -65,7 +65,7 @@ This represents both the final scrolling (vertical, y) state of the mouse wheel ```rust,ignored let mouse_wheel_state = engine.mouse_state.wheel(); -if mouse_wheel_state.y > 0 { +if mouse_wheel_state.y > 0.0 { // scrolling in one direction... } ``` diff --git a/tutorial/src/120-mouse-events.md b/tutorial/src/120-mouse-events.md index f2f4a05..c8bf7b9 100644 --- a/tutorial/src/120-mouse-events.md +++ b/tutorial/src/120-mouse-events.md @@ -39,7 +39,7 @@ for cursor_moved in &engine.mouse_location_events { Each location event has a corresponding motion event which reports the _relative_ motion of the mouse, rather than the absolute location. Mouse motion events are accessed through the `Engine.mouse_motion_events` vector and contain the [`MouseMotion`](https://docs.rs/rusty_engine/latest/rusty_engine/mouse/struct.MouseMotion.html) struct re-exported from Bevy. ```rust,ignored -for mouse_motion in &engine.state.mouse_motion_events { +for mouse_motion in &engine.mouse_motion_events { // do something with mouse_motion.delta } ``` diff --git a/tutorial/src/205-music.md b/tutorial/src/205-music.md index b8e0fbe..7032de6 100644 --- a/tutorial/src/205-music.md +++ b/tutorial/src/205-music.md @@ -13,7 +13,7 @@ The second parameter is the volume, which should be a value between `0.0` (silen game.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0); // using a filepath relative to `assets/` -game.audio_manager.play_music("audio/music/Classy 8-Bit.ogg", 1.0); +game.audio_manager.play_music("music/Classy 8-Bit.ogg", 1.0); ``` Any music already playing will be stopped when `play_music` is called. diff --git a/tutorial/src/450-game.md b/tutorial/src/450-game.md index 643d4d9..dd6f5ca 100644 --- a/tutorial/src/450-game.md +++ b/tutorial/src/450-game.md @@ -23,9 +23,9 @@ Pass a `Window` to the `window_settings` method to request specific settings for ```rust,ignored game.window_settings(Window { title: "My Awesome Game".into(), - width: 800.0, - height: 200.0, + resolution: WindowResolution::new(800.0, 200.0), ..Default::default() +}); ``` ### Adding Game Logic Functions From be90dda24367bba51c7d0eef7944e5bf66a375e6 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Sat, 7 Feb 2026 14:12:32 -0700 Subject: [PATCH 03/14] add copilot instructions --- .github/copilot-instructions.md | 102 ++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..6a86f34 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,102 @@ +# Rusty Engine - Copilot Instructions + +## Project Overview + +Rusty Engine is a 2D game engine for learning Rust, built as a beginner-friendly wrapper around [Bevy](https://bevyengine.org/). It provides a simplified API for sprites, text, audio, input handling, collision detection, and game state management. + +This is a **library crate** — there is no `main.rs`. Users create games by depending on this crate and using the `Game` struct as their entry point. + +## Architecture + +The codebase is flat — all modules live directly in `src/` with no subdirectories: + +- **`lib.rs`** — Public API surface, prelude module, direction constants (UP, DOWN, LEFT, RIGHT, etc.) +- **`game.rs`** — Core `Engine` and `Game` structs. `Engine` holds all game state (sprites, texts, input, audio, timing). `Game` wraps a Bevy `App` and provides `add_logic()` / `run()`. +- **`sprite.rs`** — `Sprite` struct (label, position, rotation, scale, collider) and `SpritePreset` enum (20 built-in sprites). +- **`text.rs`** — `Text` struct for on-screen text rendering. +- **`audio.rs`** — `AudioManager` with `play_sfx()` / `play_music()` / `stop_music()`. Includes `SfxPreset` (18 sounds) and `MusicPreset` (3 tracks). +- **`keyboard.rs`** — `KeyboardState` with pressed/just_pressed/just_released tracking and fluent `chain()` API. +- **`mouse.rs`** — `MouseState` with location, motion, buttons, and wheel state. Coordinates are game-space (positive x=right, positive y=up). +- **`physics.rs`** — Convex polygon collision detection using Separating Axis Theorem (SAT). `Collider` enum, `CollisionEvent`, `CollisionPair`. + +### Key Design Pattern + +The engine uses a **sync pattern** between user code and Bevy ECS: +1. Each frame, Bevy state is copied into the `Engine` struct +2. User logic functions receive `&mut Engine` and a mutable reference to a user-defined game state struct that the user marked with `#[derive(Resource)` and passed into `Game::run()` +3. After user logic runs, `game_logic_sync()` pushes changes back to Bevy entities + +User-facing game logic functions always have the following signature, where `GameState` is the user-defined game state struct: +```rust +fn my_logic(engine: &mut Engine, game_state: &mut GameState) { + // game logic here +} +``` + +## Code Conventions + +- **Prelude pattern**: All public types are re-exported via `rusty_engine::prelude::*` +- **Labels as identifiers**: Sprites and texts are stored in `HashMap` keyed by a unique label string +- **Constants**: Use `SCREAMING_SNAKE_CASE` (e.g., `ROTATION_SPEED`, `TEXT_DEFAULT_LAYER`) +- **Enums for presets**: Built-in assets use enums (`SpritePreset`, `SfxPreset`, `MusicPreset`) that implement `IntoIterator` and filepath conversion +- **Collider files**: Sprite colliders are stored as `.collider` files in RON format alongside sprite assets +- **Resource derive**: Game state structs must derive `#[derive(Resource)]` for Bevy compatibility +- **Doc comments**: Use `///` for public API documentation. Include usage examples in doc comments. +- **No `unwrap()` in library code** — prefer proper error handling or `expect()` with descriptive messages if the error is not recoverable. + +## Dependencies + +Key dependencies (keep versions aligned when updating): +- `bevy` — Core engine (selective features: audio, rendering, text, gamepad, GLTF) +- `bevy_prototype_lyon` 0.12.0 — Shape rendering for collider visualization +- `ron` 0.8 — RON format serialization for collider data +- `serde` 1.0 — Serialization/deserialization for colliders +- `rand` 0.8 — Dev dependency only, used in examples + +Dependencies that start with `bevy_` are bevy plugins and need to be a version supported by the version of bevy we are currently on. The README.md file for these dependencies usually has a table indicating which version(s) are compatible with bevy version(s). + +## Build, Test, and Lint + +```bash +# Run tests +cargo test --workspace --all-targets --all-features + +# Run clippy linter +cargo clippy --workspace --all-targets --all-features -- -D warnings + +# Check formatting +cargo fmt --all -- --check + +# Run a specific example +cargo run --release --example +# Available examples: collision, collider, game_state, keyboard, layer, +# level_creator, mouse, music, placement, sfx, sound, sprite, text, window +``` + +Every change should be validated by running the example(s) that touch the change. If no example touches the change, then an existing example related to the change should be updated to touch the change or, for completely new features, a new example should be added. + +Examples need to compile, run, and exit successfully. Additionally, examples need to behave correctly while running. In order to verify that examples run correctly, a human should be prompted that an example is going to be run, and then if it compiles, runs, and exits successfully then you should ask the human if the example worked correctly. + +For changes that affect more than three examples, `script/test_examples` should be run to test all examples. Upon successful build and run of all examples, you should ask a human if all examples worked correctly. + +CI runs on Ubuntu, macOS, and Windows. Linux requires `libasound2-dev` and `libudev-dev` system packages. + +## Examples and Scenarios + +- **`examples/`** — Runnable demos of individual engine features. Each has a doc comment explaining how to run it. +- **`scenarios/`** — Game programming challenges at varying difficulty (Easy → Insane). Include skeleton code and step-by-step instructions. +- **`tutorial/`** — mdBook-based tutorial covering all engine features, built with `mdbook`. + +## Release Process + +Uses `cargo-release` configured via `release.toml`. Version replacements are automated across README.md, tutorial files, and CHANGELOG.md. See `RELEASE.md` for full instructions. + +## Important Guidelines + +- This engine is designed for **beginners learning Rust** — keep the API simple and approachable +- Avoid exposing raw Bevy types in the public API; wrap them in engine-specific types. Large enums used for input are an exception, and can be passed straight through from Bevy. +- All coordinates use a game-space system where positive x is right and positive y is up (origin at center) +- Sprite layers default to 0.0; text layers default to 900.0 (rendered on top) +- When adding new presets (sprites, sounds, music), add the corresponding assets to `assets/` and update the relevant enum +- Collider polygons must be convex for the SAT collision algorithm to work correctly +- The `Engine` struct fields are split into "SYNCED" (user-modifiable, synced back to Bevy) and "INFO" (read-only, populated from Bevy each frame) categories From b543f5371c9bfc4a7bc8b564ce2fa5842257c46d Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Sat, 7 Feb 2026 15:57:33 -0700 Subject: [PATCH 04/14] upgrades: bevy 0.14.2 to 0.15.3; bevy_prototype_lyon 0.12.0 to 0.13.0, ron 0.8 to 0.9, rand 0.8 to 0.9 --- Cargo.toml | 26 +++++--------- examples/collider.rs | 6 ++-- examples/keyboard_events.rs | 15 +++++--- examples/level_creator.rs | 1 + examples/scenarios/car_shoot.rs | 10 ++---- examples/scenarios/road_race.rs | 8 ++--- examples/text.rs | 2 +- examples/window.rs | 10 +++--- src/audio.rs | 25 ++++++------- src/game.rs | 64 +++++++++++++++++---------------- src/sprite.rs | 4 +-- 11 files changed, 85 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e67f6f8..25ba841 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,40 +6,32 @@ edition = "2021" homepage = "https://github.com/CleanCut/rusty_engine" repository = "https://github.com/CleanCut/rusty_engine" readme = "README.md" -keywords = [ "game", "engine", "graphics", "audio", "rusty" ] -categories = [ "game-engines" ] +keywords = ["game", "engine", "graphics", "audio", "rusty"] +categories = ["game-engines"] license = "MIT OR Apache-2.0" -exclude = [ - "/assets", - "/.github", - "/scenarios", - "/tutorial", - "/script", - "release.toml", - "RELEASE.md", - ".gitignore", -] +exclude = ["/assets", "/.github", "/scenarios", "/tutorial", "/script", "release.toml", "RELEASE.md", ".gitignore"] [dependencies] -bevy = { version = "0.14.2", default-features = false, features = [ +bevy = { version = "0.15.3", default-features = false, features = [ "bevy_audio", "bevy_gilrs", "bevy_gltf", "bevy_render", "bevy_text", "bevy_winit", + "custom_cursor", "png", "hdr", "mp3", "x11", "vorbis", ] } -bevy_prototype_lyon = "0.12.0" -ron = "0.8" -serde = { version = "1.0", features = [ "derive" ] } +bevy_prototype_lyon = "0.13.0" +ron = "0.9" +serde = { version = "1.0", features = ["derive"] } [dev-dependencies] -rand = "0.8" +rand = "0.9" [[example]] name = "car_shoot" diff --git a/examples/collider.rs b/examples/collider.rs index 941ba0c..4a92523 100644 --- a/examples/collider.rs +++ b/examples/collider.rs @@ -193,10 +193,8 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { if convex.value != CONVEX_MESSAGE { convex.value = CONVEX_MESSAGE.into(); } - } else { - if convex.value != NOT_CONVEX_MESSAGE { - convex.value = NOT_CONVEX_MESSAGE.into(); - } + } else if convex.value != NOT_CONVEX_MESSAGE { + convex.value = NOT_CONVEX_MESSAGE.into(); } // Write the collider file if engine.keyboard_state.just_pressed(KeyCode::KeyW) { diff --git a/examples/keyboard_events.rs b/examples/keyboard_events.rs index 682ecb4..e170526 100644 --- a/examples/keyboard_events.rs +++ b/examples/keyboard_events.rs @@ -34,18 +34,25 @@ fn logic(engine: &mut Engine, _: &mut GameState) { state: ButtonState::Pressed, window: _, logical_key: _, + .. } = keyboard_event { // Handle various keypresses. The extra keys are for the Dvorak keyboard layout. ;-) match key_code { KeyCode::KeyW | KeyCode::ArrowUp | KeyCode::Comma => race_car.translation.y += 10.0, KeyCode::KeyA | KeyCode::ArrowLeft => race_car.translation.x -= 10.0, - KeyCode::KeyS | KeyCode::ArrowDown | KeyCode::KeyO => race_car.translation.y -= 10.0, - KeyCode::KeyD | KeyCode::ArrowRight | KeyCode::KeyE => race_car.translation.x += 10.0, - KeyCode::KeyZ | KeyCode::Semicolon=> race_car.rotation += std::f32::consts::FRAC_PI_4, + KeyCode::KeyS | KeyCode::ArrowDown | KeyCode::KeyO => { + race_car.translation.y -= 10.0 + } + KeyCode::KeyD | KeyCode::ArrowRight | KeyCode::KeyE => { + race_car.translation.x += 10.0 + } + KeyCode::KeyZ | KeyCode::Semicolon => { + race_car.rotation += std::f32::consts::FRAC_PI_4 + } KeyCode::KeyC | KeyCode::KeyJ => race_car.rotation -= std::f32::consts::FRAC_PI_4, KeyCode::Equal => race_car.scale *= 1.1, - KeyCode::Minus => race_car.scale *= 0.9, + KeyCode::Minus => race_car.scale *= 0.9, _ => {} } diff --git a/examples/level_creator.rs b/examples/level_creator.rs index 5465571..7121d95 100644 --- a/examples/level_creator.rs +++ b/examples/level_creator.rs @@ -100,6 +100,7 @@ fn logic(engine: &mut Engine, game_state: &mut GameState) { state, window: _, logical_key: _, + .. } = keyboard_event; if *state == ButtonState::Pressed { diff --git a/examples/scenarios/car_shoot.rs b/examples/scenarios/car_shoot.rs index ad3cf29..edcd19a 100644 --- a/examples/scenarios/car_shoot.rs +++ b/examples/scenarios/car_shoot.rs @@ -99,7 +99,7 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { if game_state.spawn_timer.tick(engine.delta).just_finished() { // Reset the timer to a new value game_state.spawn_timer = - Timer::from_seconds(thread_rng().gen_range(0.1..1.25), TimerMode::Once); + Timer::from_seconds(rand::rng().random_range(0.1..1.25), TimerMode::Once); // Get the new car if game_state.cars_left > 0 { game_state.cars_left -= 1; @@ -114,14 +114,10 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { RacingCarRed, RacingCarYellow, ]; - let sprite_preset = car_choices - .iter() - .choose(&mut thread_rng()) - .unwrap() - .clone(); + let sprite_preset = car_choices.iter().choose(&mut rand::rng()).unwrap().clone(); let car = engine.add_sprite(label, sprite_preset); car.translation.x = -740.0; - car.translation.y = thread_rng().gen_range(-100.0..325.0); + car.translation.y = rand::rng().random_range(-100.0..325.0); car.collision = true; } } diff --git a/examples/scenarios/road_race.rs b/examples/scenarios/road_race.rs index b10d7eb..195a02c 100644 --- a/examples/scenarios/road_race.rs +++ b/examples/scenarios/road_race.rs @@ -50,8 +50,8 @@ fn main() { let obstacle = game.add_sprite(format!("obstacle{}", i), preset); obstacle.layer = 5.0; obstacle.collision = true; - obstacle.translation.x = thread_rng().gen_range(800.0..1600.0); - obstacle.translation.y = thread_rng().gen_range(-300.0..300.0); + obstacle.translation.x = rand::rng().random_range(800.0..1600.0); + obstacle.translation.y = rand::rng().random_range(-300.0..300.0); } // Create the health message @@ -105,8 +105,8 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { if sprite.label.starts_with("obstacle") { sprite.translation.x -= ROAD_SPEED * engine.delta_f32; if sprite.translation.x < -800.0 { - sprite.translation.x = thread_rng().gen_range(800.0..1600.0); - sprite.translation.y = thread_rng().gen_range(-300.0..300.0); + sprite.translation.x = rand::rng().random_range(800.0..1600.0); + sprite.translation.y = rand::rng().random_range(-300.0..300.0); } } } diff --git a/examples/text.rs b/examples/text.rs index fddef59..73d301c 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -68,7 +68,7 @@ fn game_logic(engine: &mut Engine, game_state: &mut GameState) { r.rotation -= 1.5 * engine.delta_f32; let s = engine.texts.get_mut("scale").unwrap(); - s.scale = 1.5 + ((engine.time_since_startup_f64 * 0.5).cos() as f32) * -1.0; + s.scale = 1.5 - (engine.time_since_startup_f64 * 0.5).cos() as f32; let msg3 = engine.texts.get_mut("zoom_msg").unwrap(); msg3.font_size = 10.0 * (engine.time_since_startup_f64 * 0.5).cos() as f32 + 25.0; diff --git a/examples/window.rs b/examples/window.rs index 099f45b..33b9f13 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -10,16 +10,16 @@ struct GameState {} fn main() { let mut game = Game::new(); - let mut cursor = Cursor::default(); - cursor.visible = false; - game.window_settings(Window { resolution: WindowResolution::new(800.0, 200.0), title: "Custom Window Settings".into(), resizable: false, decorations: false, - cursor, - ..Default::default() // for the rest of the options, see https://docs.rs/bevy/0.10.1/bevy/index.html + cursor_options: CursorOptions { + visible: false, + ..Default::default() + }, + ..Default::default() // for the rest of the options, see https://docs.rs/bevy/0.15.3/bevy/index.html }); let _ = game.add_text( "message", diff --git a/src/audio.rs b/src/audio.rs index 503915d..0550119 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -251,17 +251,18 @@ pub fn queue_managed_audio_system( mut game_state: ResMut, ) { for (sfx, volume) in game_state.audio_manager.sfx_queue.drain(..) { - commands.spawn(AudioBundle { - source: asset_server.load(format!("audio/{}", sfx)), - settings: PlaybackSettings { + commands.spawn(( + AudioPlayer::(asset_server.load(format!("audio/{}", sfx))), + PlaybackSettings { mode: PlaybackMode::Despawn, volume: Volume::new(volume), ..Default::default() }, - }); + )); } - #[allow(for_loops_over_fallibles)] - if let Some(item) = game_state.audio_manager.music_queue.drain(..).last() { + let last_music_item = game_state.audio_manager.music_queue.pop(); + game_state.audio_manager.music_queue.clear(); + if let Some(item) = last_music_item { // stop any music currently playing if let Ok((entity, music)) = music_query.get_single() { music.stop(); @@ -270,15 +271,15 @@ pub fn queue_managed_audio_system( // start the new music...if we have some if let Some((music, volume)) = item { let entity = commands - .spawn(AudioBundle { - source: asset_server.load(format!("audio/{}", music)), - settings: PlaybackSettings { - volume: Volume::new(volume), + .spawn(( + AudioPlayer::(asset_server.load(format!("audio/{}", music))), + PlaybackSettings { mode: PlaybackMode::Loop, + volume: Volume::new(volume), ..Default::default() }, - }) - .insert(Music) + Music, + )) .id(); game_state.audio_manager.playing = Some(entity); } diff --git a/src/game.rs b/src/game.rs index 37c3995..bdf1d34 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,6 +1,6 @@ use bevy::{ app::AppExit, - prelude::{Text as BevyText, *}, + prelude::{Sprite as BevySprite, *}, time::Time, utils::HashMap, window::{PrimaryWindow, WindowPlugin}, @@ -24,7 +24,7 @@ use crate::{ }; // Public re-export -pub use bevy::window::{Cursor, Window, WindowMode, WindowResolution}; +pub use bevy::window::{CursorOptions, Window, WindowMode, WindowResolution}; /// Engine is the primary way that you will interact with Rusty Engine. Each frame this struct /// is provided to the "logic" functions (or closures) that you provided to [`Game::add_logic`]. The @@ -170,7 +170,7 @@ fn add_collider_lines(commands: &mut Commands, sprite: &mut Sprite) { .spawn(( ShapeBundle { path: GeometryBuilder::new().add(&line).build(), - spatial: SpatialBundle::from_transform(transform), + transform, ..Default::default() }, Stroke::new(Color::WHITE, 1.0 / transform.scale.x), @@ -196,11 +196,11 @@ pub fn add_sprites(commands: &mut Commands, asset_server: &Res, eng let texture_path = sprite.filepath.clone(); commands.spawn(( sprite, - SpriteBundle { - texture: asset_server.load(texture_path), - transform, + BevySprite { + image: asset_server.load(texture_path), ..Default::default() }, + transform, )); } } @@ -216,19 +216,15 @@ pub fn add_texts(commands: &mut Commands, asset_server: &Res, engin let font_path = text.font.clone(); commands.spawn(( text, - Text2dBundle { - text: BevyText::from_section( - text_string, - TextStyle { - font: asset_server.load(font_path), - font_size, - color: Color::WHITE, - }, - ) - .with_justify(JustifyText::Center), - transform, + Text2d(text_string), + TextFont { + font: asset_server.load(font_path), + font_size, ..Default::default() }, + TextColor(Color::WHITE), + TextLayout::new_with_justify(JustifyText::Center), + transform, )); } } @@ -336,7 +332,7 @@ impl Game { )) //.insert_resource(ReportExecutionOrderAmbiguities) // for debugging .add_systems(Startup, setup); - self.app.world_mut().spawn(Camera2dBundle::default()); + self.app.world_mut().spawn(Camera2d); let engine = std::mem::take(&mut self.engine); self.app.insert_resource(engine); let mut logic_functions = LogicFuncVec(vec![]); @@ -349,7 +345,7 @@ impl Game { /// /// - `engine: &mut Engine` /// - `game_state`, which is a mutable reference (`&mut`) to the game state struct you defined, - /// or `&mut ()` if you didn't define one. + /// or `&mut ()` if you didn't define one. pub fn add_logic(&mut self, logic_function: fn(&mut Engine, &mut S)) { self.logic_functions.0.push(logic_function); } @@ -370,15 +366,21 @@ fn game_logic_sync( mut collision_events: EventReader, mut query_set: ParamSet<( Query<(Entity, &mut Sprite, &mut Transform)>, - Query<(Entity, &mut Text, &mut Transform, &mut BevyText)>, + Query<( + Entity, + &mut Text, + &mut Transform, + &mut Text2d, + &mut TextFont, + )>, Query<(Entity, &mut Stroke, &mut Transform, &ColliderLines)>, )>, ) { // Update this frame's timing info engine.delta = time.delta(); - engine.delta_f32 = time.delta_seconds(); + engine.delta_f32 = time.delta_secs(); engine.time_since_startup = time.elapsed(); - engine.time_since_startup_f64 = time.elapsed_seconds_f64(); + engine.time_since_startup_f64 = time.elapsed_secs_f64(); // Copy keyboard state over to engine to give to users engine.keyboard_state = keyboard_state.clone(); @@ -402,7 +404,7 @@ fn game_logic_sync( // Copy all texts over to the engine to give to users engine.texts.clear(); - for (_, text, _, _) in query_set.p1().iter() { + for (_, text, _, _, _) in query_set.p1().iter() { let _ = engine.texts.insert(text.label.clone(), (*text).clone()); } @@ -469,20 +471,22 @@ fn game_logic_sync( add_sprites(&mut commands, &asset_server, &mut engine); // Transfer any changes in the user's Texts to the Bevy Text and Transform components - for (entity, mut text, mut transform, mut bevy_text_component) in query_set.p1().iter_mut() { + for (entity, mut text, mut transform, mut bevy_text_component, mut text_font) in + query_set.p1().iter_mut() + { if let Some(text_copy) = engine.texts.remove(&text.label) { *text = text_copy; *transform = text.bevy_transform(); - if text.value != bevy_text_component.sections[0].value { - bevy_text_component.sections[0].value = text.value.clone(); + if text.value != bevy_text_component.0 { + bevy_text_component.0 = text.value.clone(); } #[allow(clippy::float_cmp)] - if text.font_size != bevy_text_component.sections[0].style.font_size { - bevy_text_component.sections[0].style.font_size = text.font_size; + if text.font_size != text_font.font_size { + text_font.font_size = text.font_size; } let font = asset_server.load(text.font.clone()); - if bevy_text_component.sections[0].style.font != font { - bevy_text_component.sections[0].style.font = font; + if text_font.font != font { + text_font.font = font; } } else { commands.entity(entity).despawn(); diff --git a/src/sprite.rs b/src/sprite.rs index 39f1691..44c1b3c 100644 --- a/src/sprite.rs +++ b/src/sprite.rs @@ -37,8 +37,8 @@ pub struct Sprite { /// Reads the collider file and creates the collider fn read_collider_from_file(filepath: &Path) -> Collider { - match File::open(filepath) { - Ok(fh) => match ron::de::from_reader::<_, Collider>(fh) { + match std::fs::read_to_string(filepath) { + Ok(contents) => match ron::from_str::(&contents) { Ok(collider) => collider, Err(e) => { eprintln!("failed deserializing collider from file: {}", e); From 30b735b59a9d1cb3bbb232f000b9973f8b5da072 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Sat, 7 Feb 2026 17:10:36 -0700 Subject: [PATCH 05/14] upgrade from bevy 0.15.3 to 0.16, bevy_prototype_lyon 0.13.0 to 0.14.0; script/test_examples now builds all examples at once before beginning to run them --- .github/copilot-instructions.md | 2 +- Cargo.toml | 4 +-- examples/scenarios/extreme_drivers_ed.rs | 2 +- script/test_examples | 2 ++ src/audio.rs | 6 ++-- src/game.rs | 38 ++++++++++-------------- src/keyboard.rs | 2 +- src/mouse.rs | 2 +- src/physics.rs | 4 +-- 9 files changed, 29 insertions(+), 33 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6a86f34..2f363ab 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -36,7 +36,7 @@ fn my_logic(engine: &mut Engine, game_state: &mut GameState) { ## Code Conventions - **Prelude pattern**: All public types are re-exported via `rusty_engine::prelude::*` -- **Labels as identifiers**: Sprites and texts are stored in `HashMap` keyed by a unique label string +- **Labels as identifiers**: Sprites and texts are stored in `bevy::utils::HashMap` keyed by a unique label string - **Constants**: Use `SCREAMING_SNAKE_CASE` (e.g., `ROTATION_SPEED`, `TEXT_DEFAULT_LAYER`) - **Enums for presets**: Built-in assets use enums (`SpritePreset`, `SfxPreset`, `MusicPreset`) that implement `IntoIterator` and filepath conversion - **Collider files**: Sprite colliders are stored as `.collider` files in RON format alongside sprite assets diff --git a/Cargo.toml b/Cargo.toml index 25ba841..050e888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ license = "MIT OR Apache-2.0" exclude = ["/assets", "/.github", "/scenarios", "/tutorial", "/script", "release.toml", "RELEASE.md", ".gitignore"] [dependencies] -bevy = { version = "0.15.3", default-features = false, features = [ +bevy = { version = "0.16", default-features = false, features = [ "bevy_audio", "bevy_gilrs", "bevy_gltf", @@ -26,7 +26,7 @@ bevy = { version = "0.15.3", default-features = false, features = [ "x11", "vorbis", ] } -bevy_prototype_lyon = "0.13.0" +bevy_prototype_lyon = "0.14.0" ron = "0.9" serde = { version = "1.0", features = ["derive"] } diff --git a/examples/scenarios/extreme_drivers_ed.rs b/examples/scenarios/extreme_drivers_ed.rs index 8f9a147..0d749bd 100644 --- a/examples/scenarios/extreme_drivers_ed.rs +++ b/examples/scenarios/extreme_drivers_ed.rs @@ -854,7 +854,7 @@ fn logic(engine: &mut Engine, game_state: &mut GameState) { } // Player movement - let player = engine.sprites.get_mut("player".into()).unwrap(); + let player = engine.sprites.get_mut("player").unwrap(); let mut acceleration = 0.0; let mut rotation = 0.0; // Nested scope so the bare KeyCode variants only show up here where we want to use them diff --git a/script/test_examples b/script/test_examples index c9a0f4d..52d8032 100755 --- a/script/test_examples +++ b/script/test_examples @@ -1,5 +1,7 @@ #!/usr/bin/env bash +cargo build --release --examples + for file in $(find examples -name '*.rs' | sort) ; do example=$(basename $file | cut -d . -f 1) extra_args= diff --git a/src/audio.rs b/src/audio.rs index 0550119..d961453 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -255,7 +255,7 @@ pub fn queue_managed_audio_system( AudioPlayer::(asset_server.load(format!("audio/{}", sfx))), PlaybackSettings { mode: PlaybackMode::Despawn, - volume: Volume::new(volume), + volume: Volume::Linear(volume), ..Default::default() }, )); @@ -264,7 +264,7 @@ pub fn queue_managed_audio_system( game_state.audio_manager.music_queue.clear(); if let Some(item) = last_music_item { // stop any music currently playing - if let Ok((entity, music)) = music_query.get_single() { + if let Ok((entity, music)) = music_query.single() { music.stop(); commands.entity(entity).despawn(); } @@ -275,7 +275,7 @@ pub fn queue_managed_audio_system( AudioPlayer::(asset_server.load(format!("audio/{}", music))), PlaybackSettings { mode: PlaybackMode::Loop, - volume: Volume::new(volume), + volume: Volume::Linear(volume), ..Default::default() }, Music, diff --git a/src/game.rs b/src/game.rs index bdf1d34..f10aee8 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,8 +1,8 @@ use bevy::{ app::AppExit, + platform::collections::HashMap, prelude::{Sprite as BevySprite, *}, time::Time, - utils::HashMap, window::{PrimaryWindow, WindowPlugin}, }; use bevy_prototype_lyon::prelude::*; @@ -158,28 +158,20 @@ fn add_collider_lines(commands: &mut Commands, sprite: &mut Sprite) { // Add the collider lines, a visual representation of the sprite's collider let points = sprite.collider.points(); // will be empty vector if NoCollider if points.len() >= 2 { - let mut path_builder = PathBuilder::new(); - path_builder.move_to(points[0]); + let mut shape_path = ShapePath::new().move_to(points[0]); for point in &points[1..] { - path_builder.line_to(*point); + shape_path = shape_path.line_to(*point); } - path_builder.close(); // draws the line from the last point to the first point - let line = path_builder.build(); + shape_path = shape_path.close(); let transform = sprite.bevy_transform(); + let line_width = 1.0 / transform.scale.x; commands .spawn(( - ShapeBundle { - path: GeometryBuilder::new().add(&line).build(), - transform, - ..Default::default() - }, - Stroke::new(Color::WHITE, 1.0 / transform.scale.x), + ShapeBuilder::with(&shape_path) + .stroke(Stroke::new(Color::WHITE, line_width)) + .build(), + transform, )) - // .spawn(GeometryBuilder::build_as( - // &line, - // DrawMode::Stroke(StrokeMode::new(Color::WHITE, 1.0 / transform.scale.x)), - // transform, - // )) .insert(ColliderLines { sprite_label: sprite.label.clone(), }); @@ -236,7 +228,7 @@ pub fn update_window_dimensions( mut engine: ResMut, ) { // It's possible to not have a window for the first frame or two - let Ok(window) = window_query.get_single() else { + let Ok(window) = window_query.single() else { return; }; let screen_dimensions = Vec2::new(window.width(), window.height()); @@ -373,7 +365,7 @@ fn game_logic_sync( &mut Text2d, &mut TextFont, )>, - Query<(Entity, &mut Stroke, &mut Transform, &ColliderLines)>, + Query<(Entity, &mut Shape, &mut Transform, &ColliderLines)>, )>, ) { // Update this frame's timing info @@ -443,7 +435,7 @@ fn game_logic_sync( } } // Update transform & line width - for (_, mut stroke, mut transform, collider_lines) in query_set.p2().iter_mut() { + for (_, mut shape, mut transform, collider_lines) in query_set.p2().iter_mut() { if let Some(sprite) = engine.sprites.get(&collider_lines.sprite_label) { *transform = sprite.bevy_transform(); // We want collider lines to appear on top of the sprite they are for, so they need a @@ -452,7 +444,9 @@ fn game_logic_sync( } // Stroke line width gets scaled with the transform, but we want it to appear to be the same // regardless of scale, so we have to counter the scale. - stroke.options.line_width = 1.0 / transform.scale.x; + if let Some(ref mut stroke) = shape.stroke { + stroke.options.line_width = 1.0 / transform.scale.x; + } } } engine.last_show_colliders = engine.show_colliders; @@ -497,7 +491,7 @@ fn game_logic_sync( add_texts(&mut commands, &asset_server, &mut engine); if engine.should_exit { - app_exit_events.send(AppExit::Success); + app_exit_events.write(AppExit::Success); } } diff --git a/src/keyboard.rs b/src/keyboard.rs index 2991d86..07d5958 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,7 +1,7 @@ //! Facilities for dealing with keyboard input use crate::prelude::Engine; -use bevy::{prelude::*, utils::HashMap}; +use bevy::{platform::collections::HashMap, prelude::*}; // Re-export some Bevy types to use pub use bevy::input::keyboard::{KeyCode, KeyboardInput}; diff --git a/src/mouse.rs b/src/mouse.rs index 0dbd666..702fc74 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -1,7 +1,7 @@ //! Facilities for dealing with mouse input use crate::prelude::Engine; -use bevy::{prelude::*, utils::HashSet}; +use bevy::{platform::collections::HashSet, prelude::*}; // Re-export some Bevy types to use pub use bevy::{ diff --git a/src/physics.rs b/src/physics.rs index 97f92d5..5764d1c 100644 --- a/src/physics.rs +++ b/src/physics.rs @@ -145,7 +145,7 @@ fn collision_detection( .cloned() .collect(); - collision_events.send_batch(beginning_collisions.iter().map(|p| CollisionEvent { + collision_events.write_batch(beginning_collisions.iter().map(|p| CollisionEvent { state: CollisionState::Begin, pair: p.clone(), })); @@ -159,7 +159,7 @@ fn collision_detection( .cloned() .collect(); - collision_events.send_batch(ending_collisions.iter().map(|p| CollisionEvent { + collision_events.write_batch(ending_collisions.iter().map(|p| CollisionEvent { state: CollisionState::End, pair: p.clone(), })); From 6ce5d2e3caa6ca2dbd6db700d673ffe1780a7ab9 Mon Sep 17 00:00:00 2001 From: Nathan Stocks Date: Sat, 7 Feb 2026 20:13:05 -0700 Subject: [PATCH 06/14] upgrade from bevy 0.16 to 0.17; bevy_prototype_lyon from 0.14.0 to 0.15.0 --- Cargo.toml | 6 ++++-- examples/window.rs | 8 ++------ src/game.rs | 6 +++--- src/keyboard.rs | 2 +- src/mouse.rs | 14 +++++++------- src/physics.rs | 6 +++--- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 050e888..dc26fa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,12 @@ license = "MIT OR Apache-2.0" exclude = ["/assets", "/.github", "/scenarios", "/tutorial", "/script", "release.toml", "RELEASE.md", ".gitignore"] [dependencies] -bevy = { version = "0.16", default-features = false, features = [ +bevy = { version = "0.17", default-features = false, features = [ "bevy_audio", "bevy_gilrs", "bevy_gltf", "bevy_render", + "bevy_sprite_render", "bevy_text", "bevy_winit", "custom_cursor", @@ -24,9 +25,10 @@ bevy = { version = "0.16", default-features = false, features = [ "hdr", "mp3", "x11", + "wayland", "vorbis", ] } -bevy_prototype_lyon = "0.14.0" +bevy_prototype_lyon = "0.15.0" ron = "0.9" serde = { version = "1.0", features = ["derive"] } diff --git a/examples/window.rs b/examples/window.rs index 33b9f13..9e58396 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -11,15 +11,11 @@ fn main() { let mut game = Game::new(); game.window_settings(Window { - resolution: WindowResolution::new(800.0, 200.0), + resolution: WindowResolution::new(800, 200), title: "Custom Window Settings".into(), resizable: false, decorations: false, - cursor_options: CursorOptions { - visible: false, - ..Default::default() - }, - ..Default::default() // for the rest of the options, see https://docs.rs/bevy/0.15.3/bevy/index.html + ..Default::default() // for the rest of the options, see https://docs.rs/bevy/0.17/bevy/index.html }); let _ = game.add_text( "message", diff --git a/src/game.rs b/src/game.rs index f10aee8..83112fc 100644 --- a/src/game.rs +++ b/src/game.rs @@ -215,7 +215,7 @@ pub fn add_texts(commands: &mut Commands, asset_server: &Res, engin ..Default::default() }, TextColor(Color::WHITE), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), transform, )); } @@ -354,8 +354,8 @@ fn game_logic_sync( keyboard_state: Res, mouse_state: Res, time: Res