diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e898f3..6a9b2e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Upgraded to Bevy 0.6 in the back end - `Text` rotation and scale now works! 🎉 - TODO: bevy_prototype_debug_lines hasn't had a release, and the `main` branch sorta works, but the lines now appear _under_ sprites instead of over them, which is not ideal. We _must_ have a release upstream and would _love_ a fix upstream. If there isn't an upstream release, we'll need to find another line-drawing solution before release. +- Updated (or finished) all of the game scenario descriptions. ## [3.0.0] - 2021-12-30 diff --git a/examples/collision.rs b/examples/collision.rs index a94e486..09f1e6e 100644 --- a/examples/collision.rs +++ b/examples/collision.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example collision + use rusty_engine::prelude::*; const ROTATION_SPEED: f32 = 3.0; diff --git a/examples/game_state.rs b/examples/game_state.rs index e2dcf41..97b120c 100644 --- a/examples/game_state.rs +++ b/examples/game_state.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example game_state + use std::f32::consts::TAU; use rusty_engine::prelude::*; diff --git a/examples/keyboard_events.rs b/examples/keyboard_events.rs index 9e4933d..68dcd2d 100644 --- a/examples/keyboard_events.rs +++ b/examples/keyboard_events.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example keyboard_events + use rusty_engine::prelude::*; fn main() { diff --git a/examples/keyboard_state.rs b/examples/keyboard_state.rs index 89d09d4..2569dd2 100644 --- a/examples/keyboard_state.rs +++ b/examples/keyboard_state.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example keyboard_state + use std::f32::consts::PI; use rusty_engine::prelude::*; diff --git a/examples/layer.rs b/examples/layer.rs index 6f181b7..5dbf879 100644 --- a/examples/layer.rs +++ b/examples/layer.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example layer + use rusty_engine::prelude::*; fn main() { diff --git a/examples/level_creator.rs b/examples/level_creator.rs index 2c7506a..3c1d75f 100644 --- a/examples/level_creator.rs +++ b/examples/level_creator.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example level_creator + use rusty_engine::prelude::*; struct GameState { diff --git a/examples/mouse_events.rs b/examples/mouse_events.rs index 1cc1eec..81baf77 100644 --- a/examples/mouse_events.rs +++ b/examples/mouse_events.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example mouse_events + use rusty_engine::prelude::*; const ORIGIN_LOCATION: (f32, f32) = (0.0, -200.0); diff --git a/examples/mouse_state.rs b/examples/mouse_state.rs index 1912fbd..5712d2c 100644 --- a/examples/mouse_state.rs +++ b/examples/mouse_state.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example mouse_state + use rusty_engine::prelude::*; const ORIGIN_LOCATION: (f32, f32) = (0.0, -200.0); diff --git a/examples/music.rs b/examples/music.rs index e0b6b15..ef19230 100644 --- a/examples/music.rs +++ b/examples/music.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example music + //! This is an example of playing a music preset. For playing your own music file, please see the //! `sound` example. diff --git a/examples/music_sampler.rs b/examples/music_sampler.rs index e2e6230..7b6a668 100644 --- a/examples/music_sampler.rs +++ b/examples/music_sampler.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example music_sampler + use rusty_engine::prelude::*; struct GameState { diff --git a/examples/scenarios/car_shoot.rs b/examples/scenarios/car_shoot.rs index 0b9956d..04d6013 100644 --- a/examples/scenarios/car_shoot.rs +++ b/examples/scenarios/car_shoot.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example car_shoot + use rand::prelude::*; use rusty_engine::prelude::*; use SpritePreset::*; // The SpritePreset enum was imported from rusty_engine::prelude @@ -5,13 +9,19 @@ use SpritePreset::*; // The SpritePreset enum was imported from rusty_engine::pr #[derive(Default)] struct GameState { marbles_left: Vec, - cars_left: Vec, + cars_left: i32, spawn_timer: Timer, } fn main() { let mut game = Game::new(); + // Set the title of the window to be Car Shooter + game.window_settings(WindowDescriptor { + title: "Car Shoot".into(), + ..Default::default() + }); + // Create the player let player = game.add_sprite("player", RacingBarrierRed); player.rotation = UP; @@ -19,12 +29,6 @@ fn main() { player.translation.y = -325.0; player.layer = 10.0; - // Set the Window Settings - game.window_settings(WindowDescriptor { - title: "Car Shooter".into(), - ..Default::default() - }); - // Start the music game.audio_manager.play_music(MusicPreset::Classy8Bit, 0.1); @@ -33,14 +37,10 @@ fn main() { // Marbles left. We'll use these strings as labels for sprites. If they are present in the // vector, then they are available to be shot out of the marble gun. If they are not present, // then they are currently in play. - for i in 0..3 { - game_state.marbles_left.push(format!("marble{}", i)); - } + game_state.marbles_left = vec!["marble1".into(), "marble2".into(), "marble3".into()]; // Cars left in level - each integer represents a car that will be spawned - for i in 0..25 { - game_state.cars_left.push(i); - } + game_state.cars_left = 25; let cars_left = game.add_text("cars left", "Cars left: 25"); cars_left.translation = Vec2::new(540.0, -320.0); @@ -53,24 +53,16 @@ const CAR_SPEED: f32 = 300.0; fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { // Handle marble gun movement - for event in engine_state.mouse_location_events.drain(..) { - let player = engine_state.sprites.get_mut("player").unwrap(); - player.translation.x = event.position.x; + let player = engine_state.sprites.get_mut("player").unwrap(); + if let Some(location) = engine_state.mouse_state.location() { + player.translation.x = location.x; } + let player_x = player.translation.x; // Shoot marbles! - for event in engine_state.mouse_button_events.clone() { - if !matches!(event.state, ElementState::Pressed) { - continue; - } + if engine_state.mouse_state.just_pressed(MouseButton::Left) { // Create the marble if let Some(label) = game_state.marbles_left.pop() { - let player_x = engine_state - .sprites - .get_mut("player") - .unwrap() - .translation - .x; let marble = engine_state.add_sprite(label, RollingBallBlue); marble.translation.y = -275.0; marble.translation.x = player_x; @@ -89,9 +81,18 @@ fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> boo marble.translation.y += MARBLE_SPEED * engine_state.delta_f32; } + // Move cars across the screen + for car in engine_state + .sprites + .values_mut() + .filter(|car| car.label.starts_with("car")) + { + car.translation.x += CAR_SPEED * engine_state.delta_f32; + } + // Clean up sprites that have gone off the screen let mut labels_to_delete = vec![]; - for sprite in engine_state.sprites.values_mut() { + for sprite in engine_state.sprites.values() { if sprite.translation.y > 400.0 || sprite.translation.x > 750.0 { labels_to_delete.push(sprite.label.clone()); } @@ -103,15 +104,6 @@ fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> boo } } - // Move cars across the screen - for car in engine_state - .sprites - .values_mut() - .filter(|car| car.label.starts_with("car")) - { - car.translation.x += CAR_SPEED * engine_state.delta_f32; - } - // Spawn cars if game_state .spawn_timer @@ -121,10 +113,11 @@ fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> boo // Reset the timer to a new value game_state.spawn_timer = Timer::from_seconds(thread_rng().gen_range(0.1..1.25), false); // Get the next car - if let Some(i) = game_state.cars_left.pop() { - let cars_left = engine_state.texts.get_mut("cars left").unwrap(); - cars_left.value = format!("Cars left: {}", i); - let label = format!("car{}", i); + if game_state.cars_left > 0 { + game_state.cars_left -= 1; + let label = format!("car{}", game_state.cars_left); + let cars_left_text = engine_state.texts.get_mut("cars left").unwrap(); + cars_left_text.value = format!("Cars left: {}", game_state.cars_left); let car_choices = vec![ RacingCarBlack, RacingCarBlue, @@ -152,8 +145,6 @@ fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> boo continue; } if !event.pair.one_starts_with("marble") { - // it's two cars spawning on top of each other, take one out - engine_state.sprites.remove(&event.pair.0); continue; } engine_state.sprites.remove(&event.pair.0); diff --git a/examples/scenarios/extreme_drivers_ed.rs b/examples/scenarios/extreme_drivers_ed.rs index 42e9c48..f4c3ba6 100644 --- a/examples/scenarios/extreme_drivers_ed.rs +++ b/examples/scenarios/extreme_drivers_ed.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example extreme_drivers_ed + use rusty_engine::prelude::*; struct GameState { diff --git a/examples/scenarios/road_race.rs b/examples/scenarios/road_race.rs index f9aa375..44706fb 100644 --- a/examples/scenarios/road_race.rs +++ b/examples/scenarios/road_race.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example road_race + use rand::prelude::*; use rusty_engine::prelude::*; use SpritePreset::*; // The SpritePreset enum was imported from rusty_engine::prelude diff --git a/examples/sfx.rs b/examples/sfx.rs index 0079da3..2a28650 100644 --- a/examples/sfx.rs +++ b/examples/sfx.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example sfx + //! This is an example of playing a sound effect preset. For playing your own sound effect file, //! please see the `sound` example. diff --git a/examples/sfx_sampler.rs b/examples/sfx_sampler.rs index e4ea3bb..214ab12 100644 --- a/examples/sfx_sampler.rs +++ b/examples/sfx_sampler.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example sfx_sampler + use rusty_engine::prelude::*; #[derive(Default)] diff --git a/examples/sound.rs b/examples/sound.rs index 88223c0..1f99717 100644 --- a/examples/sound.rs +++ b/examples/sound.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example sound + //! This is an example of playing sound by path. For playing music or sound effect presets, please //! see the `music` or `sfx` examples. diff --git a/examples/sprite.rs b/examples/sprite.rs index bd17874..4fb7762 100644 --- a/examples/sprite.rs +++ b/examples/sprite.rs @@ -1,4 +1,7 @@ -// +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example sprite + use rusty_engine::prelude::*; fn main() { diff --git a/examples/text.rs b/examples/text.rs index 9b7295a..8ed3909 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example text + use rusty_engine::prelude::*; struct GameState { diff --git a/examples/transform.rs b/examples/transform.rs index ba1a7fe..3d7933a 100644 --- a/examples/transform.rs +++ b/examples/transform.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example transform + use rusty_engine::prelude::*; fn main() { diff --git a/examples/window.rs b/examples/window.rs index ad095bc..b9fd19a 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -1,3 +1,7 @@ +//! To run this code, clone the rusty_engine repository and run the command: +//! +//! cargo run --release --example window + use rusty_engine::prelude::*; fn main() { diff --git a/scenarios/README.md b/scenarios/README.md index 2160fdd..f2c3209 100644 --- a/scenarios/README.md +++ b/scenarios/README.md @@ -47,12 +47,13 @@ Legend: | Easy | You will be told each step, and each section includes the code that you should have ended up with, and there is a complete reference project. | | Medium | You will be told each step, but won't be shown all the code. There might be a reference project. | | Hard | You will be told what to accomplish, and maybe be given a couple pointers. There is probably no reference project. | +| Insane | You'll need to implement some game engine features yourself | ## Scenarios - (Easy) [Road Race](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/road_race.md) - (Medium) [Car Shoot](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/car_shoot.md) -- (Medium) [Driver's Ed](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/car_shoot.md) +- (Medium) [Driver's Ed](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/extreme_drivers_ed.md) - (Hard) [Cannon Practice](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/cannon_practice.md) - (Hard) [Space Invaders](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/space_invaders.md) -- (Hard) [Labrinth](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/labrinth.md) +- (Insane) [Labrinth](https://github.com/CleanCut/rusty_engine/tree/main/scenarios/labrinth.md) - Rusty Engine doesn't yet provide all the features needed to implement this scenario. diff --git a/scenarios/cannon_practice.md b/scenarios/cannon_practice.md index 8655fd3..3a8607d 100644 --- a/scenarios/cannon_practice.md +++ b/scenarios/cannon_practice.md @@ -6,16 +6,66 @@ In this scenario you will create a cannon that sits on the bottom left side of t ## Common Setup -1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup) section of the scenarios readme to set up the skeleton of the project. +1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup-do-this-first) section of the scenarios readme to set up the skeleton of the project. + +## Game State & Constants + +1. Define a game state struct with fields for: + - The firing magnitude of the cannon (an `f32`) + - The rotation of the cannon (an `f32`) + - The current velocity of the cannon ball (a `Vec2`) +1. Define a constant for acceleration due to gravity. The unit will be pixels per second per second. +1. Decide on a sprite to use as a cannon ball +1. Decide on a sprite to use as the cannon +1. Decide on a sprite to use for the goal that you are trying to hit +1. Decide on a sprite (or sprites) to use for obstacles that you should avoid hitting ## Game Initialization In your `// setup goes here` section of `main()`... -1. +1. Create the initial game state struct with good starting values +1. Create and place the cannon, obstacles, and goal sprites. You may use the `level_creator` example to do this, if you wish. + - Place the cannon on the lower left side of the screen + - Use a field from the game state to set the rotation of the cannon + - Place a single obstacle in lower middle of the screen (so you have to fire over it) + - Place the goal on the lower right side of the screen +1. Create the text for displaying the firing magnitude of the cannon, place it in the top left corner of the screen. +1. If you want music, start it now. ## Gameplay Logic -In your `game_logic(...)` function... +In your [`game_logic(...)` function](https://cleancut.github.io/rusty_engine/25-game-logic-function.html)... + +1. Decide which keyboard/mouse input will control the rotation of the cannon, and implement rotating the cannon. + - Constrain the min/max angle of rotation to angles in the first quadrant (from straight up to straight right) with [the `.clamp` method](https://doc.rust-lang.org/std/primitive.f32.html#method.clamp) and the [`UP` and `RIGHT` constants](https://docs.rs/rusty_engine/latest/rusty_engine/#constants). +1. Decide which keyboard/mouse input will fire the cannon. Implement it so that pressing (whatever you chose) creates a cannon ball sprite, but only if one does not exist. Place it at the same coordinates as the cannon, but at a layer lower than the cannon so the cannon obscures it until it is out from underneath it. + - Play a sound when the cannon is fired. +1. Set the initial velocity `Vec2` for the cannon ball. This is fairly straightforward math: +```rust +let initial_cannonball_velocity = Vec2::new( + game_state.firing_magnitude * cannon.rotation.cos(), + game_state.firing_magnitude * cannon.rotation.sin(), +); +``` + +1. Move the cannon ball sprite by the amount stored in the game state's velocity field multiplied by `engine_state.delta_f32` each frame. At this point, you should be able to run the game, rotate the cannon, fire the cannon, and see the cannon move across the screen in a straight line. +1. Implement the gravity logic. Each frame, subtract (_gravity constant_ * `engine_state.delta_f32`) from the "Y" value of the cannon ball's velocity. +1. Decide which keyboard/mouse input will change the firing magnitude of the cannon, and implement it. + - Constrain the firing magnitude between `0.0` and some semi-reasonable value. + - Every time the firing magnitude changes, change the value of the `Text` that is displaying it in the top left corner of the screen. Don't change the value of the `Text` if the firing magnitude didn't change. +1. Detect collisions between the cannon ball and obstacles. The cannon ball should be destroyed if it hits an obstacle. + 1. For collisions to be detected between two sprites, the `.collision` field of _both_ sprites must be set to true. Set this field on the cannon ball, the obstacles, and the goal. + 1. Play a sound when the cannon ball hits an obstacle +1. Detect collisions between the cannon ball and the goal. The game is won if the cannon ball hits the goal. + 1. Play a sound when the cannon ball hits the goal. + + +## Challenge -1. +- Introduce constant wind that varies between shots +- Make the obstactle move, rotate, or scale dynamically to make it so you have to time your shot correctly as well +- Replace the `Text` displaying the magnitude of your starting velocity with a visual slider (literally slide a barrier from the edge of the screen to some pre-defined point) +- Make destructible obstactles that reduce the cannon ball's velocity by half in the X direction +- Allow the cannon to move a small distance in the +/- X direction +- Add scorekeeping and alter the layout of the obstacles each time the cannon hits the goal diff --git a/scenarios/car_shoot.md b/scenarios/car_shoot.md index bd24ac1..5c6d7c6 100644 --- a/scenarios/car_shoot.md +++ b/scenarios/car_shoot.md @@ -2,36 +2,124 @@ Cars are floating past. Shoot them down! -You are at a carnival booth. Cars float across the back of the booth, occasionally obscured by obstacles. The player uses their marble gun to shoot down as many of the cars as possible before the time runs out. +You are at a carnival booth. Cars float across the back of the booth. The player uses their marble gun to shoot down as many of the cars as possible before the time runs out. This scenario can be extended to 2 players. +- [Reference Code](https://github.com/CleanCut/rusty_engine/blob/main/examples/scenarios/car_shoot.rs) + +https://user-images.githubusercontent.com/5838512/147995928-4705d9fc-c3fa-41b7-901f-d120307e455f.mp4 + ## Common Setup -1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup) section of the scenarios readme to set up the skeleton of the project. +1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup-do-this-first) section of the scenarios readme to set up the skeleton of the project. + +## Engine Initialization + +1. Define a `GameState` struct with the following fields: + - `marbles_left` - a vector of strings. These will be labels for our marble sprites. + - `cars_left` - an integer tracking how many cars are left to spawn + - `spawn_timer` - a timer indicating when it's time to spawn another car +1. Add `GameState` to your `init!` call + +## Game Setup -## Game Initialization -In your `// setup goes here` section of `main()`... +In your `// game setup goes here` section of `main`... -1. +1. (Optional) Set the [window title](https://cleancut.github.io/rusty_engine/450-game.html#window-settings) to be `Car Shooter` +1. (Optional) [Play some music.](https://cleancut.github.io/rusty_engine/205-music.html#play) We recommend the music preset `MusicPreset::Classy8Bit` at a volume of `0.1`. +1. [Create a player sprite.](https://cleancut.github.io/rusty_engine/55-sprite-creation.html) The player will be represented by a rectangle that represents the barrel of the marble gun. We'll use `SpritePreset::RacingBarrierRed` +1. We'll pretend the player is standing off the bottom of the screen, and only the barrel of their gun is visible. Let's place the sprite accordingly--set the sprite's: + - `rotation` to `UP` so it is pointing towards the top of the screen + - `scale` to `0.5` so it's about the right size + - `translation.y` to `-325.0` so it goes off the bottom of the screen a little + - `layer` to `10.0` so it will be on top of the marble which will be at a lower layer +1. Create an instance of your `GameState` struct, and set: + - `marbles_left` to the following vector of strings: `vec!["marble1".into(), "marble2".into(), "marble3".into()]` - we'll pop these off to use as labels for marble sprites, and then push them back on the vector when the marble sprites get destroyed. + - `cars_left` to a reasonable number such as `25` + - `spawn_timer` should be a `Timer` set to `0.0` seconds so that it goes off immediately. +1. Pass your game state variable to [`game.run()`](https://cleancut.github.io/rusty_engine/450-game.html#running-the-game) +1. [Create a `Text`](https://cleancut.github.io/rusty_engine/155-text-creation.html) with the label `"cars left"` that displays how many cars are left to spawn, with: + - the `value` of `format!("Cars left: {}", game_state.cars_left);` + - the `translation` of `Vec2::new(540.0, -320.0);` to put it in the bottom right corner of the screen. -## Gameplay Logic +## Game Logic -In your `game_logic(...)` function... +In your [`game_logic(...)` function](https://cleancut.github.io/rusty_engine/25-game-logic-function.html)... -1. +1. Have the "gun barrel" follow the mouse on the X axis by set the `translation.x` of the player sprite to the `x` value of the mouse location. + - Get a [mutable reference to the player sprite](https://cleancut.github.io/rusty_engine/60-sprite-transform.html#adjusting-an-existing-sprite) + - Get the mouse location via the [mouse state's `location` method](https://cleancut.github.io/rusty_engine/115-mouse-state.html#location) + - Make a variable `player_x` and set it to the player's current `translation.x` so we can use it later on: `let player_x = player.translation.x;` + +screenshot1 + +1. If the [left mouse button was just pressed](https://cleancut.github.io/rusty_engine/115-mouse-state.html#mouse-buttons), then: + 1. [If there is](https://doc.rust-lang.org/book/ch06-03-if-let.html) a label string [left in the `game_state.marbles_left` vector](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.pop), then: + - Using the label value, create a new marble sprite using `SpritePreset::RollingBallBlue` + - Make sure the label has been removed from the `game-state.marbles_left` vector. This way, we can only have as many marbles on thes screen as there are labels to remove from the vector. (We'll add the label back to the vector when we've finished with the marble). + - Set the marble sprite's: + - `translation.x` to the player's x location that we put in our `player_x` variable. + - `translation.y` to `-275.0`, which will put the marble under the end of the gun. + - `layer` to `5.0`, which will put it underneath the gun sprite + - `collision` to `true` so that we can detect collisions between the marble and the cars. + - [Play a sound effect](https://cleancut.github.io/rusty_engine/210-sfx.html#play) to indicate the firing of the marble. We suggest the sound effect preset `SfxPreset::Impact2` at a volume of `0.7`. +1. Move the marbles upwards (in the positive Y direction) + 1. Define a `MARBLE_SPEED` [constant](https://doc.rust-lang.org/std/keyword.const.html) (probably out in the module level) for how fast your marble should move and set it to the `f32` value of `600.0`. + 1. Loop through all the marble sprites (the sprites whose labels [start with](https://doc.rust-lang.org/std/string/struct.String.html#method.starts_with) `"marble"`), for each of them: + - increment the marble sprite's `translation.y` by `MARBLE_SPEED * engine_state.delta_f32` +1. Move cars right across the screen (in the positive X direction). No, we don't have any cars yet, but once we spawn them this code will move them! 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` + 1. Loop through all the car sprites (the sprites whose labels start with `"car"`), for each of them: + - increment the car sprite's `translation.x` by `CAR_SPEED * engine_state.delta_f32` +1. Clean up sprites that have moved off the top or the right side of the screen. + 1. We can't modify a hash map of sprites while we're looping through its values, so let's create an empty vector of strings and fill it with labels of sprites that we want to delete. Once we're done examining the hash map, we can loop through the vector of labels and remove those hash map entries. + 1. Create a new vector `labels_to_delete` + 1. For every sprite [value in the hash map](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.values): + - check to see if either the `translation.y > 400.0` or the `translation.x > 750.0`. If either of those conditions are true, push a clone of the label onto the `labels_to_delete` vector. + 1. For every label in `labels_to_delete`: + - [Remove the sprite entry.](https://cleancut.github.io/rusty_engine/60-sprite-transform.html#deleting-a-sprite) The hash map's `remove` method takes an immutable reference to the key type, so if you are looping through the label strings by value, you may need to add a `&` in front of your label variable: `engine_state.sprites.remove(&label)` +1. Spawn a car if the `game_state.spawn_timer` just finished! So [tick the spawn timer and check to see if it just finished](https://cleancut.github.io/rusty_engine/250-timer.html#counting-down--finishing) -- if it did, then: + 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` + - [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 + 1. [Retrieve a mutable reference to the](https://cleancut.github.io/rusty_engine/165-text-transform.html#adjusting-an-existing-text) `Text` we labeled `"cars left"` + - 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. 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)` + - `collision` to `true` so that the car will collide with marbles +1. Now it's time to handle the collisions! For each [`CollisionEvent`](https://docs.rs/rusty_engine/latest/rusty_engine/physics/struct.CollisionEvent.html) in `engine_state.collision_events`: + - We only care about the start of collisions, not the ending of them, so if `event.state.is_end()`, then `continue` the loop. + - Similarly, if one of the event pair's labels _doesn't_ start with `"marble"`, then it's either two marbles or two cars colliding with each other, which we don't care about. So if `!event.pair.one_starts_with("marble")`, then `continue` the loop. + - At this point we know that one of the pair is a marble and the other is a car, and they both need to be removed. So using the labels in the `event.pair` tuple, [delete both sprites](https://cleancut.github.io/rusty_engine/60-sprite-transform.html#deleting-a-sprite). + - Now that a marble has been "destroyed", we are allowed to shoot it from the gun again, so grab whichever label of the `event.pair` tuple that starts with `"marble"` and [push](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.push) a clone of it back onto the `game_state.marbles_left` vector. + - [Play a sound effect](https://cleancut.github.io/rusty_engine/210-sfx.html#play) for successfully hitting a car with a marble. Use `SfxPreset::Confirmation1` with a volume of `0.5` + + +You made it to the end of the main scenario! You should have a playable game prototype by this point. + +screenshot2 # Challenges * Keep track of points, display the points in a corner of the screen -* Different types of marbles -* Limited ammount of marbles -* Powerups! Powerups float across like cars +* Make it so that after the game ends, you can press a key and start a new game +* Keep track of the high score across games and display it when the game ends +* Don't allow cars to spawn on top of other cars +* Powerups! Powerups float across like cars and activate when hit. * Spread-fire * Rapid-fire * Explosion - clear the screen * Make the movement of the cars more interesting - have them drive in curvy motions * Smart black cars - black cars sometimes slow down or speed up so a shot will miss them * Armored cars - Green cars take two marbles to take down - +* Add support for a second player, with separate scores for each player. You'll need to figure out some way for the second player to control their marble gun... diff --git a/scenarios/drivers_ed.md b/scenarios/drivers_ed.md deleted file mode 100644 index ed3dec4..0000000 --- a/scenarios/drivers_ed.md +++ /dev/null @@ -1,23 +0,0 @@ -# Extreme Driver's Education - -Can you survive your driving exam? - -The screen represents a driving course full of obstacles. Carefully avoid the obstacles while driving your car around to collect all of the rewards. Only a master driver will pass this test. - -This scenario can be extended to 2 players. - -## Common Setup - -1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup) section of the scenarios readme to set up the skeleton of the project. - -## Game Initialization - -In your `// setup goes here` section of `main()`... - -1. - -## Gameplay Logic - -In your `game_logic(...)` function... - -1. diff --git a/scenarios/extreme_drivers_ed.md b/scenarios/extreme_drivers_ed.md new file mode 100644 index 0000000..a93e514 --- /dev/null +++ b/scenarios/extreme_drivers_ed.md @@ -0,0 +1,39 @@ +# Extreme Driver's Education + +Can you survive your driving exam? + +Navigate a driving course full of obstacles. Carefully avoid the obstacles while driving your car around to collect all of the white circles. Only a master driver will pass this test. + +- [Reference Code](https://github.com/CleanCut/rusty_engine/blob/main/examples/scenarios/extreme_drivers_ed.rs) + +## Common Setup + +1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup-do-this-first) section of the scenarios readme to set up the skeleton of the project. + +## Level Setup + +It can be _really_ tedious to set up dozens of obstacles via code and guessing coordinates. Instead, clone the `rusty_engine` repository and use the `level_creator` example to place several dozen obstacles and emit the level code for you to copy-and-paste into your own project. + +The sprite preset `SpritePreset::RollingHoleStart` are the goals for collecting (you _want_ to run into them). All other sprites will be obstacles. + +``` +git clone https://github.com/CleanCut/rusty_engine.git +cd rusty_engine +cargo run --release --example level_creator +``` + +## Engine Initialization + +- + +## Game Setup + +In your `// game setup goes here` section of `main`... + +1. + +## Game Logic + +In your [`game_logic(...)` function](https://cleancut.github.io/rusty_engine/25-game-logic-function.html)... + +1. diff --git a/scenarios/labrinth.md b/scenarios/labrinth.md index a11fc71..e328506 100644 --- a/scenarios/labrinth.md +++ b/scenarios/labrinth.md @@ -2,20 +2,68 @@ Guide the marble from the beginning to the end of the labrinth...but don't fall in any holes! +NOTE: This scenario is not fully supported by the capabilities of the engine. You will need to supplement the engine with your own physics logic and/or make changes to the engine itself to accomplish this complete scenario. This is included here because we _might_ add enough features to support this scenario in the future. You're certainly welcome to help! + This game consists of a [Labrinth](https://en.wikipedia.org/wiki/Labyrinth) or maze with a beginning and an end. The marble starts at the beginning of the labrynth (naturally) and must proceed to the end. Sounds easy...until you realize that there are holes all along the maze, and you don't have perfect control of the marble! If you fall in one of the holes, start over from the beginning. ## Common Setup -1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup) section of the scenarios readme to set up the skeleton of the project. +1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup-do-this-first) section of the scenarios readme to set up the skeleton of the project. + +## Game State + +1. Define a game state struct with fields for: + - Current tilt of the labrinth (a `Vec2`) + - Current velocity of the marble (a `Vec2`) + - Lives left (a `u8`) +1. Define constants for: + - Marble movement speed (an `f32`) + - Maximum tilt magnitude (an `f32`) + - Maximum marble speed (an `f32`) +1. Choose a sprite to represent the player's marble +1. Choose a sprite to represent the starting area or spot +1. Choose a sprite to represent holes in the labrinth +1. Choose a sprite to represent the ending area or spot +1. Choose a sprite to represent walls of the labrinth ## Game Initialization In your `// setup goes here` section of `main()`... -1. +1. Use the `level_creator` example to create a labrinth (maze) with the sprite you selected for walls. + - Place "holes" as obstacles to avoid. + - Place one "starting area" sprite, where the marble will start on top of + - Place one "ending area" sprite, which will signal winning the game when touched + - Save out the game, copy and paste the sprite positioning code into your `main.rs` +1. Create the player's marble sprite and place it at the same coordinates as the "starting area" sprite, but at a high layer so it will be on top of any sprites it overlaps. +1. If you would like music, start playing it now. ## Gameplay Logic -In your `game_logic(...)` function... +In your [`game_logic(...)` function](https://cleancut.github.io/rusty_engine/25-game-logic-function.html)... + +1. We will move the marble by virtually tilting the whole labrinth (even though it won't look like we're tilting it). The relative movement of the mouse will do the tilting. The more tilted the labrinth is, the faster the marble will accelerate in that direction. + - Collect [mouse movement events](https://cleancut.github.io/rusty_engine/120-mouse-events.html#mouse-motion-events) (not location events!) and add them to the current tilt of the labrinth + - Clamp the maximum length of the tilt `Vec2` to the maximum tilt magnitude constant with [the `.clamp_length_max` method](https://docs.rs/glam/latest/glam/f32/struct.Vec2.html#method.clamp_length_max). +1. Accelerate the marble. + - Each frame, multiply the tilt `Vec2` in the game state by the movement speed constant AND `engine_state.delta_f32`, and then add that resulting `Vec2` to the marble velocity in the game state. Clamp the maximum length of the marble velocity using the maximum marble velocity constant with [the `.clamp_length_max` method](https://docs.rs/glam/latest/glam/f32/struct.Vec2.html#method.clamp_length_max). + - Each frame, increment the marble sprite's translation by the velocity in the game state. + - At this point, you should be able to get the marble to move around the screen (though it ignores all the other sprites). Play with all the constant values until you get something that feels reasonable. For the game to be playable, you'll need a decently large max tilt magnitude paired with a relatively small movement speed and small max movement speed to give you enough control over the marble. But you don't want _too_ much control...it should feel like rolling a marble on a flat surface by tilting it. + +## The sort of unfinished part + +This section isn't well-supported by the underlying engine. 😬 Sorry. + +1. Make it so that you can't go through your barriers. + - Missing engine feature: Collision contact normals. +1. Make it so that if the center of the marble overlaps a hole, you lose. + - Missing engine feature: Testing if an arbitrary `Vec2` is within a sprite's collider. + +## The rest + +1. When you touch the goal, you win! + + +## Challenges -1. +- When you fall down a hole, reset the game nicely and keep playing, keeping track of number of tries. diff --git a/scenarios/space_invaders.md b/scenarios/space_invaders.md index 5239733..55fb6ee 100644 --- a/scenarios/space_invaders.md +++ b/scenarios/space_invaders.md @@ -6,7 +6,7 @@ Similar to the classic [Space Invaders](https://en.wikipedia.org/wiki/Space_Inva ## Common Setup -1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup) section of the scenarios readme to set up the skeleton of the project. +1. Follow the instructions in the [Common Setup](https://github.com/CleanCut/rusty_engine/tree/main/scenarios#common-setup-do-this-first) section of the scenarios readme to set up the skeleton of the project. ## Game Initialization @@ -16,6 +16,6 @@ In your `// setup goes here` section of `main()`... ## Gameplay Logic -In your `game_logic(...)` function... +In your [`game_logic(...)` function](https://cleancut.github.io/rusty_engine/25-game-logic-function.html)... 1.