Ram Tile Effects Primer

From Uzebox Wiki
Jump to navigation Jump to search
  • Horrible formatting, will correct when I have time.

Ram Tile "Effects" Primer

Intro

This tutorial is aimed at user's who has have at least a rudimentary understanding of video mode 3(like myself), and and are interested in adding a few easy ram tile effects to their application. This is a capability, exploited correctly, that would add a lot of creativity and possibility to games. At the very least this is an interesting frontier that I haven't noticed many examples or conversations for. It is likely that none of this is the actually the best way to implement such things, as such I'd appreciate if more experienced members would contribute their improvements. Total credit of course, I'm only supplying sample code here for now. Also I tend to rant when I'm really just trying to figure these things out myself! "If you don't know something, then teach it!", it works for me generally. Hopefully these examples are understandable but not so simplistic as to be irrelevant. Big credits to Uze for making mode 3 possible, and Paul for the inspiration on sprite curtains.

Notes

The example code is pretty lame, but as best as I can figure there is really no way to make this totally generic for all situations. Honestly I don't even see a graceful way to implement this in the kernel when you consider the huge variations in requirements per application. If you are trying this stuff during complicated gameplay, if could be hard to implement. I'd rather see a finished game with no effects than an effect ridden game that took 4 times as long. I say this only because I've wasted time on this and I'd save you from that if possible. Now if I haven't turned you off yet, below are some general ideas and a bunch of caveats for this to be useful for you.


  • Scrolling games would likely be complicated...


  • Most "effects" are extremely time intensive...


  • Currently this is bare bones and primitive, leaving the user to worry about every last detail. It's already slow, might as well be a module.


  • If you are using a ram tile for an effect, then you must insure a sprite will not use it. Make a little wrapper for placing sprites, that counts as it places sprites and stops at a certain number. Also, make it count forward on 1 frame and backwards on the next frame. It's an easy way to flicker between too many sprites(see Adventures of Lolo). After that if you are just letting your sprites go whevever they want on any axis you may wish to implement a counting mechanism to count the actual ram tiles it will take, which can be almost double under worst case. See video mode 3 implementation. You'll likely need this functionality for in game effects, ie keeping sprites below your effect tiles.


  • A lot of games just aren't suited for this, atleast during gameplay. If time is your only issue, you may want use something like "-DSCREEN_TILES_V=26 -DFIRST_RENDER_LINE=28"

in your makefile kernel options. It is a great way to make this stuff possible in anything more than Sokoban complexity. This will cause the kernel to delay vblank interrupt for 1 tile row at the top, and leave vblank 1 row early(correct?). Uze suggested this to me, stating it saves >14k cycles. Indeed even if you don't want to do ram tile effects this allows more sprites on the screen if you have the extra ram and can live with a wide screen effect. You lose the very top and bottom tiles to darkness(see Lolo when it's released). I say it's worth it.


  • Have lot's of ram tiles! If you already have your game pretty well along then design changes for a little effect might not be worth it. Otherwise I suggest creating worst case scenarios for testing, where the most cycle intensive and/or stack intensive situations in your game will occur. Increase your ram tiles by 1 then rebuild/test the worst case again. Do this until you write over the stack(glitches or crashes) then back it off by 1 and that's your limit without some redesign/optimizations. Otherwise stick to a fancy GUI.


  • If extensive use of effects is required, I'd say you will need to be in a situation where you can wait a vblank directly before/after large copies. Could hurt gameplay and there's really no way around this, unless you can deal with flicker(effect only partially done before vblank) and probably worse. In that case the effect wont finish before vblank and of course the kernel interrupts to do it's thing. You'll continue after that and be even further behind if you do it every frame, until you crash. If you can fade out or block before you set everything up, that is ideal. I hope to add a cycle count comment to all these functions to aid in determining how much you can get away with in a frame. For now common sense + trial and error. Though a higher level management interface could make this transparent by automatically calling WaitVsync(1).


  • You can create graphics in ram tiles that you don't have to store statically. Keep in mind these function calls are expensive too(and including the function just for a single call), if it takes more than a couple calls for a single tile then it might take less space just to use a normal tile. Best case is when you do several effects on a length of tiles. For text, design it's colors to fit in with the areas you wont have the extra ram tiles to change it. A nice trick for a scenario I faced, suppose you have a lot of game tiles in a tile set. You wont be able to use tiles greater than 255-RAM_TILES_COUNT, since vram is only 255 indexes and ram tile indexes are stored in vram below tiles(see the kernel code). Now what if you also need to have a large set of alpha/numerical tiles for text? What happens when you have multiple tile sets and you don't want to waste space storing the alphabet multiple times? What if you want different colored letters/background??? You could waste >8k on text for some of that! If you have the extra ram tiles you can do it nearly free. You can copy data from anywhere in flash to ram tiles, even preform swap colors during the process or other such things. Again, when you commit to this idea keep in mind you will always have to make sure your worst case scenario wont run sprites over your text ram tiles and lot of other possible situations. See Sokoban World for a simple demonstration of colored text/backgrounds, under ideal conditions. The main idea here is trade cycles for space.


