Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Displaying the level

To display the level we have made, we will make use of agb’s InfiniteScrolledMap. The InfiniteScrolledMap wraps a regular background and handles scrolling for you. It calls a callback for each tile position that becomes visible on screen, asking “what tile should I display here?” This means it only updates tiles that have actually changed, which is very efficient. For the task of displaying a level or a world, the InfiniteScrolledMap is the best choice.

Updating our imports

Update the imports in src/main.rs to include everything we need for this chapter:

use agb::{
    display::{
        GraphicsFrame, Priority,
        tiled::{
            InfiniteScrolledMap, RegularBackground, RegularBackgroundSize,
            TileFormat, TileSetting, VRAM_MANAGER,
        },
    },
    fixnum::{Rect, Vector2D, vec2},
    include_background_gfx,
};

extern crate alloc;

Adding bounds() to Level

Let’s add a convenience method to the Level struct to get its bounds as a Rect. Add this after the Level struct definition:

impl Level {
    fn bounds(&self) -> Rect<i32> {
        Rect::new(
            vec2(0, 0),
            // Rect's size is inclusive of the edge, so a size of (width - 1)
            // covers tile coordinates 0 through width - 1.
            vec2(self.width as i32 - 1, self.height as i32 - 1),
        )
    }
}

The World struct

We’ll encapsulate the background and the level into a World struct:

struct World {
    level: &'static Level,
    bg: InfiniteScrolledMap,
}

impl World {
    fn new(level: &'static Level) -> Self {
        let bg = RegularBackground::new(
            Priority::P0,
            RegularBackgroundSize::Background32x32,
            TileFormat::FourBpp,
        );
        let bg = InfiniteScrolledMap::new(bg);

        World { level, bg }
    }

    fn set_pos(&mut self, pos: Vector2D<i32>) {
        self.bg.set_scroll_pos(pos, |pos| {
            let tile = if self.level.bounds().contains_point(pos) {
                // Convert 2D coordinates to a 1D index into our flat tile array.
                // This is called row-major indexing.
                let idx = pos.x + pos.y * self.level.width as i32;
                self.level.background[idx as usize]
            } else {
                // Use the transparent tile outside the level bounds.
                // agb may contain specific optimizations around the blank tile.
                TileSetting::BLANK
            };

            (&tiles::TILES.tiles, tile)
        });
    }

    fn show(&self, frame: &mut GraphicsFrame) {
        self.bg.show(frame);
    }
}

It’s always a good idea to wrap the set_scroll_pos call in a method like this. By providing the callback every time, it makes lifetimes easier to manage.

The callback receives a Vector2D<i32> representing a tile coordinate (not a pixel coordinate). We return a tuple of the tileset and the TileSetting for that position.

The main function

Now we can write the main function to use our World:

#[agb::entry]
fn main(mut gba: agb::Gba) -> ! {
    let mut gfx = gba.graphics.get();

    VRAM_MANAGER.set_background_palettes(tiles::PALETTES);
    let mut bg = World::new(levels::LEVELS[0]);

    loop {
        // We pass (0, 0) for now since the camera doesn't move yet.
        // We'll revisit this when we add a player.
        bg.set_pos(vec2(0, 0));

        let mut frame = gfx.frame();

        bg.show(&mut frame);

        frame.commit();
    }
}

Running this should display your level on the screen!

What we did

We’ve seen how to use the InfiniteScrolledMap to display a Tiled level on the screen.

Exercise

Make a level wider than the screen (e.g. 60x20 tiles) and use the D-Pad to scroll around. You’ll need a ButtonController — see the Paddle movement chapter for a refresher on handling input.