mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 16:21:33 +11:00
Merge pull request #272 from corwinkuiper/sprites-intro-book
update the sprites page of the book
This commit is contained in:
commit
2fd41869ec
Binary file not shown.
|
@ -10,33 +10,52 @@
|
|||
// which won't be a particularly clear error message.
|
||||
#![no_main]
|
||||
|
||||
use agb::display::object::{Graphics, Tag};
|
||||
use agb::Gba;
|
||||
use agb::{
|
||||
display::object::{Graphics, Tag},
|
||||
include_aseprite,
|
||||
};
|
||||
|
||||
const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/sprites.aseprite");
|
||||
// Import the sprites in to this constant. This holds the sprite
|
||||
// and palette data in a way that is manageable by agb.
|
||||
const GRAPHICS: &Graphics = include_aseprite!("gfx/sprites.aseprite");
|
||||
|
||||
// We define some easy ways of referencing the sprites
|
||||
#[allow(dead_code)]
|
||||
const PADDLE_END: &Tag = GRAPHICS.tags().get("Paddle End");
|
||||
#[allow(dead_code)]
|
||||
const PADDLE_MID: &Tag = GRAPHICS.tags().get("Paddle Mid");
|
||||
const BALL: &Tag = GRAPHICS.tags().get("Ball");
|
||||
|
||||
// The main function must take 0 arguments and never return. The agb::entry decorator
|
||||
// ensures that everything is in order. `agb` will call this after setting up the stack
|
||||
// and interrupt handlers correctly.
|
||||
#[agb::entry]
|
||||
fn main(mut gba: Gba) -> ! {
|
||||
fn main(mut gba: agb::Gba) -> ! {
|
||||
// Get the OAM manager
|
||||
let object = gba.display.object.get();
|
||||
|
||||
const BALL: &Tag = GRAPHICS.tags().get("Ball");
|
||||
let ball_sprite = object.sprite(BALL.sprite(0));
|
||||
let mut ball = object.object(ball_sprite);
|
||||
// Create an object with the ball sprite
|
||||
let mut ball = object.object_sprite(BALL.sprite(0));
|
||||
|
||||
// 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
|
||||
object.commit();
|
||||
|
||||
let mut ball_x = 50;
|
||||
let mut ball_y = 50;
|
||||
let mut x_velocity = 1;
|
||||
let mut y_velocity = 1;
|
||||
|
||||
loop {
|
||||
// This will calculate the new position and enforce the position
|
||||
// of the ball remains within the screen
|
||||
ball_x = (ball_x + x_velocity).clamp(0, agb::display::WIDTH - 16);
|
||||
ball_y = (ball_y + y_velocity).clamp(0, agb::display::HEIGHT - 16);
|
||||
|
||||
// We check if the ball reaches the edge of the screen and reverse it's direction
|
||||
if ball_x == 0 || ball_x == agb::display::WIDTH - 16 {
|
||||
x_velocity = -x_velocity;
|
||||
}
|
||||
|
@ -45,8 +64,10 @@ fn main(mut gba: Gba) -> ! {
|
|||
y_velocity = -y_velocity;
|
||||
}
|
||||
|
||||
// Set the position of the ball to match our new calculated position
|
||||
ball.set_x(ball_x as u16).set_y(ball_y as u16);
|
||||
|
||||
// Wait for vblank, then commit the objects to the screen
|
||||
agb::display::busy_wait_for_vblank();
|
||||
object.commit();
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
- [Introduction](./introduction/introduction.md)
|
||||
- [The Game Boy Advance hardware](./hardware/hardware.md)
|
||||
- [Running an example](./setup/getting_started.md)
|
||||
- [Environment setup](./setup/setup.md)
|
||||
- [Linux setup](./setup/linux.md)
|
||||
- [Windows setup]()
|
||||
- [Mac OS setup]()
|
||||
- [Building the game](./setup/building.md)
|
||||
- [Environment setup](./setup/setup.md)
|
||||
- [Linux setup](./setup/linux.md)
|
||||
- [Windows setup]()
|
||||
- [Mac OS setup]()
|
||||
- [Building the game](./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) -->
|
||||
- [The Gba struct](./pong/02_the_gba_struct.md)
|
||||
- [Sprites](./pong/03_sprites.md)
|
||||
|
|
|
@ -38,11 +38,10 @@ For now, we will only be dealing with regular sprites.
|
|||
|
||||
# Import the sprite
|
||||
|
||||
As mentioned above, we'll need to convert the sprite data into a format that the Game Boy Advance will be able to understand (so palette information and tile data).
|
||||
Once we've converted it, we'll need to import this tile data into the Game Boy Advance's memory on start up and then create a sprite in the OAM.
|
||||
|
||||
Firstly, you're going to need to import the sprites into your project.
|
||||
Save the following image into a new folder called `gfx` in your project:
|
||||
`agb` has great support for the [aseprite](https://www.aseprite.org/) sprite editor which can be bought for $20 or you can compile it yourself for free.
|
||||
Aseprite files can be natively imported by `agb` for use on the Game Boy Advance.
|
||||
Here is the sprite sheet we will use as a png, but you should [download the aseprite file](sprites.aseprite) and place it in `gfx/sprites.aseprite`.
|
||||
|
||||
![pong sprites](sprites.png)
|
||||
|
||||
|
@ -50,89 +49,48 @@ This contains 5 `16x16px` sprites.
|
|||
The first is the end cap for the paddle.
|
||||
The second is the centre part of the paddle, which could potentially be repeated a few times.
|
||||
The third until the fifth is the ball, with various squashed states.
|
||||
The background is a lovely shade of `#ff0044` which we will use for the transparency.
|
||||
|
||||
`agb` needs to know what the tile size is, and also what the transparent colour is so that it can properly produce data that the Game Boy Advance can understand.
|
||||
So you need to create a manifest file for the image, which is declared in a toml file.
|
||||
|
||||
In the same `gfx` folder as the `sprites.png` file, also create a `sprites.toml` file with the following content:
|
||||
|
||||
```toml
|
||||
version = "1.0"
|
||||
|
||||
[image.sprites]
|
||||
filename = "sprites.png"
|
||||
tile_size = "16x16"
|
||||
transparent_colour = "ff0044"
|
||||
```
|
||||
|
||||
Now let's create a module in the `main.rs` file which imports the sprite sheet and loads it into memory.
|
||||
Anything sprite related is managed by the [`ObjectControl` struct](https://docs.rs/agb/0.8.0/agb/display/object/struct.ObjectControl.html).
|
||||
So we use that to load the sprite tile map and palette data.
|
||||
The aseprite file defines tags for these sprites, being "Paddle End", "Paddle Mid", and "Ball".
|
||||
|
||||
```rust
|
||||
// Put all the graphics related code in the gfx module
|
||||
mod gfx {
|
||||
use agb::display::object::ObjectControl;
|
||||
use agb::{include_aseprite,
|
||||
display::object::{Graphics, Tag}
|
||||
};
|
||||
|
||||
// Import the sprites into this module. This will create a `sprites` module
|
||||
// and within that will be a constant called `sprites` which houses all the
|
||||
// palette and tile data.
|
||||
agb::include_gfx!("gfx/sprites.toml");
|
||||
// Import the sprites in to this constant. This holds the sprite
|
||||
// and palette data in a way that is manageable by agb.
|
||||
const GRAPHICS: &Graphics = include_aseprite!("gfx/sprites.aseprite");
|
||||
|
||||
// Loads the sprites tile data and palette data into VRAM
|
||||
pub fn load_sprite_data(object: &mut ObjectControl) {
|
||||
object.set_sprite_palettes(sprites::sprites.palettes);
|
||||
object.set_sprite_tilemap(sprites::sprites.tiles);
|
||||
}
|
||||
// We define some easy ways of referencing the sprites
|
||||
const PADDLE_END: &Tag = GRAPHICS.tags().get("Paddle End");
|
||||
const PADDLE_MID: &Tag = GRAPHICS.tags().get("Paddle Mid");
|
||||
const BALL: &Tag = GRAPHICS.tags().get("Ball");
|
||||
```
|
||||
|
||||
This uses the `include_aseprite` macro to include the sprites in the given aseprite file.
|
||||
Now, let's put this on screen by firstly creating the object manager and then creating an object, this will also involve the creation of the main entry function using the `entry` macro.
|
||||
The signature of this function takes the `Gba` struct and has the never return type, this means Rust will enforce that this function never returns, for now we will achieve this using a busy loop.
|
||||
Using the `Gba` struct we get the [`ObjectController` struct](https://docs.rs/agb/latest/agb/display/object/struct.ObjectController.html) which manages loading and unloading sprites and objects.
|
||||
|
||||
```rust
|
||||
#[agb::entry]
|
||||
fn main(gba: mut agb::Gba) -> ! {
|
||||
// Get the OAM manager
|
||||
let object = gba.display.object.get();
|
||||
|
||||
// Create an object with the ball sprite
|
||||
let mut ball = object.object_sprite(BALL.sprite(0));
|
||||
|
||||
// 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
|
||||
object.commit();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
This uses the `include_gfx!` macro which loads the sprite information file and grabs the relevant tile data from there.
|
||||
|
||||
Now, let's put this on screen by firstly creating the object manager and loading the sprite data from above:
|
||||
|
||||
```rust
|
||||
let mut gba = Gba::new();
|
||||
|
||||
// set background mode to mode 0 (we'll cover this in more detail later)
|
||||
// for now, this is required in order for anything to show up on screen at all.
|
||||
let _tiled = gba.display.video.tiled0();
|
||||
|
||||
// Get the OAM manager
|
||||
let mut object = gba.display.object.get();
|
||||
gfx::load_sprite_data(&mut object);
|
||||
object.enable();
|
||||
```
|
||||
|
||||
Then, let's create the ball object and put it at 50, 50 on the screen:
|
||||
|
||||
```rust
|
||||
// continued from above:
|
||||
let mut ball = object.get_object_standard(); // (1)
|
||||
|
||||
ball.set_x(50) // (2)
|
||||
.set_y(50)
|
||||
.set_sprite_size(Size::S16x16)
|
||||
.set_tile_id(4 * 2)
|
||||
.show();
|
||||
|
||||
ball.commit(); // (3)
|
||||
```
|
||||
|
||||
There are a few new things introduced here, so lets go through these 1 by 1.
|
||||
|
||||
1. The first thing we're doing is creating a 'standard' object.
|
||||
These are of non-affine type.
|
||||
2. We now set some properties on the ball.
|
||||
The position and size are self explanatory.
|
||||
Interesting here is that the tile id is 4 * 2.
|
||||
This is because the tile id is calculated in 8x8 pixel chunks, and in our example we have 16x16 sprites so each sprite takes 4 tiles and this is the tile in position 2 on the tilesheet above (0 indexed).
|
||||
The final call to `.show()` will make it actually visible as sprites are hidden by default.
|
||||
3. The call to `.commit()` actually makes the change to object memory.
|
||||
Until `.commit()` is called, no changes you made will actually be visible.
|
||||
This is very handy because we might want to change sprite positions etc while the frame is rendering, and then move them in the short space of time we have before the next frame starts rendering (vblank).
|
||||
|
||||
If you run this you should now see the ball for this pong game somewhere in the top left of the screen.
|
||||
|
||||
# Making the sprite move
|
||||
|
@ -144,7 +102,7 @@ You shouldn't use this is a real game (we'll do it properly later on), but for n
|
|||
Making the sprite move 1 pixel every frame (so approximately 60 pixels per second) can be done as follows:
|
||||
|
||||
```rust
|
||||
// replace the call to ball.commit() with the following:
|
||||
// replace the call to object.commit() with the following:
|
||||
|
||||
let mut ball_x = 50;
|
||||
let mut ball_y = 50;
|
||||
|
@ -152,9 +110,12 @@ let mut x_velocity = 1;
|
|||
let mut y_velocity = 1;
|
||||
|
||||
loop {
|
||||
// This will calculate the new position and enforce the position
|
||||
// of the ball remains within the screen
|
||||
ball_x = (ball_x + x_velocity).clamp(0, agb::display::WIDTH - 16);
|
||||
ball_y = (ball_y + y_velocity).clamp(0, agb::display::HEIGHT - 16);
|
||||
|
||||
// We check if the ball reaches the edge of the screen and reverse it's direction
|
||||
if ball_x == 0 || ball_x == agb::display::WIDTH - 16 {
|
||||
x_velocity = -x_velocity;
|
||||
}
|
||||
|
@ -163,13 +124,15 @@ loop {
|
|||
y_velocity = -y_velocity;
|
||||
}
|
||||
|
||||
// Set the position of the ball to match our new calculated position
|
||||
ball.set_x(ball_x as u16).set_y(ball_y as u16);
|
||||
|
||||
// Wait for vblank, then commit the objects to the screen
|
||||
agb::display::busy_wait_for_vblank();
|
||||
ball.commit();
|
||||
object.commit();
|
||||
}
|
||||
```
|
||||
|
||||
# What we did
|
||||
|
||||
In this section, we covered why sprites are important, how to create and manage them using the `ObjectControl` 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.
|
BIN
book/src/pong/sprites.aseprite
Normal file
BIN
book/src/pong/sprites.aseprite
Normal file
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 438 B After Width: | Height: | Size: 420 B |
Loading…
Reference in a new issue