Getting started

Assuming you have everything setup to compile and run a mode 3 game, here are some basic building blocks functions that most effects would work off. Put these wherever is convenient. This is not an actual library yet.

  • These examples are recreated off the top of my head, original is application specific. These concepts do work, even if I might have some small mistakes below.
  • Will Update Later, the point of this is not really the code so much as visualizing what you can do if you are willing to throw massive cycles at something.
  • Although most examples have automatic linear incrementation, it's obvious how to copy the same tile to multiple ram tiles. Save some space for readability, many similar options.


void TileToRam(int toff, int roff, int len, const char * tiles){   //copy len number of tiles from tiles to ram tiles, use Abs below to give an absolute offset.
   toff = toff<<6;//multiply by 64 to convert from ram tile index to actual address for pixel 0 
   roff = roff<<6; 
   len  = len <<6;
   while(len--)
      ram_tiles[roff++] = pgm_read_byte(tiles+toff++);
}



void TileToRamAbs(int toff, int roff, int len, const char * tiles){   //use absolute byte offsets instead of indexes, not convenient for simple things. All Abs equivelent are similar
   while(len--)
      ram_tiles[roff++] = pgm_read_byte(tiles+toff++);
}



void TileToRamColorKey(uchar toff, uchar roff, uchar len, const char * tiles, uchar colorkey){   //copy data if tile pixel is not equal to color key.
   toff = toff<<6;//multiply by 64 to convert from ram tile index to actual address for pixel 0 
   roff = roff<<6; 
   len  = len <<6;
   uchar t;
   while(len--){
      t = pgm_read_byte(tiles+toff++);
      if(t == colorkey){ram_tiles[roff] = t;}
      roff++; 
   }
}



void TileToRamSourceColorKey(uchar toff, uchar roff, uchar len, const char * tiles, uchar colorkey){   //copy data over only if ram tile pixel is not equal to color key
   toff = toff<<6;//multiply by 64 to convert from ram tile index to actual address for pixel 0 
   roff = roff<<6; 
   len  = len <<6;
   uchar t;
   while(len--){
      t = ram_tiles[roff];
      if(t != colorkey){
       ram_tiles[roff] = pgm_read_byte(tiles+toff);
      }
      roff++;
      toff++; 
   }
}



void TileToRamColorKeyAbs(int toff, int roff, int len, const char * tiles, uchar colorkey){
"read above"
}



void SwapRamColors(int roff, int len, int color1, int color2){
   toff = toff<<6;//multiply by 64 to convert from ram tile index to actual address for pixel 0 
   roff = roff<<6; 
   len  = len <<6;
   uchar t;
   while(len--){
      t = pgm_read_byte(tiles+toff++);
      if(t == color1){ram_tiles[roff] = color2;}
      roff++;
   }
}



void SwapRamColorsAbs(...);//read above



bool PutPixelToRam(uchar roff, uchar off, uchar len, uchar color){//put a pixel to the same offset of a len number ram tiles, returning false if that color was already there in any tile
   toff = toff<<6;//multiply by 64 to convert from ram tile index to actual address for pixel 0 
   roff = roff<<6; 
   len  = len <<6;
   uchar t;
   while(len--){
      t = pgm_read_byte(tiles+toff);
      if(ram_tiles[roff] == t){return false;}//you could modify this behavior if you don't want to stop before len, this implementation is aimed at dissolving with a non used color. 
      roff += 64;//jump to the same offset in the next ram tile
      toff += 64;//jump to the same offset in the next tile
   }
   return true;
}



