NESFab Tutorial Part 3

Infinite Horizontal Scroll

This third code I want to show you is an example of how to make program an infinite horizontal scroll using NESFab. Here you can download the example.

Like the past two examples, its working is very simple. It loads a mapfab level composed of 4 horizontal 16x16 screens and scrolls arround them with the pad. The character data is made with YY-CHR english Next you have the whole code:

 
    mapfab(raw, "t4.mapfab", "chr", "palette", "metatiles", "level")

    ct UU SCROLL_AMOUNT=7 // the amount of pixels that will move when a key is pressed (1-8)
    
    vars /game_vars
        CCC/levels lvl1_mt_map // level 1 metatile map and its tiles correspondence
        CC/metatiles mt_nw
        CC/metatiles mt_ne
        CC/metatiles mt_sw
        CC/metatiles mt_se
        CC/metatiles mt_attr
    
        UU my_x_scroll=0 // this points to the current h. scroll value
        UU maximum_x // this will point to the map maximum x scroll value
        UU scr_bg_update_base_addr // addrs where to copy data during NMI
        UU scr_attr_bg_update_base_addr
        Bool scr_bg_update_needed=false
        U[30] scroll_tiles // The data we'll upload to the PPU during NMI.
        //U[30] scroll_tiles2
        U[8] scroll_attrs
    
    /*
    Writes the initial 32x30 screen. Take note the mapfab is (16*4)H x 16V. 
    It has a 16th row of tiles that will never be seen to make multiplications
    by its height a power of two (faster)
    */
    fn load_screen(CCC/levels my_lvl_pointer)
        U chr_bank = read U(my_lvl_pointer)
        CCC/palettes pal=read CCC/palettes(my_lvl_pointer)
        CC/metatiles mymetatiles=read CC/metatiles(my_lvl_pointer)
        UU size = UU(read U(my_lvl_pointer)) // width in metatiles of screen
        //after all these reads, now my_lvl_pointer points to the metatiles matrix
    
        maximum_x=size<<4 // sets the maximum x value to scroll to
        maximum_x-=256 //
    
        // set palette
        load_palette(pal)
        ppu_upload_palette()
           
        UU mt_size = UU(read U(mymetatiles)) // number of metatiles
        if mt_size == 0
            mt_size = 256
    
        lvl1_mt_map=my_lvl_pointer // the map is set in columns. it's in level.macrofab
        
        mt_nw=mymetatiles
        mt_ne=mt_nw
        mt_ne+=mt_size
        mt_sw=mt_ne
        mt_sw+=mt_size
        mt_se=mt_sw
        mt_se+=mt_size
        mt_attr=mt_se
        mt_attr+=mt_size
        // now every pointer points where it should
    
        ppu_reset_addr($2000) // first screen // https://www.nesdev.org/wiki/Mirroring
    
        {PPUSTATUS}() // Reset the address latch before 'ppu_set_addr', just to be safe.
        {PPUCTRL}(PPUCTRL_VRAM_32_DOWN) // Used to write columns of data.
    
        // upload first namespace data
        UU i=0
        UU ii=0
    
        for UU x=0; x<16; x+=1
            ppu_reset_addr($2000+ii)
            for UU y=0; y<16; y+=1
                U my_metatile=my_lvl_pointer{i}
                U nw_tile=mt_nw{my_metatile}
                U sw_tile=mt_sw{my_metatile}
                {PPUDATA}(nw_tile)
                {PPUDATA}(sw_tile)
                i+=1
            i-=16
            ii+=1
            ppu_reset_addr($2000+ii)
            for UU y=0; y<16; y+=1
                U my_metatile=my_lvl_pointer{i}
                U ne_tile=mt_ne{my_metatile}
                U se_tile=mt_se{my_metatile}
                {PPUDATA}(ne_tile)
                {PPUDATA}(se_tile)
                i+=1
            ii+=1
    
        //upload first attributes data, we need to OR it together
        //for every 2x2 metatiles block (https://www.nesdev.org/wiki/PPU_attribute_tables)
        i=0
        ii=0
        for UU a=0; a<8; a+=1
            for UU attri=0; attri<32; attri+=8
                ppu_reset_addr($2000+960+a+attri)
                U my_metatile=my_lvl_pointer{i} //tl
                U my_attr=mt_attr{my_metatile}
                //
                my_metatile=my_lvl_pointer{i+16} // tr
                my_attr|=(mt_attr{my_metatile}<<2)
                //
                my_metatile=my_lvl_pointer{i+1} //bl
                my_attr|=(mt_attr{my_metatile}<<4)
                //
                my_metatile=my_lvl_pointer{i+17} //br
                my_attr|=(mt_attr{my_metatile}<<6)
                //
                {PPUDATA}(my_attr)
                //
                my_metatile=my_lvl_pointer{i+8} //tl
                my_attr=mt_attr{my_metatile}
                //
                my_metatile=my_lvl_pointer{i+16+8} // tr
                my_attr|=(mt_attr{my_metatile}<<2)
                //
                my_metatile=my_lvl_pointer{i+1+8} //bl
                my_attr|=(mt_attr{my_metatile}<<4)
                //
                my_metatile=my_lvl_pointer{i+17+8} //br
                my_attr|=(mt_attr{my_metatile}<<6)
                //
                {PPUDATA}(my_attr)
                i+=2
            ii+=32
            i=ii
    
    
    nmi main_nmi()
        // Update OAM and poll the pads:
        ppu_upload_oam_poll_pads(0)
    
        // Turn on rendering:
        {PPUMASK}(PPUMASK_ON | PPUMASK_NO_CLIP)
    
        // Reset the scroll https://www.nesdev.org/wiki/PPU_scrolling
        // and https://www.nesdev.org/wiki/Mirroring
        if scr_bg_update_needed
            nmi_update_screen_bg()
        //
        ppu_reset_scroll_16(my_x_scroll, 0)
        
    mode main()
    : nmi main_nmi
    
        load_screen(@lev_level1) // the name lev_level1 is defined in the levels.macrofab
        // Tell the NES to trigger NMI once per frame:
        {PPUCTRL}(PPUCTRL_NMI_ON)
    
        // Wait forever, one frame at a time:
        while true
            update_pads()
            move_player() 
            update_sprites()
            nmi // Wait for the next NMI
    
    fn update_sprites()
        // Our stack index into OAM:
        U i = 0
        // Push sprites
        set_oam_x(i, 0)     // x-position
        set_oam_y(i, 0) // y-position
        set_oam_p(i, 8)    // empty tile
        set_oam_a(i, 0)      // options
        i += 4
    
        // Clear the remainder of OAM
        hide_oam(i)
    
    fn move_player()
        /*
        the X scroll runs smoothly but the Y scroll doesn't quite
        work the way you'd expect. The 9th bit selects the namespace (screen)
        and the 8 bit left (up to 239) the scroll inside that screen.
        More info in https://www.nesdev.org/wiki/PPU_scrolling
        */
        if pads[0].held & BUTTON_LEFT
            if (my_x_scroll >= SCROLL_AMOUNT)
                my_x_scroll-=SCROLL_AMOUNT
            else
                my_x_scroll=0
            calc_left_scr_bg(my_x_scroll)
        if pads[0].held & BUTTON_RIGHT
            if ((my_x_scroll+SCROLL_AMOUNT) <= maximum_x)
                my_x_scroll+=SCROLL_AMOUNT
            else
                my_x_scroll=maximum_x
            calc_right_scr_bg(my_x_scroll)
    
    
    /* This function sets a 9 bit scroll
        instead of the 8 bit ppu_reset_scroll
        from nes.fab library: https://www.nesdev.org/wiki/PPU_scrolling
        */
    fn ppu_reset_scroll_16(UU x, UU y)
        ppu_reset_scroll(x.a, y.a) // the 9th bits should go to ppuctrl right after
        {PPUCTRL}(PPUCTRL_NMI_ON | (x.b & 1) | ((y.b & 1)<<1))  // Set PPU control register, we want NMIs
    
    /*
    This function calculates the BG piece to put into the namespace 
    after right scrolling, and its attributes (palettes). 
    */
    fn calc_right_scr_bg(UU x)
        UU x_right=x+255
        UU column=(x_right.a >> 3)
        UU col_start_addr
        UU attr_base_addr
        // calculate the correct namespace 
        if (x_right.b & 1)
            col_start_addr=$2400+column
            attr_base_addr=$2400+960
            
        else
            col_start_addr=$2000+column
            attr_base_addr=$2000+960
        // calculate the 8x8 tiles that will patch the screen
        UU mt_map_col_start=(x_right >> 4)
        U y=0
        UU map_mt_pointer=(mt_map_col_start << 4)
        for U i=0; i<15; i+=1
            U metatile=lvl1_mt_map{map_mt_pointer}
            //select whether it's the left or right column of the 2x2 metatile
            if (column & 1)
                scroll_tiles[y]=mt_ne[metatile]
                scroll_tiles[y+1]=mt_se[metatile]
            else
                scroll_tiles[y]=mt_nw[metatile]
                scroll_tiles[y+1]=mt_sw[metatile]
            y+=2
            map_mt_pointer+=1
        // calculate the addresses where data will be written
        scr_bg_update_base_addr=col_start_addr
        scr_attr_bg_update_base_addr=attr_base_addr
        scr_attr_bg_update_base_addr+=(x_right.a >> 5)
        // calculate the attributes data
        UU base_mt_attr=(x_right >> 4)
        base_mt_attr&=$fffe
        base_mt_attr=(base_mt_attr<<4)
        U iii=0
        for UU i=0; i<15; i+=2
            U my_metatile=lvl1_mt_map{base_mt_attr+i} //tl
            U my_attr=mt_attr{my_metatile}
            my_metatile=lvl1_mt_map{base_mt_attr+i+16} // tr
            my_attr|=(mt_attr{my_metatile}<<2)
            my_metatile=lvl1_mt_map{base_mt_attr+i+1} //bl
            my_attr|=(mt_attr{my_metatile}<<4)
            my_metatile=lvl1_mt_map{base_mt_attr+i+17} //br
            my_attr|=(mt_attr{my_metatile}<<6)
            scroll_attrs[iii]=my_attr
            iii+=1
        //
        scr_bg_update_needed=true
    
    /*
    Copies to the adequate namespace addr the bg piece needed
    This function is to be called when NMI takes place.
    */
    fn nmi_update_screen_bg()
        {PPUSTATUS}() // Reset the address latch just to be safe.
        {PPUCTRL}(PPUCTRL_VRAM_32_DOWN) // Used to write columns of data. Addr incs in 32 every write.
        // write the column to the namespace
        ppu_reset_addr(scr_bg_update_base_addr)
        for U y=0; y<30; y+=1
            {PPUDATA}(scroll_tiles[y])
        // now write the attrs. remember we are in addr+32 mode every write
        U i=0
        for UU attri=0; attri<32; attri+=8
            ppu_reset_addr(scr_attr_bg_update_base_addr+attri)
            {PPUDATA}(scroll_attrs[i])
            {PPUDATA}(scroll_attrs[i+4])
            i+=1
        //
        scr_bg_update_needed=false
    
    
    fn calc_left_scr_bg(UU x)
        UU column=(x.a >> 3)
        UU col_start_addr
        UU attr_base_addr
        // calculate the correct namespace 
        if (x.b & 1)
            col_start_addr=$2400+column
            attr_base_addr=$2400+960
            
        else
            col_start_addr=$2000+column
            attr_base_addr=$2000+960
        // calculate the 8x8 tiles that will patch the screen
        UU mt_map_col_start=(x >> 4)
        U y=0
        UU map_mt_pointer=(mt_map_col_start << 4)
        for U i=0; i<15; i+=1
            U metatile=lvl1_mt_map{map_mt_pointer}
            //select whether it's the left or right column of the 2x2 metatile
            if (column & 1)
                scroll_tiles[y]=mt_ne[metatile]
                scroll_tiles[y+1]=mt_se[metatile]
            else
                scroll_tiles[y]=mt_nw[metatile]
                scroll_tiles[y+1]=mt_sw[metatile]
            y+=2
            map_mt_pointer+=1
        // calculate the addresses where data will be written
        scr_bg_update_base_addr=col_start_addr
        scr_attr_bg_update_base_addr=attr_base_addr
        scr_attr_bg_update_base_addr+=(x.a >> 5)
        // calculate the attributes data
        UU base_mt_attr=(x >> 4)
        base_mt_attr&=$fffe
        base_mt_attr=(base_mt_attr<<4)
        U iii=0
        for UU i=0; i<15; i+=2
            U my_metatile=lvl1_mt_map{base_mt_attr+i} //tl
            U my_attr=mt_attr{my_metatile}
            my_metatile=lvl1_mt_map{base_mt_attr+i+16} // tr
            my_attr|=(mt_attr{my_metatile}<<2)
            my_metatile=lvl1_mt_map{base_mt_attr+i+1} //bl
            my_attr|=(mt_attr{my_metatile}<<4)
            my_metatile=lvl1_mt_map{base_mt_attr+i+17} //br
            my_attr|=(mt_attr{my_metatile}<<6)
            scroll_attrs[iii]=my_attr
            iii+=1
        //
        scr_bg_update_needed=true    
