gba/book/src-bak/memory_game.md

317 lines
9.2 KiB
Markdown
Raw Normal View History

2018-12-06 15:56:39 +11:00
# Making A Memory Game
For this example to show off our new skills we'll make a "memory" game. The idea
is that there's some face down cards and you pick one, it flips, you pick a
second, if they match they both go away, if they don't match they both turn back
face down. The player keeps going until all the cards are gone, then we'll deal
the cards again.
2018-12-06 15:56:39 +11:00
There are many steps to do to get such a simple seeming game going. In fact I
stumbled a bit myself when trying to get things set up and going despite having
written and explained all the parts so far. Accordingly, we'll take each part
very slowly, and review things as we build up our game.
2018-12-06 15:56:39 +11:00
We'll start back with a nearly blank file, calling it `memory_game.rs`:
```rust
2018-12-06 15:56:39 +11:00
#![feature(start)]
#![no_std]
2018-12-06 15:56:39 +11:00
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
2018-12-06 15:56:39 +11:00
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
loop {
// TODO the whole thing
}
}
```
2018-12-06 15:56:39 +11:00
## Displaying A Background
First let's try to get a background going. We'll display a simple checker
pattern just so that we know that we did something.
Remember, backgrounds have the following essential components:
2018-12-06 15:56:39 +11:00
* Background Palette
* Background Tiles
* Screenblock
* IO Registers
2018-12-06 15:56:39 +11:00
### Background Palette
2018-12-06 15:56:39 +11:00
To write to the background palette memory we'll want to name a `VolatilePtr` for
it. We'll probably also want to be able to cast between different types either
right away or later in this program, so we'll add a method for that.
```rust
2018-12-06 15:56:39 +11:00
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct VolatilePtr<T>(pub *mut T);
impl<T> VolatilePtr<T> {
pub unsafe fn read(&self) -> T {
core::ptr::read_volatile(self.0)
}
pub unsafe fn write(&self, data: T) {
core::ptr::write_volatile(self.0, data);
}
pub fn offset(self, count: isize) -> Self {
VolatilePtr(self.0.wrapping_offset(count))
}
pub fn cast<Z>(self) -> VolatilePtr<Z> {
VolatilePtr(self.0 as *mut Z)
}
}
```
2018-12-06 15:56:39 +11:00
Now we give ourselves an easy way to write a color into a palbank slot.
```rust
pub const BACKGROUND_PALETTE: VolatilePtr<u16> = VolatilePtr(0x500_0000 as *mut u16);
pub fn set_bg_palette_4bpp(palbank: usize, slot: usize, color: u16) {
assert!(palbank < 16);
assert!(slot > 0 && slot < 16);
unsafe {
BACKGROUND_PALETTE
.cast::<[u16; 16]>()
.offset(palbank as isize)
.cast::<u16>()
.offset(slot as isize)
.write(color);
}
}
```
2018-12-06 15:56:39 +11:00
And of course we need to bring back in our ability to build color values, as
well as a few named colors to start us off:
```rust
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
blue << 10 | green << 5 | red
}
pub const WHITE: u16 = rgb16(31, 31, 31);
pub const LIGHT_GRAY: u16 = rgb16(25, 25, 25);
pub const DARK_GRAY: u16 = rgb16(15, 15, 15);
```
Which _finally_ allows us to set our palette colors in `main`:
```rust
fn main(_argc: isize, _argv: *const *const u8) -> isize {
set_bg_palette_4bpp(0, 1, WHITE);
set_bg_palette_4bpp(0, 2, LIGHT_GRAY);
set_bg_palette_4bpp(0, 3, DARK_GRAY);
```
2018-12-06 15:56:39 +11:00
### Background Tiles
2018-12-06 15:56:39 +11:00
So we'll want some light gray tiles and some dark gray tiles. We could use a
single tile and then swap it between palbanks to do the color selection, but for
now we'll just use two different tiles, since we've got tons of tile space to
spare.
```rust
2018-12-06 15:56:39 +11:00
#[derive(Debug, Clone, Copy, Default)]
#[repr(transparent)]
pub struct Tile4bpp {
pub data: [u32; 8],
}
pub const ALL_TWOS: Tile4bpp = Tile4bpp {
data: [
0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222, 0x22222222,
],
};
pub const ALL_THREES: Tile4bpp = Tile4bpp {
data: [
0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333, 0x33333333,
],
};
```
And then we have to have a way to put the tiles into video memory:
```rust
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct Charblock4bpp {
pub data: [Tile4bpp; 512],
}
pub const VRAM: VolatilePtr<Charblock4bpp> = VolatilePtr(0x0600_0000 as *mut Charblock4bpp);
pub fn set_bg_tile_4bpp(charblock: usize, index: usize, tile: Tile4bpp) {
assert!(charblock < 4);
assert!(index < 512);
unsafe { VRAM.offset(charblock as isize).cast::<Tile4bpp>().offset(index as isize).write(tile) }
}
```
And finally, we can call that within `main`:
```rust
fn main(_argc: isize, _argv: *const *const u8) -> isize {
// bg palette
set_bg_palette_4bpp(0, 1, WHITE);
set_bg_palette_4bpp(0, 2, LIGHT_GRAY);
set_bg_palette_4bpp(0, 3, DARK_GRAY);
// bg tiles
set_bg_tile_4bpp(0, 0, ALL_TWOS);
set_bg_tile_4bpp(0, 1, ALL_THREES);
```
### Setup A Screenblock
Screenblocks are a little weird because they take the same space as the
charblocks (8 screenblocks per charblock). The GBA will let you mix and match
and it's up to you to keep it all straight. We're using tiles at the base of
charblock 0, so we'll place our screenblock at the base of charblock 1.
First, we have to be able to make one single screenblock entry at a time:
```rust
#[derive(Debug, Clone, Copy, Default)]
#[repr(transparent)]
pub struct RegularScreenblockEntry(u16);
impl RegularScreenblockEntry {
pub const SCREENBLOCK_ENTRY_TILE_ID_MASK: u16 = 0b11_1111_1111;
2018-12-08 11:06:11 +11:00
pub const fn from_tile_id(id: u16) -> Self {
2018-12-06 15:56:39 +11:00
RegularScreenblockEntry(id & Self::SCREENBLOCK_ENTRY_TILE_ID_MASK)
}
}
```
2018-12-06 15:56:39 +11:00
And then with 32x32 of these things we'll have a whole screenblock. Now, we
probably won't actually make values of the screenblock type itself, but we at
least need it to have the type declared with the correct size so that we can
move our pointers around by the right amount.
```rust
2018-12-06 15:56:39 +11:00
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct RegularScreenblock {
pub data: [RegularScreenblockEntry; 32 * 32],
}
2018-12-06 15:56:39 +11:00
```
Alright, so, as I said those things are kinda big, we don't really want to be
building them up on the stack if we can avoid it, so we'll write one straight
into memory at the correct location.
2018-12-06 15:56:39 +11:00
```rust
pub fn checker_screenblock(slot: usize, a_entry: RegularScreenblockEntry, b_entry: RegularScreenblockEntry) {
let mut p = VRAM.cast::<RegularScreenblock>().offset(slot as isize).cast::<RegularScreenblockEntry>();
let mut checker = true;
for _row in 0..32 {
for _col in 0..32 {
unsafe { p.write(if checker { a_entry } else { b_entry }) };
p = p.offset(1);
checker = !checker;
}
2018-12-06 15:56:39 +11:00
checker = !checker;
}
}
2018-12-06 15:56:39 +11:00
```
And then we add this into `main`
```rust
// screenblock
let light_entry = RegularScreenblockEntry::from_tile_id(0);
let dark_entry = RegularScreenblockEntry::from_tile_id(1);
checker_screenblock(8, light_entry, dark_entry);
```
### Background IO Registers
2018-12-06 15:56:39 +11:00
Our most important step is of course the IO register step. There's four
different background layers, but each of them has the same format for their
control register. For the moment, all that we care about is being able to set
the "screen base block" value.
```rust
#[derive(Clone, Copy, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct BackgroundControlSetting(u16);
impl BackgroundControlSetting {
2018-12-08 11:06:11 +11:00
pub const SCREEN_BASE_BLOCK_MASK: u16 = 0b1_1111;
pub const fn from_base_block(sbb: u16) -> Self {
BackgroundControlSetting((sbb & Self::SCREEN_BASE_BLOCK_MASK) << 8)
2018-12-06 15:56:39 +11:00
}
}
2018-12-06 15:56:39 +11:00
pub const BG0CNT: VolatilePtr<BackgroundControlSetting> = VolatilePtr(0x400_0008 as *mut BackgroundControlSetting);
```
And... that's all it takes for us to be able to add a line into `main`
```rust
// bg0 control
unsafe { BG0CNT.write(BackgroundControlSetting::from_base_block(8)) };
```
2018-12-06 15:56:39 +11:00
### Set The Display Control Register
2018-12-06 15:56:39 +11:00
We're finally ready to set the display control register and get things going.
2018-12-06 15:56:39 +11:00
We've slightly glossed over it so far, but when the GBA is first booted most
everything within the address space will be all zeroed. However, the display
control register has the "Force VBlank" bit enabled by the BIOS, giving you a
moment to put the memory in place that you'll need for the first frame.
2018-12-06 15:56:39 +11:00
So, now that have got all of our memory set, we'll overwrite the initial
display control register value with what we'll call "just enable bg0".
2018-12-06 15:56:39 +11:00
```rust
#[derive(Clone, Copy, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct DisplayControlSetting(u16);
impl DisplayControlSetting {
pub const JUST_ENABLE_BG0: DisplayControlSetting = DisplayControlSetting(1 << 8);
}
pub const DISPCNT: VolatilePtr<DisplayControlSetting> = VolatilePtr(0x0400_0000 as *mut DisplayControlSetting);
```
2018-12-06 15:56:39 +11:00
And so finally we have a complete `main`
2018-12-06 15:56:39 +11:00
```rust
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
// bg palette
set_bg_palette_4bpp(0, 1, WHITE);
set_bg_palette_4bpp(0, 2, LIGHT_GRAY);
set_bg_palette_4bpp(0, 3, DARK_GRAY);
// bg tiles
set_bg_tile_4bpp(0, 0, ALL_TWOS);
set_bg_tile_4bpp(0, 1, ALL_THREES);
// screenblock
let light_entry = RegularScreenblockEntry::from_tile_id(0);
let dark_entry = RegularScreenblockEntry::from_tile_id(1);
checker_screenblock(8, light_entry, dark_entry);
// bg0 control
unsafe { BG0CNT.write(BackgroundControlSetting::from_base_block(8)) };
// Display Control
unsafe { DISPCNT.write(DisplayControlSetting::JUST_ENABLE_BG0) };
loop {
// TODO the whole thing
}
}
```
2018-12-06 15:56:39 +11:00
And _It works, Marty! It works!_
2018-12-06 15:56:39 +11:00
![screenshot_checkers](screenshot_checkers.png)
2018-12-06 15:56:39 +11:00
We've got more to go, but we're well on our way.