void LineToRam(int roff, int off, uchar color, bool vertical, int len){//TODO - add line width, multiple lines/colors, read tile data instead of color...
   roff = roff<<6;   

   while(len--){
      for(uchar i=0;i<8;i++){//too lazy to check, but this might off??...you guys can figure out how to draw a line though :p
         if(vertical){roff += (off*1);}//we are drawing a vertical line so off means what xoffset to start from 
         else        {roff += (off*8);}//we are drawing horizontally, off means y offset to start from

         ram_tiles[roff} = color;
         if(vertical){roff += 8;}//get to the same x position on the line below us
         else        {roff ++;}
      }
      roff += (64-8);//get back to the first pixel of the next ram tile at the same initial offset
   }   
}

void StippleRamLinearClear(int toff, int roff, int len, const char * tiles, uchar r, uchar ck){   //copy from r offset if != ck. linear increment ram tile index for len
   toff = toff<<6;//multiply by 64 to convert from ram tile index to actual address for pixel 0 
   roff = roff<<6; 
   //len  = len<<6;
   uchar t;
   while(len--){
      t = pgm_read_byte(tiles+toff+r);
      if(ck != t)
         ram_tiles[roff+r] = t; 
      
      roff += 64; 
      toff += 64;   
   }
}
  • You get the idea, I wont make this overly bloated. Below are other things that drastically increase functionality. I'll fill them out somewhere else later.
  • void TileToRamStipple()...
  • void RamToRam()...
  • void RamToRamRotated()...copy 1 ram tile to another, rotated in 90 degree increments
  • void RamToRamFlipped()...flipped x or y or both
  • void WiggleRam()..."shake it baby."
  • void BezierToRam()//TODO draw bezier curve of color to ram
  • void SquareToRam()...
  • void InvertRamColors()....etc.


This is generally extremely simple stuff as you see, trading time for something else. No laws of physics were harmed during the making of this primer. The only complications become organization,order,and timing to accomplish something useful. Hopefully these will get an interface that will make it all easier to use.

A Few Possibilities

(See Sokoban Worldfor some demonstration)

Fade Out Effects

Uzebox has fine capabilities to fade a screen to black and back to full bright, I'd use this whenever it's possible.(will be some notes on useful fader "effects" in another tutorial) However, there currently exists no pre-built way to to say dissolve the screen or fill it with a color; unless you want to have an absurd amount of duplicate tiles. What if you wanted some parts of he screen to fade but not others? The key here is that your game needs to have no more unique tiles that you wish to effect on the screen, than you have free ram tiles. Forget it otherwise. If you are letting the kernel handle sprites, as I assume everyone is, then keep sprites off your fading tiles. When a sprite crosses your tile it will overwrite/blur your entire effect with the sprite, like a Hall of Mirrors glitch you may have seen in 3d games, same concept though no "double buffer flicker". See ProcessSprites() if you don't understand this.

Example, assume your screen has no sprites on it(no ram tile indexes in vram) and you want to perform the same operation on every tile on the screen. It's possible to handle more complicated situations but for demonstration purposes I'll stay away from application specifics. Here's one way assuming 30x28 vram. Although not the only possibility, you'll likely want to implement this in a blocking function.

   AmazingFunctionThatSetsEverythingUpInTheExactWayNecessaryThatIsntReallyPossibleForManyGames(PROBABLY|FALSE);//Warned you about it, you'll need to "write" this one ;)


   int tileBaseIndex = 0;//this needs to equal the lowest tile index in vram, totally application specific. assuming linear incrementing of tile index, 
   TileToRam(tileBaseIndex,0,UNIQUETILES,GameTiles);   //copy, starting from tileBaseIndex, UNIQUETILES # of tiles from flash to ram tile 0 onwards.
   WaitVsync(1);   //avoid problems, if copying more than ~20 tiles you should wait a frame before you do the rest.

   //At this point, we have a copy of every unique tile that is on screen in a corresponding ram tile. Now we need to point vram at those ram tiles instead of tile indexes.

   for(uchar i=0;i<(30*28);i++)
      vram[i] -= tileBaseIndex;//convert our tile indexes to ram tile indexes, since we copied the tiles to ram tiles there is no visible difference. 

   //Ok the screen is set up,looks exactly the same, and we are able to directly modify it's appearance by manipulating ram tiles
   //So we can do anything we want to it, like a lovely and permiscuous woman. But not as much fun.

   uchar color = 0xFE;//off white, same color as sprite transparency

   for(uchar count=0;count<64;count++){ //we will end up doing 64 pixels, eventually
      while(!PutPixelToRam(0,rand()%64,UNIQUETILES,color))//SLOW!! put a pixel at the same random offset in each of UNIQUETILES, of color 0xFE.
                                                      // returns false if color was already there, worst case is 63 rand()%64 calls! If this is blocking, it works 
      WaitVsync(1);//this dissolve will take a little more than 1 second, would be an infinite loop if you had 0xFE in any of the tiles. You may need something more robust.
   }