mapfab screens

How it works

The code shares the basic operating structure than basic examples and has 4 major functions:

load_screen loads the level and sets some global variables that will be used by other functions.

calc_left_scr_bg and calc_right_scr_bg calculate the patch that will need to be written to the PPU memory during VBlank NMI when scrolling left or right.

nmi_update_screen_bg uploads the data to the PPU memory during VBlank. This function only uploads the data. It does not calculate it, which is done in the previous functions. This is so because VBlank is only 2273 cycles long and it's better to calculate the patch of data we need to write during the main game loop and leave the NMI to exclusively upload that data.

The data is uploaded using the PPUCTRL_VRAM_32_DOWN option, which writes data in columns, that is, every write increments the PPU memory address in 32. Perfect for writing columns of tiles.

The mapfab data is also read in colums, thanks to the level.macrofab:

 
#:name:# 
#:chr_name:# 
#:palette_name:# 
#:metatiles_name:# 

data /levels
    [] lev_#name#
        U(chr_#chr_name#_index) // The CHR banke
        (@pal_#palette_name#)   // Pointer to the palette
        (@mt_#metatiles_name#)  // Pointer to the metatiles
        U(_width)               // The width, in tiles.
        (_column_major)         // The actual level tiles

Notice the _column_major instead of the previous _row_major of the other examples. Also, the mapfab has 16 tiles per column, instead of the 15 that can be shown in a NES, to make multiplications faster using powers of two.