NESFab Tutorial Part 1

Introduction

This page is intended to be a guide for programmers that want to develop with NESFab. It's an extension of the maze tutorial . You can download the code used in this tutorial here.

Playing with single screen MapFab projects

MapFab is a very useful tool paired with YY-CHR, since YY-CHR files and MapFab projects can be imported by NESFab directly.

This example is divided in 2 versions, t2.fab loads a single screen made with YY-CHR and MapFab. Displays it, and moves sprites across the screen. The second version (t2final.fab) adds screen modification to it.

MapFab is a tool that takes YY-CHR NES 8x8 pixel tiles and groups them in a 2x2 metatile that we use to compose the level. Here is the code for the first version t2.fab, which we will disect later:

First version. t2.fab

 
mapfab(raw, "t2.mapfab", "chr", "palette", "metatiles", "level")
//mapfab(raw, "modproj1.mapfab", "chr", "palette", "metatiles", "level")

ct U BALL_SPRITE=7
ct U NUM_BALLS=4

vars /game_vars
    U[NUM_BALLS] balls_pos_x = U[](100, 110, 120, 140)
    U[NUM_BALLS] balls_pos_y = U[](150, 110, 115, 140)
    S[NUM_BALLS] balls_xinc = S[](1, -1, 1, -1)
    S[NUM_BALLS] balls_yinc = S[](1, -1, -1, 1)

    CCC/levels lvl1_mtmap_ptr // pointer to level 1 metatiles map
    