There you have an inefficient dissolve effect that should still work in time and is compact. Pretty easy, but what else could we do? How about a vertical or horizontal line fade:

   bool vertical = true;//draw horizontal or vertical line? The function handles the details, incrementing offset by 1 or by 8 if vertical.
   for(uchar i=0;i<8;i++){ //draw 8 lines total
      LineToRamTile(i,UNIQUETILES,vertical,0x00);//draw a line, of color, at offset(which means x-axis if drawing vertical or y-axis if drawing horizontal),from 0 to UNIQUETILES 
   
      WaitVsync(8);//effect takes about 1 second total
   }

From there you can figure out how to do a block out effect or other such things. You could implement a speed system, dissolve the graphics in, or countless others. To handle a more complicated case as far as tile indexes not being linear, you will have to devise your own methods. There can't be a "Do All" for this. But it wouldn't be too hard necessarily, just very app. specific. Design your graphics in order if you can, use it in situations where it works with everything else. Draw a fractal in an 8x8 framebuffer :)

Color Swapping

This is very easy, depending on what you are trying to do. If you have gobs of extra time you could even let the kernel blit your sprite, then apply one of the color replacing functions on the ram tiles it uses. Not much else to explain, there's source color and replace color. Later, some more advanced functionality.


Composite Tiles

