diff --git a/agb/src/display/tiled/infinite_scrolled_map.rs b/agb/src/display/tiled/infinite_scrolled_map.rs index ed30997d..2747f6d6 100644 --- a/agb/src/display/tiled/infinite_scrolled_map.rs +++ b/agb/src/display/tiled/infinite_scrolled_map.rs @@ -14,7 +14,7 @@ use crate::{ /// The abstraction allows only for static tiles, but it is possible to animate the tiles if needed. /// /// When you create a new infinite scrolled map, you need to provide a background which it will render itself -/// onto and a function which takes a Vector2D position and returns which tile should be rendered there. +/// onto and a function which takes a `Vector2D` position and returns which tile should be rendered there. /// /// The passed function should handle being out of bounds, as the scrolled map does buffer around the edges slightly. /// diff --git a/agb/src/input.rs b/agb/src/input.rs index c36866e4..22b3e3dd 100644 --- a/agb/src/input.rs +++ b/agb/src/input.rs @@ -1,11 +1,32 @@ +#![deny(missing_docs)] use crate::fixnum::Vector2D; use bitflags::bitflags; use core::convert::From; +/// Tri-state enum. Allows for -1, 0 and +1. +/// Useful if checking if the D-Pad is pointing left, right, or unpressed. +/// +/// Note that [Tri] can be converted directly to a signed integer, so can easily be used to update positions of things in games +/// +/// # Examples +/// ```rust,no_run +/// # #![no_std] +/// use agb::input::Tri; +/// +/// # fn main() { +/// let x = 5; +/// let tri = Tri::Positive; // e.g. from button_controller.x_tri() +/// +/// assert_eq!(x + tri as i32, 6); +/// # } +/// ``` #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub enum Tri { + /// Right or down Positive = 1, + /// Unpressed Zero = 0, + /// Left or up Negative = -1, } @@ -18,16 +39,27 @@ impl From<(bool, bool)> for Tri { } bitflags! { + /// Represents a button on the GBA pub struct Button: u32 { + /// The A button const A = 1 << 0; + /// The B button const B = 1 << 1; + /// The SELECT button const SELECT = 1 << 2; + /// The START button const START = 1 << 3; + /// The RIGHT button on the D-Pad const RIGHT = 1 << 4; + /// The LEFT button on the D-Pad const LEFT = 1 << 5; + /// The UP button on the D-Pad const UP = 1 << 6; + /// The DOWN button on the D-Pad const DOWN = 1 << 7; + /// The R button on the D-Pad const R = 1 << 8; + /// The L button on the D-Pad const L = 1 << 9; } } @@ -36,6 +68,28 @@ const BUTTON_INPUT: *mut u16 = (0x04000130) as *mut u16; // const BUTTON_INTURRUPT: *mut u16 = (0x04000132) as *mut u16; +/// Helper to make it easy to get the current state of the GBA's buttons. +/// +/// # Example +/// +/// ```rust,no_run +/// # #![no_std] +/// use agb::input::{ButtonController, Tri}; +/// +/// # fn main() { +/// let mut input = ButtonController::new(); +/// +/// loop { +/// input.update(); // call update every loop +/// +/// match input.x_tri() { +/// Tri::Negative => { /* left is being pressed */ } +/// Tri::Positive => { /* right is being pressed */ } +/// Tri::Zero => { /* Neither left nor right (or both) are pressed */ } +/// } +/// } +/// # } +/// ``` pub struct ButtonController { previous: u16, current: u16, @@ -48,6 +102,8 @@ impl Default for ButtonController { } impl ButtonController { + /// Create a new ButtonController. + /// This is the preferred way to create it. #[must_use] pub fn new() -> Self { let pressed = !unsafe { BUTTON_INPUT.read_volatile() }; @@ -57,11 +113,16 @@ impl ButtonController { } } + /// Updates the state of the button controller. + /// You should call this every frame (either at the start or the end) to ensure that you have the latest state of each button press. + /// Calls to any method won't change until you call this. pub fn update(&mut self) { self.previous = self.current; self.current = !unsafe { BUTTON_INPUT.read_volatile() }; } + /// Returns [Tri::Positive] if right is pressed, [Tri::Negative] if left is pressed and [Tri::Zero] if neither or both are pressed. + /// This is the normal behaviour you'll want if you're using orthogonal inputs. #[must_use] pub fn x_tri(&self) -> Tri { let left = self.is_pressed(Button::LEFT); @@ -70,6 +131,8 @@ impl ButtonController { (left, right).into() } + /// Returns [Tri::Positive] if down is pressed, [Tri::Negative] if up is pressed and [Tri::Zero] if neither or both are pressed. + /// This is the normal behaviour you'll want if you're using orthogonal inputs. #[must_use] pub fn y_tri(&self) -> Tri { let up = self.is_pressed(Button::UP); @@ -78,6 +141,7 @@ impl ButtonController { (up, down).into() } + /// Returns true if all the buttons specified in `keys` are pressed. #[must_use] pub fn vector(&self) -> Vector2D where @@ -87,6 +151,9 @@ impl ButtonController { } #[must_use] + /// Returns [Tri::Positive] if left was just pressed, [Tri::Negative] if right was just pressed and [Tri::Zero] if neither or both are just pressed. + /// + /// Also returns [Tri::Zero] after the call to [`update()`](ButtonController::update()) if the button is still held. pub fn just_pressed_x_tri(&self) -> Tri { let left = self.is_just_pressed(Button::LEFT); let right = self.is_just_pressed(Button::RIGHT); @@ -95,6 +162,9 @@ impl ButtonController { } #[must_use] + /// Returns [Tri::Positive] if down was just pressed, [Tri::Negative] if up was just pressed and [Tri::Zero] if neither or both are just pressed. + /// + /// Also returns [Tri::Zero] after the call to [`update()`](ButtonController::update()) if the button is still held. pub fn just_pressed_y_tri(&self) -> Tri { let up = self.is_just_pressed(Button::UP); let down = self.is_just_pressed(Button::DOWN); @@ -103,6 +173,7 @@ impl ButtonController { } #[must_use] + /// Returns a vector which represents the direction the button was just pressed in. pub fn just_pressed_vector(&self) -> Vector2D where T: From + crate::fixnum::FixedWidthUnsignedInteger, @@ -115,17 +186,39 @@ impl ButtonController { } #[must_use] + /// Returns `true` if the provided keys are all pressed, and `false` if not. pub fn is_pressed(&self, keys: Button) -> bool { let currently_pressed = u32::from(self.current); let keys = keys.bits(); (currently_pressed & keys) != 0 } + /// Returns true if all the buttons specified in `keys` are not pressed. Equivalent to `!is_pressed(keys)`. #[must_use] pub fn is_released(&self, keys: Button) -> bool { !self.is_pressed(keys) } + /// Returns true if all the buttons specified in `keys` went from not pressed to pressed in the last frame. + /// Very useful for menu navigation or selection if you want the players actions to only happen for one frame. + /// + /// # Example + /// ```no_run,rust + /// # #![no_std] + /// use agb::input::{Button, ButtonController}; + /// + /// # fn main() { + /// let mut button_controller = ButtonController::new(); + /// + /// loop { + /// button_controller.update(); + /// + /// if button_controller.is_just_pressed(Button::A) { + /// // A button was just pressed, maybe select the currently selected item + /// } + /// } + /// # } + /// ``` #[must_use] pub fn is_just_pressed(&self, keys: Button) -> bool { let current = u32::from(self.current); @@ -134,6 +227,8 @@ impl ButtonController { ((current & keys) != 0) && ((previous & keys) == 0) } + /// Returns true if all the buttons specified in `keys` went from pressed to not pressed in the last frame. + /// Very useful for menu navigation or selection if you want players actions to only happen for one frame. #[must_use] pub fn is_just_released(&self, keys: Button) -> bool { let current = u32::from(self.current); diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 1c0c587a..195edf9a 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -24,6 +24,7 @@ #![deny(clippy::cloned_instead_of_copied)] #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] +#![deny(rustdoc::invalid_html_tags)] //! # agb //! `agb` is a library for making games on the Game Boy Advance using the Rust diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index be3bdd75..41b154d2 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -7,7 +7,8 @@ - [Linux setup](./setup/linux.md) - [Windows setup]() - [Mac OS setup]() - - [Building the game](./setup/building.md) + - [Building the template](./setup/building.md) - [Learn agb part I - pong](./pong/01_introduction.md) - [The Gba struct](./pong/02_the_gba_struct.md) - [Sprites](./pong/03_sprites.md) + - [Controls](./pong/04_controls.md) diff --git a/book/src/introduction/introduction.md b/book/src/introduction/introduction.md index ec14a26e..4529d83e 100644 --- a/book/src/introduction/introduction.md +++ b/book/src/introduction/introduction.md @@ -26,4 +26,5 @@ It is super rewarding being able to play a game you made yourself on a piece of * [agb's crates.io page](https://crates.io/crates/agb) * [agb's documentation](https://docs.rs/agb) which is useful if you need a quick reference * [Awesome Game Boy Advance development](https://github.com/gbdev/awesome-gbadev) contains links to popular libraries, emulators and the super friendly gbadev discord -* [Example game](https://lostimmortal.itch.io/the-hat-chooses-the-wizard) written using agb as part of the 2021 GMTK game jam. \ No newline at end of file +* [Example game](https://lostimmortal.itch.io/the-hat-chooses-the-wizard) written using agb as part of the 2021 GMTK game jam. +* [More example games](https://github.com/agbrs/agb/releases/latest) built using agb. See them in `examples.zip` attached to the latest release. \ No newline at end of file diff --git a/book/src/pong/03_sprites.md b/book/src/pong/03_sprites.md index 77df0726..12401f9f 100644 --- a/book/src/pong/03_sprites.md +++ b/book/src/pong/03_sprites.md @@ -27,7 +27,8 @@ For our pong game, all the sprites will be 16x16 pixels to make things a bit sim Sprites are stored in the Game Boy Advance in a special area of video memory called the 'Object Attribute Memory' (OAM). This has space for the 'attributes' of the sprites (things like whether or not they are visible, the location, which tile to use etc) but it does not store the actual pixel data. -The pixel data is stored in a different part of video RAM (VRAM) and the OAM only stores which tiles to use from this area. +The pixel data is stored in a video RAM (VRAM). +Because of this split, it is possible to have multiple sprites refer to the same tiles in video RAM which saves space and allows for more objects on screen at once then repeating them would otherwise allow. Since RAM is in short supply, and at the time was quite expensive, the tile data is stored as indexed palette data. So rather than storing the full colour data for each pixel in the tile, the Game Boy Advance instead stores a 'palette' of colours and the tiles which make up the sprites are stored as indexes to the palette. @@ -52,7 +53,8 @@ The third until the fifth is the ball, with various squashed states. The aseprite file defines tags for these sprites, being "Paddle End", "Paddle Mid", and "Ball". ```rust -use agb::{include_aseprite, +use agb::{ + include_aseprite, display::object::{Graphics, Tag} }; @@ -74,7 +76,7 @@ Using the `Gba` struct we get the [`ObjectController` struct](https://docs.rs/ag ```rust #[agb::entry] fn main(gba: mut agb::Gba) -> ! { - // Get the OAM manager + // Get the object manager let object = gba.display.object.get(); // Create an object with the ball sprite @@ -83,8 +85,9 @@ fn main(gba: mut agb::Gba) -> ! { // Place this at some point on the screen, (50, 50) for example ball.set_x(50).set_y(50).show(); - // Now commit the object controller so this change is reflected on the screen, - // this should normally be done in vblank but it'll work just fine here for now + // Now commit the object controller so this change is reflected on the screen. + // This isn't how we will do this in the final version of the code, but will do + // for this example. object.commit(); loop {} @@ -95,11 +98,16 @@ If you run this you should now see the ball for this pong game somewhere in the # Making the sprite move -As mentioned before, you should `.commit()` your sprites only during `vblank` which is the (very short) period of time nothing is being rendered to screen. -`agb` provides a convenience function for waiting until this happens called `agb::display::busy_wait_for_vblank()`. +The GBA renders to the screen one pixel at a time a line at a time from left to right. +After it has finished rendering to each pixel of the screen, it briefly pauses rendering before starting again. +This period of no drawing is called `vblank`, which stands for the 'vertical blanking interval'. +There is also a 'horizontal blanking interval', but that is outside of the scope of this book. + +You should `.commit()` your sprites only during this `vblank` phase, because otherwise you may end up moving a sprite during the rendering which could cause tearing of your objects[^hblank]. +`agb` provides a convenience function for waiting until the right moment called `agb::display::busy_wait_for_vblank()`. You shouldn't use this is a real game (we'll do it properly later on), but for now we can use this to wait for the correct time to `commit` our sprites to memory. -Making the sprite move 1 pixel every frame (so approximately 60 pixels per second) can be done as follows: +Making the sprite move 1 pixel every frame (so 60 pixels per second) can be done as follows: ```rust // replace the call to object.commit() with the following: @@ -135,4 +143,7 @@ loop { # What we did -In this section, we covered why sprites are important, how to create and manage them using the `ObjectController` in `agb` and make a ball bounce around the screen. \ No newline at end of file +In this section, we covered why sprites are important, how to create and manage them using the `ObjectController` in `agb` and make a ball bounce around the screen. + +[^hblank]: Timing this can give you some really cool effects allowing you to push the hardware. + However, `agb` does not by default provide the timing accuracy needed to fully take advantage of this, erring on the side of making it easier to make games rather than squeezing every last drop of performance from the console. \ No newline at end of file diff --git a/book/src/pong/04_controls.md b/book/src/pong/04_controls.md new file mode 100644 index 00000000..a324f5c7 --- /dev/null +++ b/book/src/pong/04_controls.md @@ -0,0 +1,82 @@ +# Controls + +In this section, we'll make the ball that we displayed in the last section move by pressing the D-Pad. + +# The GBA controls + +The GBA has 10 buttons we can read the state of, and this is the only way a player can directly control the game. +They are the 4 directions on the D-Pad, A, B, Start, Select, and the L and R triggers. + +# Reading the button state + +There are two ways to capture the button state in **agb**, interrupts and polling. +In most games, you will want to use polling, so that is what we will use now. +Interrupts will be covered in a later chapter. + +To add button control to our game, we will need a [ButtonController](https://docs.rs/agb/latest/agb/input/struct.ButtonController.html). +Add this near the top of your main function: + +```rust +let mut input = agb::input::ButtonController::new(); +``` + +The button controller is not part of the `Gba` struct because it only allows for reading and not writing so does not need to be controlled by the borrow checker. + +Replace the inner loop with the following: + +```rust +let mut ball_x = 50; +let mut ball_y = 50; + +// now we initialise the x and y velocities to 0 rather than 1 +let mut x_velocity = 0; +let mut y_velocity = 0; + +loop { + ball_x = (ball_x + x_velocity).clamp(0, agb::display::WIDTH - 16); + ball_y = (ball_y + y_velocity).clamp(0, agb::display::HEIGHT - 16); + + // x_tri and y_tri describe with -1, 0 and 1 which way the d-pad + // buttons are being pressed + x_velocity = input.x_tri() as i32; + y_velocity = input.y_tri() as i32; + + ball.set_x(ball_x as u16).set_y(ball_y as u16); + + agb::display::busy_wait_for_vblank(); + object.commit(); + + // We must call input.update() every frame otherwise it won't update based + // on the actual button press state. + input.update(); +} +``` + +Here we use the `x_tri()` and `y_tri()` methods. +They return instances of the [`Tri`](https://docs.rs/agb/latest/agb/input/enum.Tri.html) enum which describes which buttons are being pressed, and are very helpful in situations like these where you want to move something in a cardinal direction based on which buttons are pressed. + +# Detecting individual button presses + +If you want to detect if any button is pressed, you can use the `is_pressed` method on `ButtonController`. +For example, we can do the following: + +```rust +use agb::input::Button; + +if input.is_pressed(Button::A) { + // the A button is pressed +} +``` + +`ButtonController` also provides the `is_just_pressed` method. +This will return true for 1 frame, the one where the player actually pressed the button. +From that point on, it'll return false again until the player presses it again. + +# What we did + +We added very basic button control to our bouncing ball example. +In the next step, we'll cover meta-sprites and actually add a bat to our game of pong. + +# Exercise + +Make it so the ball moves twice as fast if you're pressing the `A` button while moving it around. \ No newline at end of file diff --git a/book/src/setup/building.md b/book/src/setup/building.md index 2ce23aec..4b96811e 100644 --- a/book/src/setup/building.md +++ b/book/src/setup/building.md @@ -1,34 +1,31 @@ -# Building the game +# Building the template -By the end of this section, you should be able to build and run an example game made using agb! -**This section is optional.** -If you just want to get straight into building your own games, you don't need to do this. -However, we recommended doing this section to prove that your setup works. +By the end of this section, you should be able to build and run the **agb** template. # 1. Get the source code -The source code can be fetched using `git clone https://github.com/agbrs/joinedtogether.git`. +The source code can be fetched using `git clone https://github.com/agbrs/template.git`. -# 2. Build the game +# 2. Build the template -Build a copy of the game using `cargo build --release`. -This could take quite a while, but eventually you'll end up with a copy of the game in `target/thumbv4t-none-eabi/release/joinedtogether` or `target/thumbv4t-none-eabi/release/joinedtogether.elf` depending on platform. +Build a copy of the template using `cargo build --release`. +This could take quite a while, but eventually you'll end up with a copy of the template in `target/thumbv4t-none-eabi/release/template` or `target/thumbv4t-none-eabi/release/template.elf` depending on platform. This can be run directly by some emulators, but we need to run an extra step in order to convert the elf file into a '.gba' file. ```sh -arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/joinedtogether joinedtogether.gba -gbafix joinedtogether.gba +arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/template template.gba +gbafix template.gba ``` or ```sh -arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/joinedtogether.elf joinedtogether.gba -gbafix joinedtogether.gba +arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/template.elf template.gba +gbafix template.gba ``` And then load the resulting file in your emulator of choice. That's all there is to it! -If you have `mgba-qt` in your path, then you can launch the game directly using `cargo run --release` \ No newline at end of file +If you have `mgba-qt` in your path, then you can launch the template directly using `cargo run --release`. \ No newline at end of file