Update book and document controls (#332)

Adds a section about controls and documents the input module

- [x] Changelog updated / no changelog update needed
This commit is contained in:
Gwilym Kuiper 2022-11-01 22:08:41 +00:00 committed by GitHub
commit 9526def3f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 214 additions and 26 deletions

View file

@ -14,7 +14,7 @@ use crate::{
/// The abstraction allows only for static tiles, but it is possible to animate the tiles if needed. /// 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 /// 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<i32> position and returns which tile should be rendered there. /// onto and a function which takes a `Vector2D<i32>` 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. /// The passed function should handle being out of bounds, as the scrolled map does buffer around the edges slightly.
/// ///

View file

@ -1,11 +1,32 @@
#![deny(missing_docs)]
use crate::fixnum::Vector2D; use crate::fixnum::Vector2D;
use bitflags::bitflags; use bitflags::bitflags;
use core::convert::From; 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)] #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum Tri { pub enum Tri {
/// Right or down
Positive = 1, Positive = 1,
/// Unpressed
Zero = 0, Zero = 0,
/// Left or up
Negative = -1, Negative = -1,
} }
@ -18,16 +39,27 @@ impl From<(bool, bool)> for Tri {
} }
bitflags! { bitflags! {
/// Represents a button on the GBA
pub struct Button: u32 { pub struct Button: u32 {
/// The A button
const A = 1 << 0; const A = 1 << 0;
/// The B button
const B = 1 << 1; const B = 1 << 1;
/// The SELECT button
const SELECT = 1 << 2; const SELECT = 1 << 2;
/// The START button
const START = 1 << 3; const START = 1 << 3;
/// The RIGHT button on the D-Pad
const RIGHT = 1 << 4; const RIGHT = 1 << 4;
/// The LEFT button on the D-Pad
const LEFT = 1 << 5; const LEFT = 1 << 5;
/// The UP button on the D-Pad
const UP = 1 << 6; const UP = 1 << 6;
/// The DOWN button on the D-Pad
const DOWN = 1 << 7; const DOWN = 1 << 7;
/// The R button on the D-Pad
const R = 1 << 8; const R = 1 << 8;
/// The L button on the D-Pad
const L = 1 << 9; 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; // 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 { pub struct ButtonController {
previous: u16, previous: u16,
current: u16, current: u16,
@ -48,6 +102,8 @@ impl Default for ButtonController {
} }
impl ButtonController { impl ButtonController {
/// Create a new ButtonController.
/// This is the preferred way to create it.
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
let pressed = !unsafe { BUTTON_INPUT.read_volatile() }; 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) { pub fn update(&mut self) {
self.previous = self.current; self.previous = self.current;
self.current = !unsafe { BUTTON_INPUT.read_volatile() }; 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] #[must_use]
pub fn x_tri(&self) -> Tri { pub fn x_tri(&self) -> Tri {
let left = self.is_pressed(Button::LEFT); let left = self.is_pressed(Button::LEFT);
@ -70,6 +131,8 @@ impl ButtonController {
(left, right).into() (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] #[must_use]
pub fn y_tri(&self) -> Tri { pub fn y_tri(&self) -> Tri {
let up = self.is_pressed(Button::UP); let up = self.is_pressed(Button::UP);
@ -78,6 +141,7 @@ impl ButtonController {
(up, down).into() (up, down).into()
} }
/// Returns true if all the buttons specified in `keys` are pressed.
#[must_use] #[must_use]
pub fn vector<T>(&self) -> Vector2D<T> pub fn vector<T>(&self) -> Vector2D<T>
where where
@ -87,6 +151,9 @@ impl ButtonController {
} }
#[must_use] #[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 { pub fn just_pressed_x_tri(&self) -> Tri {
let left = self.is_just_pressed(Button::LEFT); let left = self.is_just_pressed(Button::LEFT);
let right = self.is_just_pressed(Button::RIGHT); let right = self.is_just_pressed(Button::RIGHT);
@ -95,6 +162,9 @@ impl ButtonController {
} }
#[must_use] #[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 { pub fn just_pressed_y_tri(&self) -> Tri {
let up = self.is_just_pressed(Button::UP); let up = self.is_just_pressed(Button::UP);
let down = self.is_just_pressed(Button::DOWN); let down = self.is_just_pressed(Button::DOWN);
@ -103,6 +173,7 @@ impl ButtonController {
} }
#[must_use] #[must_use]
/// Returns a vector which represents the direction the button was just pressed in.
pub fn just_pressed_vector<T>(&self) -> Vector2D<T> pub fn just_pressed_vector<T>(&self) -> Vector2D<T>
where where
T: From<i32> + crate::fixnum::FixedWidthUnsignedInteger, T: From<i32> + crate::fixnum::FixedWidthUnsignedInteger,
@ -115,17 +186,39 @@ impl ButtonController {
} }
#[must_use] #[must_use]
/// Returns `true` if the provided keys are all pressed, and `false` if not.
pub fn is_pressed(&self, keys: Button) -> bool { pub fn is_pressed(&self, keys: Button) -> bool {
let currently_pressed = u32::from(self.current); let currently_pressed = u32::from(self.current);
let keys = keys.bits(); let keys = keys.bits();
(currently_pressed & keys) != 0 (currently_pressed & keys) != 0
} }
/// Returns true if all the buttons specified in `keys` are not pressed. Equivalent to `!is_pressed(keys)`.
#[must_use] #[must_use]
pub fn is_released(&self, keys: Button) -> bool { pub fn is_released(&self, keys: Button) -> bool {
!self.is_pressed(keys) !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] #[must_use]
pub fn is_just_pressed(&self, keys: Button) -> bool { pub fn is_just_pressed(&self, keys: Button) -> bool {
let current = u32::from(self.current); let current = u32::from(self.current);
@ -134,6 +227,8 @@ impl ButtonController {
((current & keys) != 0) && ((previous & keys) == 0) ((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] #[must_use]
pub fn is_just_released(&self, keys: Button) -> bool { pub fn is_just_released(&self, keys: Button) -> bool {
let current = u32::from(self.current); let current = u32::from(self.current);

View file

@ -24,6 +24,7 @@
#![deny(clippy::cloned_instead_of_copied)] #![deny(clippy::cloned_instead_of_copied)]
#![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::private_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)]
#![deny(rustdoc::invalid_html_tags)]
//! # agb //! # agb
//! `agb` is a library for making games on the Game Boy Advance using the Rust //! `agb` is a library for making games on the Game Boy Advance using the Rust

View file

@ -7,7 +7,8 @@
- [Linux setup](./setup/linux.md) - [Linux setup](./setup/linux.md)
- [Windows setup]() - [Windows setup]()
- [Mac OS 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) - [Learn agb part I - pong](./pong/01_introduction.md)
- [The Gba struct](./pong/02_the_gba_struct.md) - [The Gba struct](./pong/02_the_gba_struct.md)
- [Sprites](./pong/03_sprites.md) - [Sprites](./pong/03_sprites.md)
- [Controls](./pong/04_controls.md)

View file

@ -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 crates.io page](https://crates.io/crates/agb)
* [agb's documentation](https://docs.rs/agb) which is useful if you need a quick reference * [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 * [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. * [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.

View file

@ -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). 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. 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. 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. 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". The aseprite file defines tags for these sprites, being "Paddle End", "Paddle Mid", and "Ball".
```rust ```rust
use agb::{include_aseprite, use agb::{
include_aseprite,
display::object::{Graphics, Tag} display::object::{Graphics, Tag}
}; };
@ -74,7 +76,7 @@ Using the `Gba` struct we get the [`ObjectController` struct](https://docs.rs/ag
```rust ```rust
#[agb::entry] #[agb::entry]
fn main(gba: mut agb::Gba) -> ! { fn main(gba: mut agb::Gba) -> ! {
// Get the OAM manager // Get the object manager
let object = gba.display.object.get(); let object = gba.display.object.get();
// Create an object with the ball sprite // 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 // Place this at some point on the screen, (50, 50) for example
ball.set_x(50).set_y(50).show(); ball.set_x(50).set_y(50).show();
// Now commit the object controller so this change is reflected on the screen, // 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 // This isn't how we will do this in the final version of the code, but will do
// for this example.
object.commit(); object.commit();
loop {} 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 # 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. The GBA renders to the screen one pixel at a time a line at a time from left to right.
`agb` provides a convenience function for waiting until this happens called `agb::display::busy_wait_for_vblank()`. 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. 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 ```rust
// replace the call to object.commit() with the following: // replace the call to object.commit() with the following:
@ -135,4 +143,7 @@ loop {
# What we did # 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. 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.

View file

@ -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.

View file

@ -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! By the end of this section, you should be able to build and run the **agb** template.
**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.
# 1. Get the source code # 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`. 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 game in `target/thumbv4t-none-eabi/release/joinedtogether` or `target/thumbv4t-none-eabi/release/joinedtogether.elf` depending on platform. 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. 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 ```sh
arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/joinedtogether joinedtogether.gba arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/template template.gba
gbafix joinedtogether.gba gbafix template.gba
``` ```
or or
```sh ```sh
arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/joinedtogether.elf joinedtogether.gba arm-none-eabi-objcopy -O binary target/thumbv4t-none-eabi/release/template.elf template.gba
gbafix joinedtogether.gba gbafix template.gba
``` ```
And then load the resulting file in your emulator of choice. And then load the resulting file in your emulator of choice.
That's all there is to it! 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` If you have `mgba-qt` in your path, then you can launch the template directly using `cargo run --release`.