/*
Loads a 16x15 metatiles screen made with MapFab. 
Its only argument is a pointer to the levels struct 
made with the macros included in the earlier mapfab()
*/
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

    // 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
    //after this read now mymetatiles points to the 
    // metatiles struct made with the metatiles.macrofab in the mapfab ()

    //pointer to the arrays of the 2x2 tiles (and attr) that make every metatile
    CC/metatiles mt_nw
    CC/metatiles mt_ne
    CC/metatiles mt_sw
    CC/metatiles mt_se
    CC/metatiles mt_attr
    
    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

    lvl1_mtmap_ptr=my_lvl_pointer // gobal var to the mt map

    U i=0
    ppu_reset_addr($2000)
    
    // upload namespace data
    for U y=0; y<15; y+=1
        for U x=0; x<16; x+=1
            U my_metatile=my_lvl_pointer{i}
            U nw_tile=mt_nw{my_metatile}
            U ne_tile=mt_ne{my_metatile}
            {PPUDATA}(nw_tile)
            {PPUDATA}(ne_tile)
            i+=1
        i=i-16
        for U x=0; x<16; x+=1
            U my_metatile=my_lvl_pointer{i}
            U sw_tile=mt_sw{my_metatile}
            U se_tile=mt_se{my_metatile}
            {PPUDATA}(sw_tile)
            {PPUDATA}(se_tile)
            i+=1

    //upload attributes data, we need to OR it together
    //for every 2x2 metatiles block (https://www.nesdev.org/wiki/PPU_attribute_tables)
    i=0
    for U y=0; y<15; y+=2
        for U x=0; x<16; x+=2
            U my_metatile=my_lvl_pointer{i}
            U my_attr=mt_attr{my_metatile}
            //
            my_metatile=my_lvl_pointer{i+1}
            my_attr|=(mt_attr{my_metatile}<<2)
            //
            my_metatile=my_lvl_pointer{i+16}
            my_attr|=(mt_attr{my_metatile}<<4)
            //
            my_metatile=my_lvl_pointer{i+17}
            my_attr|=(mt_attr{my_metatile}<<6)
            i+=2
            {PPUDATA}(my_attr)
        i+=16
            

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
    ppu_reset_scroll(0, 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

    // update balls position
    for U n=0; n<NUM_BALLS; n+=1
        balls_pos_x[n]+=balls_xinc[n]
        balls_pos_y[n]+=balls_yinc[n]

    // check collisions
    for U n=0; n<NUM_BALLS; n+=1
        if get_metatile_at_pix((balls_pos_x[n]+4), balls_pos_y[n]) > 0
            balls_yinc[n]=1
        if get_metatile_at_pix((balls_pos_x[n]+4), (balls_pos_y[n]+8)) > 0
            balls_yinc[n]=-1
        if get_metatile_at_pix(balls_pos_x[n], (balls_pos_y[n]+4)) > 0
            balls_xinc[n]=1
        if get_metatile_at_pix((balls_pos_x[n]+8), (balls_pos_y[n]+4)) > 0
            balls_xinc[n]=-1

    // Push sprites
    for U n=0; n<NUM_BALLS; n+=1
        set_oam_x(i, balls_pos_x[n])     // x-position
        set_oam_y(i, balls_pos_y[n]) // y-position
        set_oam_p(i, BALL_SPRITE)    // tile
        set_oam_a(i, 0)      // options
        i += 4

    // Clear the remainder of OAM
    hide_oam(i)

/*
returns the map metatile at screen pixel position (px, py)
*/
fn get_metatile_at_pix (U px, U py) U
    UU lvlmtm=(((py>>4)<<4)+(px>>4))
    U t=lvl1_mtmap_ptr{lvlmtm}
    return t
 

The first line mapfab(raw, "t2.mapfab", "chr", "palette", "metatiles", "level") is very important. It loads the MapFab project and runs some macros on the file that define a series of variables. Here are two of the macros that define variables we will use later. Take a look:

The first one is defined in metatiles.macrofab, and defines the metatiles made of 2x2 YY-CHR tiles as an array.

 
  #:name:# 
  #:chr_name:#
  #:palette_name:#
  
  omni data /metatiles
      [] mt_#name#
          U(_num)     // The amount of metatiles.
          (_nw)       // North-west tiles of each metatile
          (_ne)       // North-east tiles of each metatile
          (_sw)       // South-west tiles of each metatile
          (_se)       // South-east tiles of each metatile
          (_combined) // The collisions and attributes, OR'd together.
 

The second is defined in levels.macrofab, and defines an array that contains the map of metatiles that define the level.

 
#: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.
        (_row_major)         // The actual level tiles
 

The second place we should look at is the load_screen function:

 
  /*
Loads a 16x15 metatiles screen made with MapFab. 
Its only argument is a pointer to the levels struct 
made with the macros included in the earlier mapfab()
*/
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

    // 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
    //after this read now mymetatiles points to the 
    // metatiles struct made with the metatiles.macrofab in the mapfab ()

    //pointer to the arrays of the 2x2 tiles (and attr) that make every metatile
    CC/metatiles mt_nw
    CC/metatiles mt_ne
    CC/metatiles mt_sw
    CC/metatiles mt_se
    CC/metatiles mt_attr
    
    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

    lvl1_mtmap_ptr=my_lvl_pointer // gobal var to the mt map

    U i=0
    ppu_reset_addr($2000)
    
    // upload namespace data
    for U y=0; y<15; y+=1
        for U x=0; x<16; x+=1
            U my_metatile=my_lvl_pointer{i}
            U nw_tile=mt_nw{my_metatile}
            U ne_tile=mt_ne{my_metatile}
            {PPUDATA}(nw_tile)
            {PPUDATA}(ne_tile)
            i+=1
        i=i-16
        for U x=0; x<16; x+=1
            U my_metatile=my_lvl_pointer{i}
            U sw_tile=mt_sw{my_metatile}
            U se_tile=mt_se{my_metatile}
            {PPUDATA}(sw_tile)
            {PPUDATA}(se_tile)
            i+=1

    //upload attributes data, we need to OR it together
    //for every 2x2 metatiles block (https://www.nesdev.org/wiki/PPU_attribute_tables)
    i=0
    for U y=0; y<15; y+=2
        for U x=0; x<16; x+=2
            U my_metatile=my_lvl_pointer{i}
            U my_attr=mt_attr{my_metatile}
            //
            my_metatile=my_lvl_pointer{i+1}
            my_attr|=(mt_attr{my_metatile}<<2)
            //
            my_metatile=my_lvl_pointer{i+16}
            my_attr|=(mt_attr{my_metatile}<<4)
            //
            my_metatile=my_lvl_pointer{i+17}
            my_attr|=(mt_attr{my_metatile}<<6)
            i+=2
            {PPUDATA}(my_attr)
        i+=16

It perfoms a series of reads, which read the value where the pointer is pointing and advances the pointer. This is necessary to read the values of the variables created in the .macrofab files and get pointers to all the variables we need, like the palette, the 2x2 tiles metatiles definition, and the array of metatiles that make the screen.

Once it gets pointers to all the level definition variables, writes the data to the NES GFX memory, palette, namespace (map) and attributes data.

Here is the rest of the code:

 
  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
  ppu_reset_scroll(0, 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

  // update balls position
  for U n=0; n<NUM_BALLS; n+=1
      balls_pos_x[n]+=balls_xinc[n]
      balls_pos_y[n]+=balls_yinc[n]

  // check collisions
  for U n=0; n<NUM_BALLS; n+=1
      if get_metatile_at_pix((balls_pos_x[n]+4), balls_pos_y[n]) > 0
          balls_yinc[n]=1
      if get_metatile_at_pix((balls_pos_x[n]+4), (balls_pos_y[n]+8)) > 0
          balls_yinc[n]=-1
      if get_metatile_at_pix(balls_pos_x[n], (balls_pos_y[n]+4)) > 0
          balls_xinc[n]=1
      if get_metatile_at_pix((balls_pos_x[n]+8), (balls_pos_y[n]+4)) > 0
          balls_xinc[n]=-1

  // Push sprites
  for U n=0; n<NUM_BALLS; n+=1
      set_oam_x(i, balls_pos_x[n])     // x-position
      set_oam_y(i, balls_pos_y[n]) // y-position
      set_oam_p(i, BALL_SPRITE)    // tile
      set_oam_a(i, 0)      // options
      i += 4

  // Clear the remainder of OAM
  hide_oam(i)

/*
returns the map metatile at screen pixel position (px, py)
*/
fn get_metatile_at_pix (U px, U py) U
  UU lvlmtm=(((py>>4)<<4)+(px>>4))
  U t=lvl1_mtmap_ptr{lvlmtm}
  return t

The rest of the code is pretty easy to read. It defines the NMI function to read the pads and upload the sprites data.

Then the main function (mode) that loads the screen with the load_screen function. The update_sprites function moves the sprite balls around the screen and the final function, get_metatile_at_pix returns the metatile at a specific pixel to check for collisions of the balls with the walls.

Here is what the screen looks like:

example 1 screen

Second version. t2final.fab

Here is the code for the final version. This time we check for collisions with the walls and change the namespace (screen map) with every collision.

 
  mapfab(raw, "t2.mapfab", "chr", "palette", "metatiles", "level")
  //mapfab(raw, "modproj1.mapfab", "chr", "palette", "metatiles", "level")
  
  ct U BALL_SPRITE=7
  ct U NUM_BALLS=4
  ct U TOUCHED_WALL_MT=8 // metatile for touched wall
  
  vars /game_vars
      U[NUM_BALLS] balls_pos_x = U[](100, 110, 120, 140)
      U[NUM_BALLS] balls_pos_y = U[](150, 110, 115, 140)
      S[NUM_BALLS] balls_xinc = S[](1, -1, 1, -1)
      S[NUM_BALLS] balls_yinc = S[](1, -1, -1, 1)
  
      U[16*15] lvl1_mt_map // level 1 map of metatiles in RAM
  
      //pointer to the arrays of the 2x2 tiles (and attr) that make every metatile
      CC/metatiles mt_nw
      CC/metatiles mt_ne
      CC/metatiles mt_sw
      CC/metatiles mt_se
      CC/metatiles mt_attr
  
      // we temporary need to store the collision 
      // to update the map/GFX mem during VBLANK
      Bool is_collision_pending=false
      U collision_col
      U collision_row
  
  /*
  Loads a 16x15 metatiles screen made with MapFab. 
  Its only argument is a pointer to the levels struct 
  made with the macros included in the earlier mapfab()
  */
  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
  
      // 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
      //after this read now mymetatiles points to the 
      // metatiles struct made with the metatiles.macrofab in the mapfab ()
      
      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
  
      U i=0
      ppu_reset_addr($2000)
      
      // upload namespace data
      for U y=0; y<15; y+=1
          for U x=0; x<16; x+=1
              U my_metatile=my_lvl_pointer{i}
              U nw_tile=mt_nw{my_metatile}
              U ne_tile=mt_ne{my_metatile}
              {PPUDATA}(nw_tile)
              {PPUDATA}(ne_tile)
              i+=1
          i=i-16
          for U x=0; x<16; x+=1
              U my_metatile=my_lvl_pointer{i}
              U sw_tile=mt_sw{my_metatile}
              U se_tile=mt_se{my_metatile}
              {PPUDATA}(sw_tile)
              {PPUDATA}(se_tile)
              i+=1
  
      //upload attributes data, we need to OR it together
      //for every 2x2 metatiles block (https://www.nesdev.org/wiki/PPU_attribute_tables)
      i=0
      for U y=0; y<15; y+=2
          for U x=0; x<16; x+=2
              U my_metatile=my_lvl_pointer{i}
              U my_attr=mt_attr{my_metatile}
              //
              my_metatile=my_lvl_pointer{i+1}
              my_attr|=(mt_attr{my_metatile}<<2)
              //
              my_metatile=my_lvl_pointer{i+16}
              my_attr|=(mt_attr{my_metatile}<<4)
              //
              my_metatile=my_lvl_pointer{i+17}
              my_attr|=(mt_attr{my_metatile}<<6)
              i+=2
              {PPUDATA}(my_attr)
          i+=16
              
      // copy the level metatiles map to RAM
      for U iv=0; iv<(16*15); iv+=1
          lvl1_mt_map{iv}=my_lvl_pointer{iv}
  
  
  nmi main_nmi()
      // we need to update GFXs while NMI, so we do this now
      if is_collision_pending
          set_metatile_at_colrow(collision_col, collision_row, TOUCHED_WALL_MT)
          is_collision_pending=false
  
      // Update OAM and poll the pads:
      ppu_upload_oam_poll_pads(0)
  
      // Turn on rendering:
      {PPUMASK}(PPUMASK_ON | PPUMASK_NO_CLIP)
  
      // Reset the scroll
      ppu_reset_scroll(0, 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
  
      // update balls position
      for U n=0; n 0
              balls_yinc[n]=1
              is_collision_pending=true
              collision_col=((balls_pos_x[n]+4)>>4)
              collision_row=(balls_pos_y[n]>>4)
          if get_metatile_at_pix((balls_pos_x[n]+4), (balls_pos_y[n]+8)) > 0
              balls_yinc[n]=-1
              is_collision_pending=true
              collision_col=((balls_pos_x[n]+4)>>4)
              collision_row=((balls_pos_y[n]+8)>>4)
          if get_metatile_at_pix(balls_pos_x[n], (balls_pos_y[n]+4)) > 0
              balls_xinc[n]=1
              is_collision_pending=true
              collision_col=(balls_pos_x[n]>>4)
              collision_row=((balls_pos_y[n]+4)>>4)
          if get_metatile_at_pix((balls_pos_x[n]+8), (balls_pos_y[n]+4)) > 0
              balls_xinc[n]=-1
              is_collision_pending=true
              collision_col=((balls_pos_x[n]+8)>>4)
              collision_row=((balls_pos_y[n]+4)>>4)
  
      // Push sprites
      for U n=0; nn<NUM_BALLS; n+=1
          set_oam_x(i, balls_pos_x[n])     // x-position
          set_oam_y(i, balls_pos_y[n]) // y-position
          set_oam_p(i, BALL_SPRITE)    // tile
          set_oam_a(i, 0)      // options
          i += 4
  
      // Clear the remainder of OAM
      hide_oam(i)
  
  /*
  returns the map metatile at screen pixel position (px, py)
  */
  fn get_metatile_at_pix (U px, U py) U
      UU lvlmtm=(((py>>4)<<4)+(px>>4))
      U t=lvl1_mt_map{lvlmtm}
      return t
  
  /*
  Sets the row,col metatile to a new value.
  This is done in the collisions_map variable because
  it's likely lvl1_mtmap_ptr points to ROM 
  Notice that x,y are UU values, if defined as U, 
  the math here for bit shifting wouldn't work
  */
  fn set_metatile_at_colrow (UU x, UU y, U new_mt)
      // write to the ram metatiles map
      UU map_displacement=((y<<4)+x)
      lvl1_mt_map{map_displacement}=new_mt
  
      // change that metatile position in GFX RAM
      // first the tiles
      UU gfx_addr=((y<<6)+(x<<1))
      gfx_addr+=$2000
      ppu_reset_addr(gfx_addr)    
      {PPUDATA}( mt_nw{new_mt} )
      {PPUDATA}( mt_ne{new_mt} )
      gfx_addr+=32
      ppu_reset_addr(gfx_addr)    
      {PPUDATA}( mt_sw{new_mt} )
      {PPUDATA}( mt_se{new_mt} )
  
      //calculate the new 2x2 metatiles block attributes byte
      UU mt_m_disp=(((y & %11111110) <<4)+(x & %11111110))
      U mt_2x2_pos=U(((y&1)<<1)|(x & 1))
      //
      U my_metatile=lvl1_mt_map{mt_m_disp}
      U new_attr=mt_attr{my_metatile}
      //
      my_metatile=lvl1_mt_map{mt_m_disp+1}
      new_attr|=(mt_attr{my_metatile}<<2)
      //
      my_metatile=lvl1_mt_map{mt_m_disp+16}
      new_attr|=(mt_attr{my_metatile}<<4)
      //
      my_metatile=lvl1_mt_map{mt_m_disp+17}
      new_attr|=(mt_attr{my_metatile}<<6)
      //
  
      //now access the attributes(palettes) address
      UU attr_addr_disp=(((y>>1)<<3)+(x>>1))
      ppu_reset_addr($2000+960+attr_addr_disp)
      {PPUDATA}(new_attr)  

Notice how, this time, in the load_screen function, we copy the map of metatiles to an array in RAM:

 
  // copy the level metatiles map to RAM
  for U iv=0; iv<(16*15); iv+=1
      lvl1_mt_map{iv}=my_lvl_pointer{iv}

This is done because, probably, the array of metatiles we passed to load_screen is probably in ROM, and we want to modify that array with every ball collision.

Now, at every collision of a ball with a wall, we mark the affected metatile with the variables is_collision_pending, collision_col and collision_row.

 
  // check collisions
  for U n=0; nn<NUM_BALLS; n+=1
      if get_metatile_at_pix((balls_pos_x[n]+4), balls_pos_y[n]) > 0
          balls_yinc[n]=1
          is_collision_pending=true
          collision_col=((balls_pos_x[n]+4)>>4)
          collision_row=(balls_pos_y[n]>>4)

It's made that way because, during NMI, we must change the tiles in the NES GFX memory, as well as in the lvl1_mt_map variable, that contains the map of metatiles in RAM.

 
  nmi main_nmi()
      // we need to update GFXs while NMI, so we do this now
      if is_collision_pending
          set_metatile_at_colrow(collision_col, collision_row, TOUCHED_WALL_MT)
          is_collision_pending=false

That change is made in the set_metatile_at_colrow function, which updates the lvl1_mt_map variable and the NES gfx memory:


  /*
  Sets the row,col metatile to a new value.
  This is done in the collisions_map variable because
  it's likely lvl1_mtmap_ptr points to ROM 
  Notice that x,y are UU values, if defined as U, 
  the math here for bit shifting wouldn't work
  */
  fn set_metatile_at_colrow (UU x, UU y, U new_mt)
      // write to the ram metatiles map
      UU map_displacement=((y<<4)+x)
      lvl1_mt_map{map_displacement}=new_mt
  
      // change that metatile position in GFX RAM
      // first the tiles
      UU gfx_addr=((y<<6)+(x<<1))
      gfx_addr+=$2000
      ppu_reset_addr(gfx_addr)    
      {PPUDATA}( mt_nw{new_mt} )
      {PPUDATA}( mt_ne{new_mt} )
      gfx_addr+=32
      ppu_reset_addr(gfx_addr)    
      {PPUDATA}( mt_sw{new_mt} )
      {PPUDATA}( mt_se{new_mt} )
  
      //calculate the new 2x2 metatiles block attributes byte
      UU mt_m_disp=(((y & %11111110) <<4)+(x & %11111110))
      U mt_2x2_pos=U(((y&1)<<1)|(x & 1))
      //
      U my_metatile=lvl1_mt_map{mt_m_disp}
      U new_attr=mt_attr{my_metatile}
      //
      my_metatile=lvl1_mt_map{mt_m_disp+1}
      new_attr|=(mt_attr{my_metatile}<<2)
      //
      my_metatile=lvl1_mt_map{mt_m_disp+16}
      new_attr|=(mt_attr{my_metatile}<<4)
      //
      my_metatile=lvl1_mt_map{mt_m_disp+17}
      new_attr|=(mt_attr{my_metatile}<<6)
      //
  
      //now access the attributes(palettes) address
      UU attr_addr_disp=(((y>>1)<<3)+(x>>1))
      ppu_reset_addr($2000+960+attr_addr_disp)
      {PPUDATA}(new_attr)  

Notice that, we were able to copy the metatiles (16H*15V) to RAM because the screen is small. Since we only have 2KB of RAM, if we were to work with a map several screens wide, we would need to use another approach.

final example 1 screen