diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..9ccf9cf --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,104 @@ +# 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 `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 +- **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` — Shape rendering for collider visualization +- `ron` — RON format serialization for collider data +- `serde` — Serialization/deserialization for colliders +- `rand` — 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`. + +All examples, scenarios, and the tutorial documentation should be updated whenever the public API is changed. + +## 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 126a99a..8a323af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ ## [Unreleased] - ReleaseDate +### Breaking changes + +- The `KeyCode` enum (passed through from Bevy) has had many variants renamed. For example, `KeyCode::Key1` is now `KeyCode::Digit1`, `KeyCode::A` is now `KeyCode::KeyA`, and `KeyCode::Right` is now `KeyCode::ArrowRight`. Please refer to Bevy's [`KeyCode` documentation](https://docs.rs/bevy/0.18.0/bevy/input/keyboard/enum.KeyCode.html) for the full list of variants and their new names. +- Bevy's `WindowResolution::new` function now takes `u32`s instead of `f32`s +- Bevy's `Color` struct has had many changes. For example, `Color::rgb` is now `Color::srgb`. Please refer to Bevy's [`Color` documentation](https://docs.rs/bevy/0.18.0/bevy/color/enum.Color.html) for the full list of associated functions and their new names. + + +### Improved + +- Update bevy from 0.12 to 0.18 +- Update bevy_prototype_lyon from 0.10 to 0.16 +- Update ron from 0.9 to 0.12 +- Update rand from 0.8 to 0.9 +- The tutorial has been updated to reflect the API changes and fix a few issues. + ## [6.0.0] - 2023-12-03 ### Breaking changes diff --git a/Cargo.toml b/Cargo.toml index c44bd6b..fee0cb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,40 +6,33 @@ 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.12.1", default-features = false, features = [ +bevy = { version = "0.18", default-features = false, features = [ "bevy_audio", "bevy_gilrs", "bevy_gltf", "bevy_render", + "bevy_sprite_render", "bevy_text", "bevy_winit", + "custom_cursor", "png", "hdr", "mp3", "x11", "vorbis", ] } -bevy_prototype_lyon = "0.10.0" -ron = "0.8" -serde = { version = "1.0", features = [ "derive" ] } +bevy_prototype_lyon = "0.16.0" +ron = "0.12" +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 c0db2c3..4a92523 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; @@ -194,13 +193,11 @@ 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::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..e170526 100644 --- a/examples/keyboard_events.rs +++ b/examples/keyboard_events.rs @@ -30,22 +30,29 @@ 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::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::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 e01d510..14910d9 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, KeyCode::Comma]) { 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, KeyCode::KeyO]) { race_car.translation.y -= move_amount; } - if ks.pressed_any(&[KeyCode::D, KeyCode::Right, KeyCode::E]) { + 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::Z, KeyCode::Semicolon], |_| { + .pressed_any(&[KeyCode::KeyZ, KeyCode::Semicolon], |_| { race_car.rotation += rotation_amount; }) - .pressed_any(&[KeyCode::C, KeyCode::J], |_| { + .pressed_any(&[KeyCode::KeyC, KeyCode::KeyJ], |_| { 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..7121d95 100644 --- a/examples/level_creator.rs +++ b/examples/level_creator.rs @@ -95,45 +95,45 @@ 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/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/extreme_drivers_ed.rs b/examples/scenarios/extreme_drivers_ed.rs index d2f8ace..0d749bd 100644 --- a/examples/scenarios/extreme_drivers_ed.rs +++ b/examples/scenarios/extreme_drivers_ed.rs @@ -854,24 +854,24 @@ 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 { 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, KeyO]) { 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, KeyE]) { rotation -= 1.0; } } diff --git a/examples/scenarios/road_race.rs b/examples/scenarios/road_race.rs index 57f330b..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 @@ -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; } @@ -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..9e58396 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -10,16 +10,12 @@ 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), + resolution: WindowResolution::new(800, 200), 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 + ..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/scenarios/car_shoot.md b/scenarios/car_shoot.md index e217221..061804d 100644 --- a/scenarios/car_shoot.md +++ b/scenarios/car_shoot.md @@ -76,7 +76,7 @@ In your [`game_logic(...)` function](https://cleancut.github.io/rusty_engine/25- 1. Set `game_state.spawn_timer` to a new `Timer` with a random value between `0.1` and `1.25` - Add the `rand` crate as a dependency in your `Cargo.toml` - Add `use rand::prelude::*;` to the top of your `main.rs` file - - Use `thread_rng().gen_range(0.1..1.25)` to obtain a random `f32` value between `0.1` and `1.25` + - Use `rand::rng().random_range(0.1..1.25)` to obtain a random `f32` value between `0.1` and `1.25` - [Create a non-repeating `Timer`](https://cleancut.github.io/rusty_engine/250-timer.html#creation) and assign it as the value to `game_state.spawn_timer` 1. If there are any cars left (check the value of `game_state.cars_left`), then: 1. Decrement `game_state.cars_left` by one @@ -84,10 +84,10 @@ In your [`game_logic(...)` function](https://cleancut.github.io/rusty_engine/25- - Set the `value` to `format!("Cars left: {}", game_state.cars_left)` 1. Create a label for the current car that starts with `car`: `format!("car{}", game_state.cars_left)` (remember, a label starting with `car` is what the movement code is looking for). 1. Create a vector of `SpritePreset`s of cars to randomly select from: `let car_choices = vec![SpritePreset::RacingCarBlack, SpritePreset::RacingCarBlue, SpritePreset::RacingCarGreen, SpritePreset::RacingCarRed, SpritePreset::RacingCarYellow];` - 1. Make a random sprite preset choice: `car_choices.iter().choose(&mut thread_rng()).unwrap().clone()` + 1. Make a random sprite preset choice: `car_choices.iter().choose(&mut rand::rng()).unwrap().clone()` 1. Actually create the sprite with the label and sprite preset selected above. Set the sprite's: - `translation.x` to `-740.0` - - `translation.y` to a random value from `-100.0` to `325.0` -- `thread_rng().gen_range(-100.0..325.0)` + - `translation.y` to a random value from `-100.0` to `325.0` -- `rand::rng().random_range(-100.0..325.0)` - `collision` to `true` so that the car will collide with marbles 1. Move cars right across the screen (in the positive X direction). The logic for this section is _very_ similar to the previous section that moved marbles. 1. Define a `CAR_SPEED` constant and set it to `250.0` diff --git a/scenarios/road_race.md b/scenarios/road_race.md index 586e97c..43441dd 100644 --- a/scenarios/road_race.md +++ b/scenarios/road_race.md @@ -173,8 +173,8 @@ Now it's time to add some obstacles. Interesting obstacles will be in random loc 1. Add a sprite with that preset and a label that starts with `"obstacle"`, and ends with the number value of `i`. (Use the `format!()` macro to construct the label string). 1. Set the sprite's `layer` to `5.0` so that the obstacle will be on top of road lines, but underneath the player. 1. set the sprite's `collision` to `true` so that it will generate collision events with the race car. - 1. Set the `x` location to a random value between `800.0` and `1600.0` using `thread_rng()` - * `sprite.translation.x = thread_rng().gen_range(800.0..1600.0);` + 1. Set the `x` location to a random value between `800.0` and `1600.0` using `rand::rng()` + * `sprite.translation.x = rand::rng().random_range(800.0..1600.0);` 1. Do the same for `y`, only between `-300.0` and `300.0` ```rust @@ -184,8 +184,8 @@ for (i, preset) in obstacle_presets.into_iter().enumerate() { 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); } ``` @@ -201,8 +201,8 @@ The obstacles need to move, too, so they appear to be on the road! In the `game 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/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 a47fde3..d961453 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); //! # } //! ``` //! @@ -242,34 +251,35 @@ 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_relative(volume), + volume: Volume::Linear(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() { + if let Ok((entity, music)) = music_query.single() { music.stop(); commands.entity(entity).despawn(); } // 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_relative(volume), + .spawn(( + AudioPlayer::(asset_server.load(format!("audio/{}", music))), + PlaybackSettings { mode: PlaybackMode::Loop, + volume: Volume::Linear(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 7728f95..43426ba 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,9 +1,9 @@ use bevy::{ app::AppExit, - prelude::{Text as BevyText, *}, + platform::collections::HashMap, + prelude::{Sprite as BevySprite, *}, time::Time, - utils::HashMap, - window::{close_on_esc, PrimaryWindow, WindowPlugin}, + window::{PrimaryWindow, WindowPlugin}, }; use bevy_prototype_lyon::prelude::*; use std::{ @@ -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 @@ -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(), - spatial: SpatialBundle::from_transform(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(), }); @@ -196,11 +188,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 +208,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_alignment(TextAlignment::Center), - transform, + Text2d(text_string), + TextFont { + font: asset_server.load(font_path), + font_size, ..Default::default() }, + TextColor(Color::WHITE), + TextLayout::new_with_justify(Justify::Center).with_no_wrap(), + transform, )); } } @@ -240,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()); @@ -308,7 +296,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 +324,7 @@ impl Game { )) //.insert_resource(ReportExecutionOrderAmbiguities) // for debugging .add_systems(Startup, setup); - self.app.world.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 +337,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); } @@ -366,19 +354,25 @@ fn game_logic_sync( keyboard_state: Res, mouse_state: Res, time: Res