diff --git a/.github/workflows/tutorial.yml b/.github/workflows/tutorial.yml new file mode 100644 index 0000000..d28ae50 --- /dev/null +++ b/.github/workflows/tutorial.yml @@ -0,0 +1,32 @@ +name: Publish Tutorial +on: + push: + branches: + - main +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: install stable Rust toolchain + id: install_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + # Don't use a 'components:' entry--we don't need them with beta/nightly, plus nightly often doesn't have them + override: true + + - name: install mdbook + run: cargo install mdbook + + - name: build tutorial 🔧 + run: mdbook build tutorial + + - name: Deploy to GitHub Pages 🚀 + uses: JamesIves/github-pages-deploy-action@4 + with: + branch: gh-pages # The branch the action should deploy to. + folder: tutorial/book # The folder the action should deploy. diff --git a/.gitignore b/.gitignore index 74ae49f..af517d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +.cargo/config.toml .vscode **/*.rs.bk **/target Cargo.lock -.cargo/config.toml +tutorial/book diff --git a/CHANGELOG.md b/CHANGELOG.md index f9cd5a0..fb28343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,20 +34,23 @@ run. Return `false` to abort running any later functions during the frame. - `Sprite::write_collider` has been added (see note below about changes to colliders) - `TextActor` has been renamed to `Text` to eliminate the confusing "actor" terminology. - `TextActor.text` is now `Text.value` for similar reasons. -- `Sprite`s may now be created with either a `SpritePreset` or the path to an image file via both `Sprite::new` or `EngineState::add_sprite` +- `Sprite`s may now be created with either a `SpritePreset` or the path to an image file via both `Sprite::new` or `EngineState::add_sprite`. The image file needs to be stored in `assets/sprite` or one of its subdirectories. When specifying the path to the file, the path relative to `assets/sprite` should be used. For example, if your image is `assets/sprite/circus/animal.png` then you would pass `circus/animal.png` to one of the methods to create the sprite. - `SpritePreset::build_from_name` and `SpritePreset::build` have been removed (see note above about the new, more flexible way to create sprites) - `SpritePreset::collider()` has been removed since colliders are no longer hard-coded features of presets (see note below about changes to colliders) - `SpritePreset::filename -> String` has been replaced by `SpritePreset::filepath -> PathBuf`, which powers the `impl From for PathBuf` implementation. - Colliders are now loaded from collider files. Collider files use the [Rusty Object Notation (RON)](https://github.com/ron-rs/ron) format. The easiest way to create a collider is to run the `collider_creator` example by cloning the `rusty_engine` repository and running `cargo run --release --example collider_creator relative/path/to/my/image.png`. The image needs to be somewhere inside the `assets/` directory. You could also create the collider programmatically, set it on the `Sprite` struct, and call the sprite's `write_collider()` method. Or you could copy an existing collider file, name it the same as your image file (but with the `.collider` extension) and change it to match your image. Collider coordinates need to define a convex polygon with points going in clockwise order. Coordinates are floating point values relative to the center of the image, with the center of the image being (0.0, 0.0). - All sprites' colliders in the asset pack have been recreated more cleanly using the new `collider_creator` example. +- The `assets/fonts` directory in the asset pack has been renamed to `assets/font` for consistency with the other directories. +- `KeyboardState` and `MouseState` now both have 6 similar methods for processing key- and button-presses: + - `pressed` -> `pressed_any` + - `just_pressed` -> `just_pressed_any` + - `just_released` -> `just_released_any` ### Other Changes - `AudioManager::music_playing()` will return whether or not music is currently playing (accessible through `EngineState:audio_manager`) -- Custom fonts may now be set on a `Text` at creation time. -`EngineState::add_text_with_font` was added for a convenience. The font specified should be -a `.ttf` or `.otf` file stored in `assets/fonts` +- A custom font may now be selected by placing it in `assets/font` and specifying the relative filepath on `Text.font`. - Custom sounds may now be played via `AudioManager::play_music` and `AudioManager::play_sfx` by specifying a path to a sound file relative to `assets/audio`. - `Collider` now implements `PartialEq`, `Serialize`, and `Deserialize` diff --git a/README.md b/README.md index 7370690..1e5c517 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ If you like Rusty Engine, please sponsor me [on GitHub] or [on Patreon], or [tak # Rusty Engine -Rusty Engine is a simple, 2D game engine for those who are learning Rust. Create simple game prototypes using straightforward Rust code without any advanced game engine concepts! It works on macOS, Linux, and Windows. Rusty Engine is a simplification wrapper over [Bevy], which I encourage you to use directly for more serious game engine needs. +Rusty Engine is a simple, 2D game engine for those who are learning Rust. Create simple game prototypes using straightforward Rust code without needing to learning difficult game engine concepts! It works on macOS, Linux, and Windows. Rusty Engine is a simplification wrapper over [Bevy], which I encourage you to use directly for more serious game engine needs. [Questions], [bug reports], and contributions are most welcome! @@ -10,22 +10,26 @@ https://user-images.githubusercontent.com/5838512/122880590-651bae00-d2f7-11eb-8 ## Features +- Asset pack included (sprites, music, sound effects, and fonts) - Sprites (2D images) - - Includes 2 built-in asset packs + - Use sprites from the included asset pack, or bring your own + - Collision detection with custom colliders - Audio (music & sound effects) - - Includes 2 built-in asset packs -- Collision detection + - Looping music + - Multi-channel sound effects - Text - - Includes 2 built-in fonts + - 2 fonts included, or bring your own - Input handling (keyboard, mouse) - Timers +- Custom game state +- Window customization ## Courses -I teach courses which use this game engine: +The following courses use Rusty Engine in their curriculum: -- `Ultimate Rust 2: Intermediate Concepts` on Udemy, etc. Coming soon! -- [Rust in 3 Weeks](https://agileperception.com) conducted live on O'Reilly Online. +- [Ultimate Rust 2: Intermediate Concepts](https://www.udemy.com/course/ultimate-rust-2/?referralCode=8ED694EBE5637F954414) on Udemy (the sequel to [Ultimate Rust Crash Course](https://www.udemy.com/course/ultimate-rust-crash-course/?referralCode=AF30FAD8C6CCCC2C94F0)) +- [Rust in 3 Weeks](https://agileperception.com) conducted live on O'Reilly Online approximately once each quarter. ## Linux Dependencies (Including WSL 2) diff --git a/assets/fonts/FiraMono-Medium.ttf b/assets/font/FiraMono-Medium.ttf similarity index 100% rename from assets/fonts/FiraMono-Medium.ttf rename to assets/font/FiraMono-Medium.ttf diff --git a/assets/fonts/FiraSans-Bold.ttf b/assets/font/FiraSans-Bold.ttf similarity index 100% rename from assets/fonts/FiraSans-Bold.ttf rename to assets/font/FiraSans-Bold.ttf diff --git a/examples/collider_creator.rs b/examples/collider_creator.rs index 4b5dc17..9da030c 100644 --- a/examples/collider_creator.rs +++ b/examples/collider_creator.rs @@ -21,16 +21,21 @@ fn main() { let args = std::env::args().skip(1).collect::>(); if args.len() != 1 { println!( - "Please pass in the path of an image inside the assets directory! For example:\n\ - cargo run --release --example collider_creator -- sprite/racing/car_green.png" + "Please pass in the path of an image inside the `assets/sprite` directory! For example:\n\ + cargo run --release --example collider_creator -- racing/car_green.png" ); std::process::exit(1); } - // Convert the string to an actual path, and make sure it exists + // If the user passed in `assets/sprite/something...` then we need to strip `assets/` (the asset loader will prepend `assets/`) let mut path = PathBuf::from(args[0].clone()); - if path.starts_with("assets") { - path = path.strip_prefix("assets").unwrap().to_path_buf(); + if path.starts_with("assets/sprite") { + path = path + .strip_prefix("assets") + .unwrap() + .strip_prefix("sprite") + .unwrap() + .to_path_buf(); } if !(PathBuf::from("assets").join(&path)).exists() { println!("Couldn't find the file {}", path.to_string_lossy()); diff --git a/examples/collision.rs b/examples/collision.rs index 40a19b1..9b2db07 100644 --- a/examples/collision.rs +++ b/examples/collision.rs @@ -41,11 +41,11 @@ fn main() { fn logic(engine_state: &mut EngineState, _: &mut ()) -> bool { // If a collision event happened last frame, print it out and play a sound - for event in engine_state.collision_events.drain(..) { + for collision_event in engine_state.collision_events.drain(..) { let text = engine_state.texts.get_mut("collision text").unwrap(); - match event.state { + match collision_event.state { CollisionState::Begin => { - text.value = format!("{:?}", event.pair); + text.value = format!("{:?}", collision_event.pair); engine_state.audio_manager.play_sfx(SfxPreset::Switch1, 1.0) } CollisionState::End => { diff --git a/examples/music.rs b/examples/music.rs index 8165c8d..f891d4f 100644 --- a/examples/music.rs +++ b/examples/music.rs @@ -13,12 +13,12 @@ fn main() { ); msg.translation.y = 50.0; - let msg2 = game.add_text_with_font( + let msg2 = game.add_text( "msg2", "engine_state.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0);", - "FiraMono-Medium.ttf", ); msg2.translation.y = -50.0; + msg2.font = "FiraMono-Medium.ttf".to_string(); game.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0); diff --git a/examples/sfx.rs b/examples/sfx.rs index 7352e98..91f33c4 100644 --- a/examples/sfx.rs +++ b/examples/sfx.rs @@ -13,12 +13,12 @@ fn main() { ); msg.translation.y = 50.0; - let msg2 = game.add_text_with_font( + let msg2 = game.add_text( "msg2", "engine_state.audio_manager.play_sfx(SfxPreset::Jingle1, 1.0);", - "FiraMono-Medium.ttf", ); msg2.translation.y = -50.0; + msg2.font = "FiraMono-Medium.ttf".to_string(); game.audio_manager.play_sfx(SfxPreset::Jingle1, 1.0); diff --git a/examples/sound.rs b/examples/sound.rs index afd385a..4877a1b 100644 --- a/examples/sound.rs +++ b/examples/sound.rs @@ -13,12 +13,12 @@ fn main() { ); msg.translation.y = 100.0; - let msg2 = game.add_text_with_font( + let msg2 = game.add_text( "msg2", "engine_state.audio_manager.play_sfx(\"sfx/congratulations.ogg\", 1.0);", - "FiraMono-Medium.ttf", ); msg2.translation.y = -100.0; + msg2.font = "FiraMono-Medium.ttf".to_string(); game.audio_manager.play_sfx("sfx/congratulations.ogg", 1.0); diff --git a/examples/text.rs b/examples/text.rs index 7867f15..ef56edd 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -8,8 +8,9 @@ rusty_engine::init!(GameState); fn main() { let mut game = Game::new(); - let fps = game.add_text_with_font("fps", "FPS: ", "FiraMono-Medium.ttf"); + let fps = game.add_text("fps", "FPS: "); fps.translation = Vec2::new(0.0, 250.0); + fps.font = "FiraMono-Medium.ttf".to_string(); fps.font_size = 60.0; let zoom_msg = game.add_text( @@ -19,12 +20,12 @@ fn main() { zoom_msg.font_size = 35.0; zoom_msg.translation = Vec2::new(0.0, 150.0); - let font_msg = game.add_text_with_font( + let font_msg = game.add_text( "font_msg", - "You can choose a font at creation time by providing the filename of a font stored in assets/fonts.\n\"FiraSans-Bold.ttf\" is the default. \"FiraMono-Medium.ttf\" is also included in the asset pack.", - "FiraMono-Medium.ttf", + "You can choose a font at creation time by providing the filename of a font stored in assets/font.\n\"FiraSans-Bold.ttf\" is the default. \"FiraMono-Medium.ttf\" is also included in the asset pack." ); font_msg.font_size = 20.0; + font_msg.font = "FiraMono-Medium.ttf".to_string(); font_msg.translation.y = 0.0; let msg = game.add_text("msg", "Changing the text's translation, rotation*, and scale* is fast,\n so feel free to do that a lot."); diff --git a/release.toml b/release.toml index 911a242..c4b401e 100644 --- a/release.toml +++ b/release.toml @@ -4,6 +4,7 @@ tag-message = "{{tag_name}}" tag-name = "{{prefix}}v{{version}}" pre-release-replacements = [ { file = "README.md", search = "rusty_engine = \".*\"", replace = "rusty_engine = \"{{version}}\"", exactly = 1 }, + { file = "tutorial/src/05-config.md", search = "rusty_engine = \".*\"", replace = "rusty_engine = \"{{version}}\"", exactly = 1 }, { file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}", min = 1 }, { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", exactly = 1 }, { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}", min = 1 }, diff --git a/src/game.rs b/src/game.rs index 57ede46..d1c9fe4 100644 --- a/src/game.rs +++ b/src/game.rs @@ -39,9 +39,7 @@ pub struct EngineState { /// [`add_sprite`](EngineState::add_sprite) method. Modify & remove sprites as you like. pub sprites: HashMap, /// SYNCED - The state of all texts this frame. For convenience adding a text, use the - /// [`add_text`](EngineState::add_text) or - /// [`add_text_with_font`](EngineState::add_text_with_font) methods. Modify & remove - /// text as you like. + /// [`add_text`](EngineState::add_text) method. Modify & remove text as you like. pub texts: HashMap, /// SYNCED - If set to `true`, the game exits. Note: the current frame will run to completion first. pub should_exit: bool, @@ -91,7 +89,7 @@ pub struct EngineState { pub time_since_startup_f64: f64, /// A struct with methods to play sound effects and music pub audio_manager: AudioManager, - /// INFO - Window dimensions in pixels + /// INFO - Window dimensions in logical pixels pub window_dimensions: Vec2, } @@ -132,31 +130,6 @@ impl EngineState { // Unwrap: Can't crash because we just inserted the text self.texts.get_mut(&label).unwrap() } - - #[must_use] - /// Add a [`Text`] with a specific font. Use the `&mut Text` that is returned to set the - /// translation, rotation, etc. Use a unique label for each text. Attempting to add two texts - /// with the same label will crash. The `font` parameter should be the filename of a font - /// located in the assets/fonts directory. The default font is "FiraSans-Bold.ttf". - pub fn add_text_with_font(&mut self, label: L, text: T, font: F) -> &mut Text - where - L: Into, - T: Into, - F: Into, - { - let label = label.into(); - let text = text.into(); - let font = font.into(); - let curr_text = Text { - label: label.clone(), - value: text, - font, - ..Default::default() - }; - self.texts.insert(label.clone(), curr_text); - // Unwrap: Can't crash because we just inserted the sprite - self.texts.get_mut(&label).unwrap() - } } // startup system - grab window settings, initialize all the starting sprites @@ -181,7 +154,7 @@ pub fn add_sprites( ) { for (_, sprite) in engine_state.sprites.drain() { let transform = sprite.bevy_transform(); - let texture_handle = asset_server.load(sprite.filepath.clone()); + let texture_handle = asset_server.load(PathBuf::from("sprite").join(&sprite.filepath)); commands.spawn().insert(sprite).insert_bundle(SpriteBundle { material: materials.add(texture_handle.into()), transform, @@ -202,7 +175,7 @@ pub fn add_texts( let transform = text.bevy_transform(); let font_size = text.font_size; let text_string = text.value.clone(); - let font_path = format!("fonts/{}", text.font); + let font_path = format!("font/{}", text.font); commands.spawn().insert(text).insert_bundle(Text2dBundle { text: BevyText::with_section( text_string, @@ -527,6 +500,11 @@ fn game_logic_sync( if text.font_size != bevy_text_component.sections[0].style.font_size { bevy_text_component.sections[0].style.font_size = text.font_size; } + let font_path = format!("font/{}", text.font); + let font = asset_server.load(font_path.as_str()); + if bevy_text_component.sections[0].style.font != font { + bevy_text_component.sections[0].style.font = font; + } } else { commands.entity(entity).despawn(); } diff --git a/src/mouse.rs b/src/mouse.rs index 2d85d55..6813b24 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -87,17 +87,17 @@ impl MouseState { pub fn just_released(&self, mouse_button: MouseButton) -> bool { self.just_released.contains(&mouse_button) } - /// Returns an iterator over all the mouse buttons were being pressed at the end of the last frame - pub fn get_pressed(&self) -> impl ExactSizeIterator { - self.pressed.iter() + /// Returns true if any of the indicated mouse buttons were pressed + pub fn pressed_any(&self, mouse_buttons: &[MouseButton]) -> bool { + mouse_buttons.iter().any(|k| self.pressed(*k)) } - /// Returns an iterator over all the mouse buttons that started being pressed at the end of the last frame - pub fn get_just_pressed(&self) -> impl ExactSizeIterator { - self.just_pressed.iter() + /// Returns true if any of the indicated mouse buttons were just pressed this frame + pub fn just_pressed_any(&self, mouse_buttons: &[MouseButton]) -> bool { + mouse_buttons.iter().any(|k| self.just_pressed(*k)) } - /// Returns an iterator over all the mouse buttons that started being released at the end of the last frame - pub fn get_just_released(&self) -> impl ExactSizeIterator { - self.just_released.iter() + /// Returns true if any of the indicated mouse buttons were just released this frame + pub fn just_released_any(&self, mouse_buttons: &[MouseButton]) -> bool { + mouse_buttons.iter().any(|k| self.just_released(*k)) } } diff --git a/src/sprite.rs b/src/sprite.rs index e665eec..bc20f43 100644 --- a/src/sprite.rs +++ b/src/sprite.rs @@ -61,7 +61,7 @@ impl Sprite { let filepath = file_or_preset.into(); let mut collider_filepath = filepath.clone(); collider_filepath.set_extension("collider"); - let actual_collider_filepath = PathBuf::from("assets").join(&collider_filepath); + let actual_collider_filepath = PathBuf::from("assets/sprite").join(&collider_filepath); let collider = if actual_collider_filepath.exists() { read_collider_from_file(actual_collider_filepath.as_path()) } else { @@ -214,26 +214,26 @@ impl SpritePreset { /// `impl From for PathBuf` implementation. pub fn filepath(&self) -> PathBuf { match self { - SpritePreset::RacingBarrelBlue => "sprite/racing/barrel_blue.png", - SpritePreset::RacingBarrelRed => "sprite/racing/barrel_red.png", - SpritePreset::RacingBarrierRed => "sprite/racing/barrier_red.png", - SpritePreset::RacingBarrierWhite => "sprite/racing/barrier_white.png", - SpritePreset::RacingCarBlack => "sprite/racing/car_black.png", - SpritePreset::RacingCarBlue => "sprite/racing/car_blue.png", - SpritePreset::RacingCarGreen => "sprite/racing/car_green.png", - SpritePreset::RacingCarRed => "sprite/racing/car_red.png", - SpritePreset::RacingCarYellow => "sprite/racing/car_yellow.png", - SpritePreset::RacingConeStraight => "sprite/racing/cone_straight.png", - SpritePreset::RollingBallBlue => "sprite/rolling/ball_blue.png", - SpritePreset::RollingBallBlueAlt => "sprite/rolling/ball_blue_alt.png", - SpritePreset::RollingBallRed => "sprite/rolling/ball_red.png", - SpritePreset::RollingBallRedAlt => "sprite/rolling/ball_red_alt.png", - SpritePreset::RollingBlockCorner => "sprite/rolling/block_corner.png", - SpritePreset::RollingBlockNarrow => "sprite/rolling/block_narrow.png", - SpritePreset::RollingBlockSmall => "sprite/rolling/block_small.png", - SpritePreset::RollingBlockSquare => "sprite/rolling/block_square.png", - SpritePreset::RollingHoleEnd => "sprite/rolling/hole_end.png", - SpritePreset::RollingHoleStart => "sprite/rolling/hole_start.png", + SpritePreset::RacingBarrelBlue => "racing/barrel_blue.png", + SpritePreset::RacingBarrelRed => "racing/barrel_red.png", + SpritePreset::RacingBarrierRed => "racing/barrier_red.png", + SpritePreset::RacingBarrierWhite => "racing/barrier_white.png", + SpritePreset::RacingCarBlack => "racing/car_black.png", + SpritePreset::RacingCarBlue => "racing/car_blue.png", + SpritePreset::RacingCarGreen => "racing/car_green.png", + SpritePreset::RacingCarRed => "racing/car_red.png", + SpritePreset::RacingCarYellow => "racing/car_yellow.png", + SpritePreset::RacingConeStraight => "racing/cone_straight.png", + SpritePreset::RollingBallBlue => "rolling/ball_blue.png", + SpritePreset::RollingBallBlueAlt => "rolling/ball_blue_alt.png", + SpritePreset::RollingBallRed => "rolling/ball_red.png", + SpritePreset::RollingBallRedAlt => "rolling/ball_red_alt.png", + SpritePreset::RollingBlockCorner => "rolling/block_corner.png", + SpritePreset::RollingBlockNarrow => "rolling/block_narrow.png", + SpritePreset::RollingBlockSmall => "rolling/block_small.png", + SpritePreset::RollingBlockSquare => "rolling/block_square.png", + SpritePreset::RollingHoleEnd => "rolling/hole_end.png", + SpritePreset::RollingHoleStart => "rolling/hole_start.png", } .into() } diff --git a/src/text.rs b/src/text.rs index 5e42346..cd2aff9 100644 --- a/src/text.rs +++ b/src/text.rs @@ -16,7 +16,7 @@ pub struct Text { /// SYNCED: The actual text value you want to display. pub value: String, /// CREATION: The font to use when creating this text. Should be a file name of an .otf or - /// .ttf font located within the assets/fonts folder. Defaults to "FiraSans-Bold.ttf" (included + /// .ttf font located within the assets/font folder. Defaults to "FiraSans-Bold.ttf" (included /// in the default asset pack). pub font: String, /// SYNCED: The font size of the text you want to display. WARNING: As font sizes get larger, diff --git a/tutorial/book.toml b/tutorial/book.toml new file mode 100644 index 0000000..8c67f77 --- /dev/null +++ b/tutorial/book.toml @@ -0,0 +1,6 @@ +[book] +authors = [ "Nathan Stocks" ] +language = "en" +multilingual = false +src = "src" +title = "Rusty Engine Tutorial" diff --git a/tutorial/src/00-welcome.md b/tutorial/src/00-welcome.md new file mode 100644 index 0000000..0a36312 --- /dev/null +++ b/tutorial/src/00-welcome.md @@ -0,0 +1,16 @@ +# Welcome to Rusty Engine! + +Rusty Engine is a simple, 2D game engine for those who are learning Rust. Create simple game prototypes using straightforward Rust code without needing to learning difficult game engine concepts! It works on macOS, Linux, and Windows. Rusty Engine is a simplification wrapper over [Bevy](https://bevyengine.org/), which I encourage you to use directly for more serious game engine needs. + +The following courses use Rusty Engine in their curriculum: + +- [Ultimate Rust 2: Intermediate Concepts](https://www.udemy.com/course/ultimate-rust-2/?referralCode=8ED694EBE5637F954414) on Udemy (the sequel to [Ultimate Rust Crash Course](https://www.udemy.com/course/ultimate-rust-crash-course/?referralCode=AF30FAD8C6CCCC2C94F0)) +- [Rust in 3 Weeks](https://agileperception.com) conducted live on O'Reilly Online approximately once each quarter. + +# Tutorial + +This tutorial will walk you through all the major features of the engine. For more details, see also: + +- [Game Scenarios](https://github.com/CleanCut/rusty_engine/tree/main/scenarios) +- [The Rusty Engine Documentation](https://docs.rs/rusty_engine/latest/rusty_engine/) +- [The `examples` directory in the repository](https://github.com/CleanCut/rusty_engine/tree/main/examples) diff --git a/tutorial/src/02-quick-start.md b/tutorial/src/02-quick-start.md new file mode 100644 index 0000000..37baf0b --- /dev/null +++ b/tutorial/src/02-quick-start.md @@ -0,0 +1,56 @@ +# Quick Start Example + +- Create a new Rust project and add `rusty_engine` to the `[dependencies]` section of your `Cargo.toml` file. +- Download the [Asset Pack](10-assets.md) to you project. +- Write your game: + +```rust,ignore +// in src/main.rs + use rusty_engine::prelude::*; + + // Define a struct to hold custom data for your game (it can be a lot more complicated than this one!) + struct GameState { + health: i32, + } + + // Initialize the engine with your custom struct + rusty_engine::init!(GameState); + + fn main() { + // Create a game + let mut game = Game::new(); + // Set up your game. `Game` exposes all of the methods (but not fields) of `EngineState` as well. + let sprite = game.add_sprite("player", SpritePreset::RacingCarBlue); + sprite.scale = 2.0; + game.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0); + // Add one or more functions with logic for your game. When the game is run, the logic + // functions will run in the order they were added. + game.add_logic(game_logic); + // Run the game, with an initial state + let initial_game_state = GameState { health: 100 }; + game.run(initial_game_state); + } + + // Your game logic functions can be named anything, but the first parameter is always a + // `&mut EngineState`, and the second parameter is a mutable reference to your custom game + // state struct (`&mut GameState` in this case). The function returns a `bool`. + // + // This function will be run once each frame. + fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { + // The `EngineState` contains all sorts of built-in goodies. + // Get access to the player sprite... + let player = engine_state.sprites.get_mut("player").unwrap(); + // Rotate the player... + player.rotation += std::f32::consts::PI * engine_state.delta_f32; + // Damage the player if it is out of bounds... + if player.translation.x > 100.0 { + game_state.health -= 1; + } + // Returning `true` means the next logic function in line should be run. + true + } + ``` + +- Run your game with `cargo run --release`. Don't forget the `--release`! + +example screenshot diff --git a/tutorial/src/05-config.md b/tutorial/src/05-config.md new file mode 100644 index 0000000..7fb772b --- /dev/null +++ b/tutorial/src/05-config.md @@ -0,0 +1,23 @@ +# Configuration + +- Create a new Rust project +- In your `Cargo.toml` file, add `rusty_engine` to your `[dependencies]` section: + +```toml +[dependencies] +rusty_engine = "2.0.1" +``` + +### (Optional) Make `dev` profile act like `release` + +If you don't want to have remember to constantly add `--release` to your `cargo build` and `cargo run` commands, you can add this config section to your `Cargo.toml` to make your `dev` profile act like the `release` profile: + +```toml +[profile.dev] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false +incremental = false +codegen-units = 16 +``` diff --git a/tutorial/src/10-assets.md b/tutorial/src/10-assets.md new file mode 100644 index 0000000..6629853 --- /dev/null +++ b/tutorial/src/10-assets.md @@ -0,0 +1,36 @@ +# Asset Pack + +Rusty Engine assumes the asset pack is present, so you MUST download the asset pack. + +Here are three different ways to download the assets (pick any of them--it should end up the same in the end): +- RECOMMENDED: On a posix compatible shells run this command inside your project directory: +```shell +curl -L https://github.com/CleanCut/rusty_engine/archive/refs/heads/main.tar.gz | tar -zxv --strip-components=1 rusty_engine-main/assets +``` +- OR, clone the `rusty_engine` repository and copy/move the `assets/` directory over to your own project +- OR, download a [zip file](https://github.com/CleanCut/rusty_engine/archive/refs/heads/main.zip) or [tarball](https://github.com/CleanCut/rusty_engine/archive/refs/heads/main.tar.gz) of the `rusty_engine` repository, extract it, and copy/move the `assets/` directory over to your own project. + +## Asset Directory Structure + +All assets reside inside an `assets/` directory at the top folder of your Rust project (in the same directory as `Cargo.toml`). + +The structure looks like this: + +```text +assets +├── audio +│ ├── music +│ └── sfx +├── fonts +└── sprite + ├── racing + └── rolling +``` + +All audio files should be stored in `assets/audio`*. The asset pack divides sounds into `music` and `sfx` subdirectories. + +All font files should be stored in `assets/font`*. + +All sprites should be stored in `assets/sprite`*. + +*Additional subdirectories may be used for organization inside of these directories, as the asset pack does in some cases. diff --git a/tutorial/src/100-input.md b/tutorial/src/100-input.md new file mode 100644 index 0000000..eb804ba --- /dev/null +++ b/tutorial/src/100-input.md @@ -0,0 +1,3 @@ +# Input + +For games to be interactive, you need to process input from the user! Rusty Engine handles keyboard and mouse input. diff --git a/tutorial/src/105-keyboard-state.md b/tutorial/src/105-keyboard-state.md new file mode 100644 index 0000000..1081de9 --- /dev/null +++ b/tutorial/src/105-keyboard-state.md @@ -0,0 +1,60 @@ +# Keyboard State + +You can think of keyboard _state_ as a snapshot of exactly which keys are pressed (or not) at the start of the frame. Keyboard state is best for interactive things like character movement. If you need to process every single keystroke (like when entering text), check out the [Keyboard Event](110-keyboard-event.md) section instead. + +The `EngineState.keyboard_state` field is a struct through which you query the state of the key(s) you are interested in. + +Rusty Engine exposes [Bevy](https://bevyengine.org/)'s [`KeyCode`](https://docs.rs/bevy/latest/bevy/input/keyboard/enum.KeyCode.html) enum directly. See [the `KeyCode` documentation](https://docs.rs/bevy/latest/bevy/input/keyboard/enum.KeyCode.html) for all the possible key variants. + +### Pressed / Released + +Use the `pressed` method see if a single key is currently pressed or not: + +```rust,ignored +if engine_state.keyboard_state.pressed(KeyCode::Enter) { + // do stuff every frame that the key is still pressed +} +``` + +If a key is _not_ pressed, then it is released, so there is no dedicated method to check if a key is released. Just negate the condition by putting a `!` before the method call. + +### Just Pressed / Just Released + +The `just_pressed` method will let you know if the key was pressed for the first time _this_ frame, which is useful for triggering actions that you only want to happen once per keypress. + +```rust,ignored +if engine_state.keyboard_state.just_pressed(KeyCode::Escape) { + // do a thing when the key has just been pressed +} +``` + +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_state.keyboard_state.just_released(KeyCode::W) { + // do a thing when the key has just been released +} +``` + +### Handling Multiple Keys + +There is an `*_any` method for each of the three single key methods that does the same thing, but considering multiple keys at a time. This is especially helpful if you want to, e.g. treat WASD and arrow keys identically. + +- `pressed` -> `pressed_any` +- `just_pressed` -> `just_pressed_any` +- `just_released` -> `just_released_any` + +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_state.keyboard_state.pressed_any(&[KeyCode::W, KeyCode::Up]) { + // player moves upward +} +if engine_state.keyboard_state.just_pressed_any(&[KeyCode::Q, KeyCode::F1]) { + // open menu +} +if engine_state.keyboard_state.just_released_any(&[KeyCode::Space, KeyCode::LControl]) { + // reevaluate your life choices +} +``` + diff --git a/tutorial/src/110-keyboard-events.md b/tutorial/src/110-keyboard-events.md new file mode 100644 index 0000000..596f331 --- /dev/null +++ b/tutorial/src/110-keyboard-events.md @@ -0,0 +1,27 @@ +# Keyboard Events + +Keyboard events are what your operating system passes to text input boxes. If you go to a text box in a browser and hold down the space bar, you'll typically see one space, a short pause, and then several spaces come out faster after that. Those are keyboard events. You typically _only_ want keyboard events if you are trying to capture sequences of keypresses as if they are text. For things like character movement and button presses where you only care about the final state of the keyboard each frame, you should check out the [Keyboard State](105-keyboard-state.md) section instead. + +Keyboard events are passed through from Bevy as instances of the [`KeyboardInput`](https://docs.rs/rusty_engine/latest/rusty_engine/keyboard/struct.KeyboardInput.html) struct. Here is an example of processing keyboard events to adjust the position of a sprite: + +```rust,ignored +for keyboard_event in game_state.keyboard_events.drain(..) { + // We're using `if let` and a pattern to both destructure the struct and at the + // same time match "Pressed" events containing some key code. + if let KeyboardInput { + scan_code: _, + key_code: Some(key_code), + state: ElementState::Pressed, + } = 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, + _ => {} + } + } +} +``` + diff --git a/tutorial/src/115-mouse-state.md b/tutorial/src/115-mouse-state.md new file mode 100644 index 0000000..ed96c57 --- /dev/null +++ b/tutorial/src/115-mouse-state.md @@ -0,0 +1,72 @@ +# Mouse State + +Everything said about the [Keyboard State](105-keyboard-state.md) is true for Mouse State as well, just for your mouse instead of your keyboard. Mouse state is perfect for character movement or game controls such as buttons. If you need to process every bit of mouse input, such as all the locations the mouse was in since the beginning of the last frame, then you'll need to look at [Mouse Events](120-mouse-events.md) instead. + +All mouse state is stored in the `EngineState.mouse_state`, and queried via methods. + +### Mouse Buttons + +Mouse button handling closely parallels [keyboard state handling](105-keyboard-state.md), with the same six methods. Only instead of accepting `KeyCode` variants, they accept [`MouseButton`](https://docs.rs/rusty_engine/latest/rusty_engine/mouse/enum.MouseButton.html) variants. + +- `pressed` -> `pressed_any` +- `just_pressed` -> `just_pressed_any` +- `just_released` -> `just_released_any` + +Rather than repeat the entire discussion for each of the six methods, here's a quick example covering them all: + +```rust,ignored +if engine_state.mouse_state.pressed(MouseButton::Left) { + // The left mousebutton is currently pressed -- process some continuous movement +} +if engine_state.mouse_state.just_pressed(MouseButton::Right) { + // click that button! +} +if engine_state.mouse_state.just_released(MouseButton::Right) { + // nope, unclick the button. +} +if engine_state.mouse_state.pressed_any(&[MouseButton::Left, MouseButton::Right]) { + // one or more of the main mouse buttons are currently pressed +} +if engine_state.mouse_state.just_pressed_any(&[MouseButton::Middle, MouseButton::Other(4)]) { + // the middle button or the 4th button (or both) was just pressed +} +if engine_state.mouse_state.just_released_any(&[MouseButton::Left, MouseButton::Middle]) { + // one of those buttons was just released +} +``` + +### Location + +Use the `location` method to see where the mouse is. It returns an `Option`. If `None` is returned, then the mouse pointer isn't in the window. If present, the `Vec2` value is in the same 2D world coordinate system as the rest of the game. See the [section on sprite translation](60-sprite-transform.html) for more info about `Vec2` or the world coordinate system. + +It is easy to demonstrate `location` by having a sprite appear wherever your mouse is located: + +```rust,ignored +// `player` is a sprite +if let Some(location) = engine_state.mouse_state.location() { + player.translation = location; +} +``` + +### Motion + +The relative motion that the mouse moved last frame is accumulated into a single `Vec2`. This could be useful if you want to base some logic over how fast or in which direction the mouse is moving. + +```rust,ignored +let motion = engine_state.mouse_state.motion(); +if motion.length() > 50.0 { + // mouse is moving pretty fast +} +``` + +### Mouse Wheel + +This represents both the final scrolling (vertical, y) state of the mouse wheel and the final tilt (horizontal, x) state of the mouse wheel. See the [`MouseWheelState` docs](https://docs.rs/rusty_engine/latest/rusty_engine/mouse/struct.MouseWheelState.html) for more info on that. + +```rust,ignored +let mouse_wheel_state = engine_state.mouse_state.wheel(); +if mouse_wheel_state.y > 0 { + // scrolling in one direction... +} +``` + diff --git a/tutorial/src/120-mouse-events.md b/tutorial/src/120-mouse-events.md new file mode 100644 index 0000000..29a820c --- /dev/null +++ b/tutorial/src/120-mouse-events.md @@ -0,0 +1,55 @@ +# Mouse Events + +Every movement of the mouse, click of a mouse button, or scrolling tick of a scroll wheel generates a mouse event. All of the mouse events are stored into a set of vectors on `EngineState` that can be examined. At the end of each frame, any unprocessed events are thrown away. + +Mouse events are most useful when you want to process multiple events that happened within a single frame, such as processing all of the points that a mouse traversed, or all of the mousewheel clicks that happened in a single frame. + +### Mouse button events + +You usually want to use [mouse state](115-mouse-state.md) for mouse buttons, which are less awkward to deal with than mouse events when you only care about the state the mouse ended up in at the end of the frame. Mouse events are available in the `EngineState.mouse_button_events` vector. The Bevy struct [`MouseButtonInput`](https://docs.rs/rusty_engine/latest/rusty_engine/mouse/struct.MouseButtonInput.html) is used for the event value. Here is an example of using mouse button events to rotate a sprite by a fixed amount for each click. This is guaranteed not to miss any clicks in the (unlikely) event that two clicks come in on the same frame. + + +```rust,ignored +for mouse_button_input in &engine_state.mouse_button_events { + if mouse_button_input.state != ElementState::Pressed { + break; + } + match mouse_button_input.button { + MouseButton::Left => sprite.rotation += std::f32::consts::FRAC_PI_4, + MouseButton::Right => sprite.rotation -= std::f32::consts::FRAC_PI_4, + _ => {} + } +} +``` + +### Mouse location events + +Mouse location events are most useful if you are trying to capture all the points the mouse was at during the frame. Unlike mouse button events, there are *often* multiple mouse location events, since moving the mouse produces a series of events for each location that the mouse cursor is rendered on screen. If you only care about the final location of the mouse during the frame, you should use [mouse state](115-mouse-state.md) instead. + +Mouse location events are accessed through the `EngineState.mouse_location_events` vector and contain the [`CursorMoved`](https://docs.rs/rusty_engine/latest/rusty_engine/mouse/struct.CursorMoved.html) struct re-exported from Bevy. If one wanted to draw a trail of sparkles wherever a mouse went, mouse location events might be a good source of data: + +```rust,ignored +for cursor_moved in &engine_state.mouse_location_events { + // draw sparkles at cursor_moved.position +} +``` + +### Mouse motion 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 `EngineState.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 { + // do something with mouse_motion.delta +} +``` + +### Mouse wheel events + +As the mouse wheel tends to produce multiple events in a single frame, mouse wheel events may tend to be more useful than the mouse wheel state. Mouse wheel events are accessed through the `EngineState.mouse_wheel_events` vector and contain the [`MouseWheel`](https://docs.rs/rusty_engine/latest/rusty_engine/mouse/struct.MouseWheel.html) struct re-exported from Bevy. Here's an example of using the mouse wheel to scale the size of a sprite up or down. The `y` field represents the turning of the wheel. The `x` field represents sideways tilting motion for mouse wheels that support it. + +```rust,ignored +for mouse_wheel in &engine_state.mouse_wheel_events { + sprite.scale *= 1.0 + (0.05 * mouse_wheel.y); +} +``` diff --git a/tutorial/src/15-init.md b/tutorial/src/15-init.md new file mode 100644 index 0000000..6d17069 --- /dev/null +++ b/tutorial/src/15-init.md @@ -0,0 +1,86 @@ +# Engine Initialization + +Rusty Engine has a prelude which you should import all of in your `main.rs`: + +```rust,ignore +use rusty_engine::prelude::*; +``` + +The Rusty Engine `Game` struct generated in the top level your `main.rs` by the `init` macro. If you don't have a [`GameState`](20-game-state.md) struct, then you can leave the macro call empty and a unit struct `()` will be assumed for your game state: + +```rust,ignore +use rusty_engine::prelude::*; + +// Call init!() before your main() function +rusty_engine::init!(); + +fn main() { + // ... +} +``` + +If you do have a [`GameState`](20-game-state.md) struct (which we'll go over in the [game state section](20-game-state.md)), then you need to pass that type to the `init` macro: + +```rust,ignore +use rusty_engine::prelude::*; + +struct GameState { + number: i32, +} + +rusty_engine::init!(GameState); // There's a game state section later in the tutorial! + +fn main() { + // ... +} +``` + +Once you have initialized the engine, you should create a new `Game` struct in your `main` function and assign it to a mutable variable, usually called `game`. + +```rust,ignore +fn main() { + let mut game = Game::new(); + // ... +``` + +At this point you will use your `Game` instance to set up your game and register logic functions to run each frame. + +Finally, at the end of main you will run your `Game` with `Game::run()`. The `run` method takes an initial game state. If you didn't provide a game state struct type to the `init` macro, then your initial game state is a unit struct `()`: + +```rust,ignore +fn main() { + // ... + game.run(()); +} +``` + +If you did provide a game state struct type to the `init` macro, then pass `run` a value of that type: + +```rust,ignore +fn main() { + // ... + game.run(GameState { number: 42 }); +} +``` + +## Example + +If you put it all together, it looks like this. This example will run and create an empty window, but won't _do_ anything, yet. + +```rust,ignore +use rusty_engine::prelude::*; + +struct GameState { + number: i32, +} + +rusty_engine::init!(GameState); + +fn main() { + let mut game = Game::new(); + + // get your game stuff ready here + + game.run(GameState { number: 42 }); +} +``` diff --git a/tutorial/src/150-text.md b/tutorial/src/150-text.md new file mode 100644 index 0000000..3500bf5 --- /dev/null +++ b/tutorial/src/150-text.md @@ -0,0 +1,3 @@ +# Text + +Text in Rusty Engine is a lot like sprites. It has the same set of translation, rotation, scale, and layer fields. It is placed using the same world coordinate system as sprites. (Indeed, Rusty Engine went to great pains to use the same coordinate system for _everything_: rather than having separate coordinate systems for world and screen space, there is only world space.) Only instead of being based on a rectangular image file, text is based on a string value combined with a font and font size which are used to generate an image at runtime. diff --git a/tutorial/src/155-text-creation.md b/tutorial/src/155-text-creation.md new file mode 100644 index 0000000..deb4416 --- /dev/null +++ b/tutorial/src/155-text-creation.md @@ -0,0 +1,18 @@ +# Text Creation + +Text creation is quite similar to sprite creation. You create text through the [`EngineState.add_text`](400-engine-state.md) method. Since `Game` implements `DerefMut`, you can also call all of `EngineState`'s creation methods through `Game` in your `main()` function. In either case, it looks something like this when you create text: + +```rust,ignored +// Through your `Game` in `main()` +let _ = game.add_text("title", "The Fun Game"); + +// Or later in a game logic function through the `EngineState` +let _ = engine_state.add_text("score", "Score: 0"); +``` + +The first parameter is a unique label. It is used in the same way as sprite labels are used (to identify the text later on). The second parameter is actual string value to render. + +`add_text` returns a mutable reference to a `Text` (`&mut Text`). Note that this is one case where Rusty Engine does _not_ re-export something from Bevy. Bevy has also has a struct named `Text`, but it is entirely a different thing. + +Since it will emit a warning to silently ignore the mutable reference to the `Text`, you should explicitly ignore it if you are not going to use it by doing `let _ = ...` as in the examples above. However, most of the time you will want to use the mutable reference to immediately adjust your text, as we'll see in the following sections. + diff --git a/tutorial/src/160-text-attributes.md b/tutorial/src/160-text-attributes.md new file mode 100644 index 0000000..75d42e8 --- /dev/null +++ b/tutorial/src/160-text-attributes.md @@ -0,0 +1,55 @@ +# Text Value, Font & Font Size + +Changing the string value, the chosen font, or the font size causes the `Text` to be re-rendered as a new image at the end of the frame. This is relatively expensive in terms of performance, so you should avoid changing these attributes except when you actually need to. + +All existing text values can be accessed through the `EngineState.texts` vector. + + +### Value + +`Text.value` is the actual string that gets rendered to the screen. If you change the value, then the `Text` will be re-rendered as a new image at the end of the frame with the new value. + +```rust,ignored +let score_text = engine_state.texts.get_mut("score_text").unwrap(); +score_text.value = format!("Score: {}", score); +``` + +### Font + +If you change the font, then the `Text` will be re-rendered as a new image at the end of the frame with the new value. + +The asset pack contains two fonts: + +- `FiraMono-Medium.ttf` +- `FiraSans-Bold.ttf` (the default font if none is specified) + + +```rust,ignored +let mono = engine_state.add_text("mono", "This text is using a monospace font"); +mono.font = "FiraMono-Medium.ttf".to_string(); +``` + +To use a custom font, place a valid `otf` or `ttf` file in `assets/font` and set it on your `Text`. + +```rust,ignored +// After placing `party.otf` in the `assets/font/` directory... +let party = engine_state.add_text("party", "Let's Party!"); +mono.font = "party.otf".to_string(); +``` + +If you specify a font file which can't be loaded successfully, you will get an console error like this: + +```text +Dec 30 15:15:20.624 WARN bevy_asset::asset_server: encountered an error while reading an asset: path not found: /Users/nathan/rust/rusty_engine/assets/font/nonexistent.ttf +``` + +### Font Size + +If you change the font size, then the `Text` will be re-rendered as a new image at the end of the frame with the font size. + +The default font size is `30.0`. Setting the font size doesn't require a lot of explanation: + +```rust,ignored +let large = engine_state.add_text("large", "This is a large font size!"); +mono.font_size = 96.0; +``` diff --git a/tutorial/src/165-text-transform.md b/tutorial/src/165-text-transform.md new file mode 100644 index 0000000..df67f9a --- /dev/null +++ b/tutorial/src/165-text-transform.md @@ -0,0 +1,96 @@ +# Text Transform + +Text is rendered as an image. This rendering (or re-rendering) happens at the end of the frame after any of the [Value, Font & Font Size](160-text-attributes.md) attributes are changed. However, when _transform_ values such as translation, rotation, scale, or layer are changed, the image remains the same and its representation on screen is manipulated in the GPU, which is high performance. + +In short, feel free to change your text's transform attributes every frame without any big hit in performance. + +### Translation + +`Text.translation` is a [`Vec2`](https://docs.rs/glam/latest/glam/f32/struct.Vec2.html) containing the X and Y coordinates of your text's position on the screen. This `Vec2` location is in the exact center of the text, both vertically and horizontally. In other words, text is always rendered with "centered" alignment on both axes. + +The coordinate system works just like it does in math class. `(0.0, 0.0)` is in the center of the screen. Positive X goes to the right side of the screen. Positive Y goes to the top of the screen. Every increment of `1.0` is one logical pixel on the screen. Hi-DPI screens may have more than one physical pixel per logical pixel. See the [`EngineState`](400-engine-state.md) section for details on how to check the logical pixel dimensions of your window. + +```rust,ignored +let score_text = game.add_text("score_text", "Score: 0"); +score_text.translation = Vec2::new(400.0, -325.0); +``` + +### Rotation + +`Text.rotation`* is an `f32` representing the angle in radians from the positive X axis. In other words, a rotation of `0.0` results in normal, horizontal text along the X axis. A rotation of `PI` would result in upside-down text. + +*Bevy 0.5 does not support text rotation, so modifying this field is currently a no-op. Once Bevy 0.6 is released, and Rusty Engine is updated to use it, this will actually worked as described. + +```rust,ignored +let angled = engine_state.add_text("angled", "This text is at an angle."); +score_text.rotation = std::f32::consts::PI / 4.0; +``` + +### Scale + +`Text.scale`* is an `f32`. `1.0` means matching a pixel of the source image to a pixel on the screen. `2.0` makes the image twice as wide and tall, etc. + +Usually, you will want to leave text at a scale of `1.0`, but if you wish to have text zoom or shrink, modifying the scale has two important advantages compared to changing the font size: + +- Changing the scale is _fast_. The text image does not need to be re-rendered, and the size transformation is handled all in GPU hardware. +- Changing the scale doesn't cause weird re-rendering inconsistencies, so animating scale changes looks smooth. + +The main drawback of changing the scale is that since the font is not re-rendered, it looks pixellated when scaled up. Though, this could be considered as a stylistic plus as well. + +*Bevy 0.5 does not support changing text scale, so modifying this field is currently a no-op. Once Bevy 0.6 is released, and Rusty Engine is updated to use it, this will actually worked as described. + +```rust,ignored +let zoomed = engine_state.add_text("zoomed", "This text is twice as big as normal."); +score_text.scale = 2.0; +``` + +### Layer + +`Text.layer` is an `f32` that affects what sprite or text is "on top" of another sprite or text when they overlap. `0.0` is the default and "bottom" layer, and `999.0` is the "top" layer. The order of sprites or text on the same layer is random and unstable (can change frame to frame), so you should make sure that sprites and text that will overlap are on different layers. A good practice is to choose a few layers and assign them to constants. For example: + +```rust,ignored +const BACKGROUND_LAYER: f32 = 0.0; +const CHARACTER_LAYER: f32 = 1.0; +const EFFECTS_LAYER: f32 = 2.0; +const UI_BOTTOM_LAYER: f32 = 3.0; +const UI_TOP_LAYER: f32 = 4.0; +``` + +### Adjusting your newly-created text + +When you create a `Text`, you get a mutable reference to the newly-created text that you can use to adjust it. + +```rust,ignored +let text = engine_state.add_text("msg", "This is an important message."); +text.translation = Vec2::new(0.0, -300.0); +text.layer = UI_TOP_LAYER; // as in previous code snippet +``` + +The `Vec2` type used for the `translation` field is from `glam`, and has [its own documentation](https://docs.rs/glam/latest/glam/f32/struct.Vec2.html) you can read up on if you're interested. The thing you'll probably use the most are its `x` and `y` fields. The code below is the same as setting `text.translation = Vec2::new(0.0, -300.0);` + +```rust,ignored +text.translation.x = 0.0; +player.translation.y = -300.0; +``` + +NOTE: If you want to adjust your text's transform smoothly, you will need to multiply your change by the frame's delta value. See the [`EngineState`](400-engine-state.md) section for more details. + +### Adjusting an existing text + +To adjust a text which already exists, you need to get a mutable reference to it. This is where that "label" comes in. The `EngineState.texts` field is a hash map of labels to texts. You get a mutable reference to a text with the `HashMap::get_mut` method: + + +```rust,ignored +// Be careful with unwrap()! If the entry isn't there, this will crash your game. +let spinning_message = engine_state.texts.get_mut("spinning_message").unwrap(); +spinning_message.rotation += TURN_SPEED_PER_SEC * engine_state.delta_f32; +``` + +### Deleting a text + +To delete a text, simply remove it from the `EngineState.texts` hash map. + +```rust,ignored +engine_state.texts.remove("old_message"); +``` + diff --git a/tutorial/src/20-game-state.md b/tutorial/src/20-game-state.md new file mode 100644 index 0000000..ff62e93 --- /dev/null +++ b/tutorial/src/20-game-state.md @@ -0,0 +1,42 @@ +# Game State + +You will need somewhere to store data for your game that is not part of the engine but that you need access to for more than a single frame. That somewhere is your _game state struct_. You provide a struct type to use for your own game state. Within that struct, you can store just about anything. Some examples of things you may want to put in your game state: + +- Player attributes (name, color, health, energy, money, etc.) +- Game attributes (score, day, turn, etc.) +- Timers for animation, spawning events, etc. +- Collections of sprite labels to iterate through (perhaps a vector of labels of all the enemy sprites, or a vector of widgets to animate, or...whatever) +- Anything else that needs to persist across frames that isn't already stored in the engine + +You can name your game state struct whatever you want, but since there can only ever be one game state type, we suggest you call it `GameState` for simplicity, as we will do in this tutorial. Here is an example of a game state you might define for a simple game which keeps track of a current score and a high score, the labels of enemies which need to move around, and a timer for when to spawn a new enemy. + +```rust,ignore +struct GameState { + high_score: u32, + current_score: u32, + enemy_labels: Vec, + spawn_timer: Timer, +} +``` + +Once you have defined your struct, pass it to the `init` macro. If you don't give the `init` macro a struct value, the unit struct `()` will be used instead. + +```rust,ignore +rusty_engine::init!(GameState); +``` + +When you start your game, you will need to pass it an initial value of the game state. You will receive a mutable reference to this game state value in your logic function(s) each frame. + +```rust,ignore +fn main() { + // ... + let game_state = GameState { + high_score: 2345, + current_score: 0, + enemy_labels: Vec::new(), + spawn_timer: Timer::from_seconds(10.0, false), + }; + game.run(game_state); +} +``` + diff --git a/tutorial/src/200-audio.md b/tutorial/src/200-audio.md new file mode 100644 index 0000000..2af63b5 --- /dev/null +++ b/tutorial/src/200-audio.md @@ -0,0 +1,7 @@ +# Audio + +Rusty Engine has a basic audio system. You can play one looping music track, and quite a few concurrent sound effects. There are some music and sound effect files included in the asset pack. + +Supported audio file formats are `ogg` (including `oga`), `mp3`, `flac`, and `wav`. + +All audio is accessed through methods on the audio manager accessed through `EngineState.audio_manager`. diff --git a/tutorial/src/205-music.md b/tutorial/src/205-music.md new file mode 100644 index 0000000..deefc01 --- /dev/null +++ b/tutorial/src/205-music.md @@ -0,0 +1,37 @@ +# Music + +One music file may be played at a time. Music always loops repeatedly until explicitly stopped (or the program exits). As with other `EngineState` fields, the audio manager is also available through the `Game` in your `main` function: + +### Play + +The `play_music` method starts playing looping music. The first parameter should be a `MusicPreset` enum variant or a music file path relative to `assets/audio`. All music from the asset pack have variants present in the `MusicPreset` enum for convenience. + +The second parameter is the volume, which should be a value between `0.0` (silent) and `1.0` full volume. + +```rust,ignored +// using a preset +game.audio_manager.play_music(MusicPreset::Classy8Bit, 1.0); + +// using a filepath relative to `assets/audio` +game.audio_manager.play_music("music/Classy 8-Bit.oga", 1.0); +``` + +Any music already playing will be stopped to play a new music selection. + +### Stop + +The `stop_music` method stops any music that is playing. If no music is playing, it is a no-op. + +```rust,ignored +engine_state.audio_manager.stop_music(); +``` + +### Music playing status + +The `music_playing` method will return a `bool` indicating whether or not music is currently playing. + +```rust,ignored +if engine_state.audio_manager.music_playing() { + // yep, you remembered to start the music +} +``` diff --git a/tutorial/src/210-sfx.md b/tutorial/src/210-sfx.md new file mode 100644 index 0000000..369b186 --- /dev/null +++ b/tutorial/src/210-sfx.md @@ -0,0 +1,19 @@ +# Sound Effects + +At least a dozen sound effects can play concurrently. The exact number is probably dependent on the particular hardware you have available on your local machine. Sound effects are played in a "fire and forget" manner, and will each play in a separate channel (if available) and terminate once reaching the end of the audio source. + +### Play + +The `play_sfx` method plays a sound effect. The first parameter should be a `SfxPreset` enum variant or a music file path relative to `assets/audio`. All sound effects from the asset pack have variants present in the `SfxPreset` enum for convenience. + +The second parameter is the volume, which should be a value between `0.0` (silent) and `1.0` full volume. + +```rust,ignored +// using a preset +engine_state.audio_manager.play_sfx(SfxPreset::Jingle1, 1.0); + +// using a filepath relative to `assets/audio` +engine_state.audio_manager.play_sfx("sfx/jingle1.ogg", 1.0); +``` + +There is no way to interact with sound effects to monitor status or stop them early. diff --git a/tutorial/src/25-game-logic-function.md b/tutorial/src/25-game-logic-function.md new file mode 100644 index 0000000..6306d9d --- /dev/null +++ b/tutorial/src/25-game-logic-function.md @@ -0,0 +1,79 @@ +# Game Logic Function + +A game is divided up into _frames_. A _frame_ is one run through your game logic to produce a new image to display on the screen. Commonly, you will get about 60 frames each second. Each frame, Rusty Engine tries to run your game logic function(s). + +A game logic function definition looks like this: + +```rust,ignore +fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { + // your actual game logic goes here +} +``` + +The function may be named anything you want. We used `game_logic` in the example above, which is the conventional name if you only have one. However, if you use more than one game logic function, each will need to have a unique name. + +Your `game_state` parameter should be a mutable reference to your game state struct type. The example above is for a game state struct literally named `GameState`. If you did not provide a game state type, then you will need to use a unit struct `()` instead: + +```rust,ignore +game_state: &mut () +``` + +Rusty Engine will attempt to run all of your game logic functions that it knows about each frame. You need to "add" your game logic functions by calling `Game::add_logic` in your `main` function: + +```rust,ignore +game.add_logic(game_logic); +``` + +You can add multiple game logic functions, which will always run in the order they were added. For example, this game will always run the `menu_logic` function first, and then the `game_logic`. + +```rust,ignore +game.add_logic(menu_logic); +game.add_logic(game_logic); +``` + +After each logic function is run, Rusty Engine checks the return value to see if it should continue with the next logic function, or skip the rest. Game logic functions return a `bool`. If the return value is `true`, that means "keep on going!" and run the next logic function. If the return value is `false`, then Rusty Engine skips the rest of the logic functions this frame. This is so, for example, if you don't want your game logic to run while you are in a menu, then your menu logic can return `false` and the game logic simply won't run. + +Most of the time you want the next function to run, so most game logic functions end like this: + +```rust,ignore + // ... + true +} +``` + +The return value of the last game logic function in the chain is ignored. + +## Example + +Here's an example game logic function using the game state from the [game state section](20-game-state.md). It simply increments the score and outputs that score to the console once per frame. + +```rust,ignore +use rusty_engine::prelude::*; + +struct GameState { + high_score: u32, + current_score: u32, + enemy_labels: Vec, + spawn_timer: Timer, +} + +rusty_engine::init!(GameState); + +fn main() { + let mut game = Game::new(); + let game_state = GameState { + high_score: 2345, + current_score: 0, + enemy_labels: Vec::new(), + spawn_timer: Timer::from_seconds(10.0, false), + }; + game.add_logic(game_logic); // Don't forget to add the logic function to the game! + game.run(game_state); +} + +fn game_logic(engine_state: &mut EngineState, game_state: &mut GameState) -> bool { + game_state.current_score += 1; + println!("Current score: {}", game_state.current_score); + true +} +``` diff --git a/tutorial/src/250-timer.md b/tutorial/src/250-timer.md new file mode 100644 index 0000000..3938122 --- /dev/null +++ b/tutorial/src/250-timer.md @@ -0,0 +1,34 @@ +# Timer + +Rusty Engine re-export's Bevy's [`Timer`](https://docs.rs/rusty_engine/latest/rusty_engine/prelude/struct.Timer.html) struct. Please see the [`Timer` API documentation](https://docs.rs/rusty_engine/latest/rusty_engine/prelude/struct.Timer.html) for full details. Below, is a quick introduction to the most vital parts. + +Timers are super cheap, performance-wise. Feel free to create them and throw them away as much as you like. + +### Creation + +It is easy to create a timer with the `from_seconds` method. The first parameter is a number of seconds to countdown, and the second parameter is whether or not the timer is repeating. `false` means the timer will only countdown once, and then remain at `0.0` once finished. `true` means the timer will start counting down again from the same countdown time as soon as it has reached `0.0`. + +```rust,ignored +// A one-shot timer. +let timer_once = Timer::from_seconds(10.0, false); + +// A repeating timer. +let timer_repeating = Timer::from_seconds(3.0, true); +``` + +### Counting down & Finishing + +Timers must be ticked with the `tick` method to make them actually count down the time. The `tick` method takes a `Duration` of time that has gone by, which is exactly what `EngineState.delta` is for. `tick` returns an immutable reference to the timer, so you can chain a method call to `finished` or `just_finished`. + +```rust,ignored +if timer_once.tick(engine_state.delta).just_finished() { + // the one-shot timer just finished, do the thing +} + +if timer_repeating.tick(engine_state.delta).just_finished() { + // the repeating timer finished (again), do the thing (again) + // the timer has already begun counting down from the top again at this point +} +``` + +If you don't tick a timer, it is effectively paused. diff --git a/tutorial/src/400-engine-state.md b/tutorial/src/400-engine-state.md new file mode 100644 index 0000000..7d88d00 --- /dev/null +++ b/tutorial/src/400-engine-state.md @@ -0,0 +1,14 @@ +# Engine State + +The `EngineState` struct is central to Rusty Engine and has already shown up in many places in this tutorial. It is highly recommended to read through all of the [`EngineState` API documentation](https://docs.rs/rusty_engine/latest/rusty_engine/game/struct.EngineState.html). + +Here are a few other tidbits that are worth calling out: + +- `should_exit` - a `bool` field you can set to `true` to cause Rusty Engine to cleanly exit at the end of the frame. +- `delta` - the duration of the previous frame as a `Duration`. This should be used for ticking any `Timer`s. +- `delta_f32` - the duration of the previous frame as an `f32`. This should be used to produce smooth animation. For example, if you define a movement speed in `pixels per second` such as `const MOVE_SPEED: f32 = 50.0`, then you can use it to actually move a sprite at that speed by multiplying it by `delta_f32` like this: `sprite.translation.x += MOVE_SPEED * engine_state.delta_f32` +- `time_since_startup` - the duration since the start of the program as a `Duration` +- `time_since_startup_f64` - the duration since the start of the program as an `f64`. This needs to be a 64-bit float because it would be easy for an `f32` to reach a number high enough to be low precision. If you want to do math with this number, you should do the math with `f64`'s, and then convert it to an `f32` at the very end. +- `window_dimensions` - a `Vec2` describing the width and height of the window in pixels. Since `(0.0, 0.0)` is the center of the screen, the edges of the screen are +/- `window_dimensions / 2.0`. + +...for the rest of the fields (and methods), see the [`EngineState` API documentation](https://docs.rs/rusty_engine/latest/rusty_engine/game/struct.EngineState.html) diff --git a/tutorial/src/450-game.md b/tutorial/src/450-game.md new file mode 100644 index 0000000..7c79786 --- /dev/null +++ b/tutorial/src/450-game.md @@ -0,0 +1,39 @@ +# Game + +The `Game` struct is generated by the `rusty_engine::init!(GameState);` macro call. It mostly exists to get your custom game state injected into Bevy, and to serve as a proxy for `EngineState` before the game has started. + +Even though `Game` is generated, some [`Game` API documentation](https://docs.rs/rusty_engine/latest/rusty_engine/game/struct.Game.html) has been stubbed out for reading -- just be aware that some bits (like the types of some things) will be inaccurate, since the docs aren't actually being generated off of the real bits that you will be using. + +Since `Game` implements `DerefMut`, any field or method not found on `Game` will be searched for on `EngineState` and used if it is found. So, in a sense, `Game` is also the `EngineState` while you are setting things up in `main`. However, there are a couple additional things that are unique to `Game`: + +### New + +The first, and most obvious, difference is that `Game` has a `new` method, as documented in the [Engine Initialization](15-init.md) section. You need to call `new` in your `main` function to create a new game. The variable you assign this value to should be mutable. + +```rust,ignored +fn main() { + let mut game = Game::new(); +} +``` + +### Window Settings + +Rusty Engine re-exports the [`WindowDescriptor`](https://docs.rs/rusty_engine/latest/rusty_engine/game/struct.WindowDescriptor.html) struct from Bevy, whose fields are all used to request certain window attributes. Please be aware that these are only _requests_ for configuration, and that the underlying operating system may refuse (or be unable) to give you exactly what you ask for. For example, you may not be able to obtain a window with larger dimensions than the physical monitor. + +Pass a `WindowDescriptor` to the `window_settings` method to request specific settings for your game window. This is a great time to take advantage of "struct update" syntax so you don't have to re-specify the fields which you aren't customizing. + +```rust,ignored +game.window_settings(WindowDescriptor { + title: "My Awesome Game".into(), + width: 800.0, + height: 200.0, + ..Default::default() +``` + +### Adding Game Logic Functions + +Game has an `add_logic` method to add game logic functions to your game. Please see the [Engine Initialization](15-init.md) for more details on this method. + +### Running the game + +The last thing you will do in your main function is to call the `run` method to begin your game. The `run` method takes an instance of whatever game state struct you passed to the `init` macro, or a unit struct `()` if you didn't pass one. diff --git a/tutorial/src/50-sprite.md b/tutorial/src/50-sprite.md new file mode 100644 index 0000000..a99f00a --- /dev/null +++ b/tutorial/src/50-sprite.md @@ -0,0 +1,11 @@ +# Sprite + +A _sprite_ in Rusty Engine is a 2D image, its transform (translation, rotation, scale), its collider, and other associated metadata. You will use sprites for all the graphics in your game. Many sprites will represent some _thing_. For example, a race car sprite may represent your player character in your game. + +![green race car sprite](https://github.com/CleanCut/rusty_engine/raw/main/assets/sprite/racing/car_green.png) + +A barrel sprite may represent an obstacle, an enemy, or maybe it's just part of the background. It's up to you to decide how to treat the sprites. + +![blue barrel sprite](https://github.com/CleanCut/rusty_engine/raw/main/assets/sprite/racing/barrel_blue.png) + +[Next: Sprite Creation](55-sprite-creation.md) diff --git a/tutorial/src/55-sprite-creation.md b/tutorial/src/55-sprite-creation.md new file mode 100644 index 0000000..d81cf31 --- /dev/null +++ b/tutorial/src/55-sprite-creation.md @@ -0,0 +1,19 @@ +# Sprite Creation + +Sprites are created through the [`EngineState`](400-engine-state.md). Since `Game` implements `DerefMut`, you can also call all of `EngineState`'s creation methods through `Game` in your `main()` function. In either case, it looks something like this when you create a sprite with a preset: + +```rust,ignored +// Through your `Game` in `main()` +let _ = game.add_sprite("my_player", SpritePreset::RacingCarBlue); + +// Or later in a game logic function +let _ = engine_state.add_sprite("my_player", SpritePreset::RacingCarBlue); +``` + +All sprites in the asset pack have a "preset", which is just a fancy `enum` that makes it easy for you as a user to select one of sprite image files. You could also specify the image filepath, relative to the `assets/sprite` directory, which you would do if you add your own images. For example, the full filepath of the blue racing car is `assets/sprite/racing/car_blue.png`, so to create it by filepath you would do: + +```rust,ignored +let _ = engine_state.add_sprite("my_player", "racing/car_blue.png"); +``` + +`add_sprite` returns a mutable reference to a `Sprite` (`&mut Sprite`). Since it will emit a warning to silently ignore the reference, you should explicitly ignore it if you are not going to use it by doing `let _ = ...` as in the examples above. However, most of the time you will want to use the mutable reference to immediately adjust your sprite. diff --git a/tutorial/src/60-sprite-transform.md b/tutorial/src/60-sprite-transform.md new file mode 100644 index 0000000..2013551 --- /dev/null +++ b/tutorial/src/60-sprite-transform.md @@ -0,0 +1,69 @@ +# Transform + +A "transform" is a fancy term for the way that you can position and size your sprite. There are four different fields you can use to position and size your sprite: + +### Translation + +`Sprite.translation` is a [`Vec2`](https://docs.rs/glam/latest/glam/f32/struct.Vec2.html) containing the X and Y coordinates of your sprite's position on the screen. The coordinate system works just like it does in math class. `(0.0, 0.0)` is in the center of the screen. Positive X goes to the right side of the screen. Positive Y goes to the top of the screen. Every increment of `1.0` is one logical pixel on the screen. Hi-DPI screens may have more than one physical pixel per logical pixel. See the [`EngineState`](400-engine-state.md) section for details on how to check the logical pixel dimensions of your window. + +### Rotation + +`Sprite.rotation` is an `f32` representing the angle in radians from the positive X axis. In other words, a rotation of `0.0` is facing to the right, so custom images you want to use in Rusty Engine should also be "facing" to the right in their raw form (whatever "to the right" means is up to you). `2 * PI` brings you in a full circle, so `0.5 * PI` is "up", `PI` is "left", and `1.5 * PI` is "down". There are a bunch of helpful constants defined for cardinal directions if you don't want to remember the numerical value yourself. + +### Scale + +`Sprite.scale` is an `f32`. `1.0` means matching a pixel of the source image to a pixel on the screen. `2.0` makes the image twice as wide and tall, etc. + +### Layer + +`Sprite.layer` is an `f32` that affects what sprite or text is "on top" of another sprite or text when they overlap. `0.0` is the default and "bottom" layer, and `999.0` is the "top" layer. The order of sprites or text on the same layer is random and unstable (can change frame to frame), so you should make sure that sprites and text that will overlap are on different layers. A good practice is to choose a few layers and assign them to constants. For example: + +```rust,ignored +const BACKGROUND_LAYER: f32 = 0.0; +const CHARACTER_LAYER: f32 = 1.0; +const EFFECTS_LAYER: f32 = 2.0; +const UI_BOTTOM_LAYER: f32 = 3.0; +const UI_TOP_LAYER: f32 = 4.0; +``` + + +### Adjusting your newly-created sprite + +When you create a sprite, you get a mutable reference to the newly-created sprite that you can use to adjust it. + +```rust,ignored +let player = engine_state.add_sprite("my_player", SpritePreset::RacingCarBlue); +player.translation = Vec2::new(200.0, 100.0); // Move the car up and to the right +player.rotation = UP; // UP is one of the built-in constants you can use +player.scale = 2.5; // It's a BIG car! +player.layer = CHARACTER_LAYER; // as in previous code snippet +``` + +The `Vec2` type used for the `translation` field is from `glam`, and has [its own documentation](https://docs.rs/glam/latest/glam/f32/struct.Vec2.html) you can read up on if you're interested. The thing you'll probably use the most are its `x` and `y` fields: + +```rust,ignored +player.translation.x += 45.0 * engine_state.delta_f32; +player.translation.y -= 10.0 * engine_state.delta_f32; +``` + +NOTE: If you want to adjust your sprite smoothly, you will need to multiply it by the frame's delta value. See the [`EngineState`](400-engine-state.md) section for more details. + +### Adjusting an existing sprite + +To adjust a sprite which already exists, you need to get a mutable reference to it. This is where that "label" comes in. The `EngineState.sprites` field is a hash map of labels to sprites. You get a mutable reference to a sprite with the `HashMap::get_mut` method: + + +```rust,ignored +// Be careful with unwrap()! If the entry isn't there, this will crash your game. +let player_reference = engine_state.sprites.get_mut("my_player").unwrap(); +player_reference.rotation += TURN_SPEED_PER_SEC * engine_state.delta_f32; +``` + +### Deleting a sprite + +To delete a sprite, simply remove it from the `EngineState.sprites` hash map. + +```rust,ignored +engine_state.sprites.remove("my_player"); +``` + diff --git a/tutorial/src/65-sprite-collider.md b/tutorial/src/65-sprite-collider.md new file mode 100644 index 0000000..58cf3c6 --- /dev/null +++ b/tutorial/src/65-sprite-collider.md @@ -0,0 +1,43 @@ +# Sprite Collider + +Rusty Engine has an basic collision system. You may define one convex polygon shape to be a collider for a sprite. When two sprites with colliders whose `collision` fields are both set to `true` begin or end overlapping, a [`CollisionEvent`](https://docs.rs/rusty_engine/latest/rusty_engine/physics/struct.CollisionEvent.html) will be produced. If either of the sprites lacks a collider, or if either of the sprites has their `collision` field set to `false`, then no collision event is produced. + +Colliders will be rendered as lines on the screen if `EngineState.debug_sprite_colliders` is set to `true`. + +Colliders are stored in files with the same name and path as the image file they are for, but with the `.collider` extension. If a valid collider file exists when a sprite is created, it will be loaded automatically. However, the `collision` field for a sprite always defaults to `false`, so you must opt in to the collision system by setting `collision` to `true` on your sprites. + +### Processing collision events + +Your game logic should process collision events each frame. Collision events which you don't handle are discarded at the end of each frame. Collision events are accessed through the `EngineState.collision_events` vector. + +Each `CollisionEvent` consists of a `CollisionState` (an enum of either `Begin` or `End`) and a `CollisionPair`, which is a tuple of the labels of the two sprites involved in the collision. It is up to you to figure out what to do with the information that a collision occurred. + + +```rust,ignored +for event in engine_state.collision_events.drain(..) { + match event.state { + CollisionState::Begin => { + println!("{} and {} collided!", event.pair.0, event.pair.1); + } + CollisionState::End => { + println!("{} and {} are no longer colliding.", event.pair.0, event.pair.1); + } + } +} +``` + +### Creating colliders + +All of the sprite presets in the game already have colliders, so you don't need to worry about creating any of them. + +Creating colliders for custom sprites from scratch can be quite difficult, so there is an "example" program called `collider_creator` that you can run to create the collider by clicking around a sprite! Clone the [`rusty_engine`](https://github.com/CleanCut/rusty_engine/) repository, place your image file in the `assets/sprite` directory (let's call it `db.png`), and then run: + +```text +$ cargo run --release --example collider_creator -- db.png +``` + +Then follow the directions to create (or re-create) a collider and write it to a file. + +Screen Shot 2021-12-26 at 10 45 40 PM + +Once you have a good collider created, copy (or move) both your image and `.collider` file to your own project, under the `assets/sprite` directory. diff --git a/tutorial/src/SUMMARY.md b/tutorial/src/SUMMARY.md new file mode 100644 index 0000000..accca8a --- /dev/null +++ b/tutorial/src/SUMMARY.md @@ -0,0 +1,31 @@ +# Summary + +[Welcome to Rusty Engine!](00-welcome.md) +[Quick Start Example](02-quick-start.md) + +--- + +- [Configuration](05-config.md) +- [Asset Pack](10-assets.md) +- [Engine Initialization](15-init.md) +- [Game State](20-game-state.md) +- [Game Logic Function](25-game-logic-function.md) +- [Sprite](50-sprite.md) + - [Creation](55-sprite-creation.md) + - [Transform](60-sprite-transform.md) + - [Collider](65-sprite-collider.md) +- [Input](100-input.md) + - [Keyboard State](105-keyboard-state.md) + - [Keyboard Events](110-keyboard-events.md) + - [Mouse State](115-mouse-state.md) + - [Mouse Events](120-mouse-events.md) +- [Text](150-text.md) + - [Creation](155-text-creation.md) + - [Value, Font & Font Size](160-text-attributes.md) + - [Transform](165-text-transform.md) +- [Audio](200-audio.md) + - [Music](205-music.md) + - [Sound Effects](210-sfx.md) +- [Timer](250-timer.md) +- [EngineState](400-engine-state.md) +- [Game](450-game.md) diff --git a/userguide/book.toml b/userguide/book.toml deleted file mode 100644 index a55e01b..0000000 --- a/userguide/book.toml +++ /dev/null @@ -1,6 +0,0 @@ -[book] -authors = ["Nathan Stocks"] -language = "en" -multilingual = false -src = "src" -title = "Rusty Engine User Guide" diff --git a/userguide/src/SUMMARY.md b/userguide/src/SUMMARY.md deleted file mode 100644 index 3577f05..0000000 --- a/userguide/src/SUMMARY.md +++ /dev/null @@ -1,4 +0,0 @@ -# Summary - -- [Welcome to Rusty Engine!](./welcome.md) -- [Beginning Your Game Project](./beginning.md) diff --git a/userguide/src/beginning.md b/userguide/src/beginning.md deleted file mode 100644 index 43f51b5..0000000 --- a/userguide/src/beginning.md +++ /dev/null @@ -1,2 +0,0 @@ -# Beginning Your Game Project - diff --git a/userguide/src/welcome.md b/userguide/src/welcome.md deleted file mode 100644 index b91d498..0000000 --- a/userguide/src/welcome.md +++ /dev/null @@ -1,12 +0,0 @@ -# Welcome to Rusty Engine! - -Rusty Engine is a game engine for writing simple 2D game prototypes in Rust. It is often paired -with projects for crash courses such as the [Ultimate Rust Crash Course]. The goal of Rusty Engine -is to abstract away most of the hard parts of a game engine so you can focus on learning Rust -while making a simple game. - -You are welcome to keep tinkering with Rusty Engine once you have a solid grasp of Rust, of course, -but we won't be surprised if you move on to a more fully-featured game engine such as [Bevy] - -[Ultimate Rust Crash Course]: https://agileperception.com/ultimate_rust_crash_course -[Bevy]: https://bevyengine.org/