Let's say you wanted to create a tile out of 2 or more tiles from anywhere in flash. All you have to do is use your effects in the right order. If you had a grass tile, and wanted to draw another tile on top of it only where that tile wasn't 0xFF it would be slow but not difficult. Here's an example as well as a simple(demonstration [http://uzebox.org/wiki/images/a/a9/Oldlolo.hex) TODO images??

   for(uchar x=0;x<2;x++)  //the map is 13x10 each having "LO", we will draw 2 of them starting at (2,2) 
      for(uchar i=0;i<10;i++) //read 10 rows
         for(uchar j=0;j<13;j++) //read 13 columns
            vram[((i+2)*30)+j+2+(x*13)] = pgm_read_byte(&TitleMap[(i*13)+j])+10;  //+10 because we will leave the first 10 ram tiles to be used for Lolo's sprites

   WaitVsync(1);
   
   for(int i=10;i<26;i++){//copy the brick tile to 14 ram tiles that we will later fade LOLO title into
      TileToRam(YOURTILE,i,1,GameTiles); //copy to each ram tile from 10 and on, YOUR TILE from GameTiles. A function that doesn't increment toff would not require the for loop  
   
   WaitVsync(1);
   FadeIn(4,true);

   //Stipple in LOLO
   uchar stippleCount = 0;

   while(YOUR LOOP HERE, MOVE PLAYER WHATEVER){ //YOUR GAME SPECIFIC LOOP THAT WILL HAPPEN AS IT STIPPLES INTO A COMPOSITE TILE
   if(stippleCount < 64){StippleRamLinearClear(0,11,14,GuiTiles,stippleCount++,0xFF);} //put 1 pixel at same location in each ram tile from flash if it is not 0xFF

   //....

Now you have that tile stored and you can layer many things on top of it. You can also modify the colors of any of the layers before applying them or perform transforms. In short, anything you could do with any other frame buffer. No magic, you just need a convenient way of moving data around.

Sprite Occluding Foreground(tile aligned masked blit)

TODO Sorry no demo, will document more later! This is a large area of personal interest, when I have time this is number one priority to demonstrate in a bomberman demo. Here we are assuming no scrolling and that we are manually blitting sprites or have applied a hook per sprite in ProcessSprites(). The speed penalty would be low if few foreground are used, again scrolling could be much harder. This costs NO ram tiles, but for every tile that is a foreground it requires a tile mask(255=transparent 0=opaque for instance). This allows complex shapes for foreground tiles at no *extra* cost. The best method I have thought of would be to check which tile every ram tile is being drawn over against registered foreground tiles. One slow method would be to use a bounding box test for every sprite versus every foreground in the list, inside of ProcessSprites(). It would allow non-tile aligned foregrounds. Either way if an overlap occurs, then we jump to our foreground blitter. Inside our blitter, it's pretty much like normal, except where the mask tile is 255 we read from the foreground tiles data for the pixel instead of the sprite. Under the current implementation this could be done right now by using ram tiles with higher indexes as the foreground and manual sprite handling. But I think we'd have more cycles than ram at this point, could be wrong. Lot of different ways to do this.

Example: TODO! This does work and theres still enough time for game logic. I will try and get a decent little demo together or implement it in MegaBomber. It does look cool!

Sprite Palettes/Compression

I have implemented this in the latest version of MegaBomber(not yet released) to extreme usefulness(actually makes it possible to finish it). Essentially sprite pallets allow you to reuse source data with different colors, in MegaBomber this saves 75% of sprite space allowing 4 colored bombers(and flashing poisoned bombers, not possible before, basically any colors you want) without storing each one separately. I have also implemented sprite compression to 16 colors or 4 bits per pixel, further reducing the cost of sprites to about 12.5% of what it would have normally been. It has been a huge difference, and I am of the opinion this is a very important thing to have in the kernel as an option. Hopefully by demonstrating its effectiveness, we can come up with a universal solution for 4bpp and 2bpp applications. I have created a very rudimentary tool for compressing and creating the required pallet. This will come at the cost of some speed, but currently it is working with MegaBomber. I am fairly sure it will be the most computationally expensive game so far, and at least I can show its possible with a demonstration. I am working on completing this.

Colored text

Copy text to ram tiles, swap colors. Use simple printramtiles function. See below, same concept except you only swap colors once. No speed penalty after the setup.


Rainbow text

(Again see Sokoban Worldfor some demonstration) Rainbow text is just an expansion on the color swapping concept. Below is some trash I hacked into Sokoban World, I just increment coff and call this every few frames. For these types of things to be easily useful, we'd need a system that kept track of what letters were already in ram tiles. Store that in a bit so it would only cost us 4 bytes of ram and would be fast after we have letters in vram. There is also a semi-related possibility of 15-segment(2 byte)display for font compression, though monochrome storage would achieve better quality(8 byte). Or a crazy hybrid but I'm getting off track here...

uchar printrainbow(uchar x,uchar y,const char *string,uchar ramoff,uchar sc,uchar dc,uchar coff){//wrote this in like a minute... Also I dont use an entire fontset... Example only
   int i=0;                                                                                   //Costs a ram tile for EVERY character you draw, silly! Space,duplicates etc.
   char c;
   uchar t;
   uchar sc2,dc2;
   int off=ramoff*64;//get our offset to the ram tile
	
   while(true){
      c=pgm_read_byte(&(string[i++]));		
      if(c!=0){
         sc2 = 0xFF;   //trade white from the source tiles, if your font isn't white change this
         dc2 = pgm_read_byte(&colormap[coff++]); //array holding color values we cycle through for each letter, might want this in ram
	 if(coff > 3)
            coff = 0;
		   
          c=((c&127)-32);

          for(uchar j=0;j<64;j++){//copy over the data. very slow. Long strings would run out of time before vsync.		
             t = pgm_read_byte(&GameTiles[((c+FONTSTART-19)*64)+j]);  //eh, weird font set
	     if(t == sc){
                ram_tiles[off++] = dc;
	     }else if(t == sc2){
                ram_tiles[off++] = dc2;
             }else{                      
                ram_tiles[off++] = t
             }
	     vram[(y*30)+x] = ramoff++;
	     x++;
             if(x>29)
                return ramoff;//clip it to the screen if you're worried about it ;)
	     else
	        break;
	  }
     }
     return ramoff;//return what ram tile offset we are at so the user could take this into consideration for the next strings starting point in ram tiles
}

GUI Applications

All these thing most readily lend themselves to improving a GUI(tutorial later), where you could have multiple colored menus and text. Time is on your side in these instances, assuming it is a blocking implement.

Some achievable concepts are:

  • Smooth menu corners, allowing the tiles underneath to be shown as if you were drawing them with sprites(which is likely better in reality). The only tricky part is to keep track of the tiles under each corner of the menu BEFORE you draw it. Then you will know which tile sources to draw from over the "masked" part of your menu corners(whatever color you decide that is). Sokoban World has this implementation(open the in game menu) and I do like the effect, but I think it's better done with sprites like I said.
  • Multi color anything. As described above, applied to GUI stuff.
  • Use no tiles for GUI, describe borders with lines and a curve. Apply horizontal flip + copy and 90 rotate after you have created a side and a corner(save codespace).
  • Use stipple effects to do "translucent" effects over other stuff, if you have that kind of resources laying around.
  • This needs an interface to be useful! It gets ugly without.
  • More later.