home *** CD-ROM | disk | FTP | other *** search
/ Collection of Hack-Phreak Scene Programs / cleanhpvac.zip / cleanhpvac / FSTSCRL1.ZIP / SCROLL.TXT < prev    next >
Text File  |  1995-01-10  |  13KB  |  314 lines

  1.  
  2. This  article  describes  a  fast  and efficient method of scrolling a
  3. tiled world, using 320x200 Mode X. It will scroll an infinite distance
  4. vertically,  horizontally  and diagonally. On my system, a 386/40 with
  5. an ancient 8-bit video card, it has enough time to bounce  nine  small
  6. (16x10)  sprites  around  on the screen while scrolling at a steady 70
  7. fps.
  8.  
  9. Probably I'll be flamed for posting such kindergarten stuff, but  when
  10. I  took  an  interest  in game programming (not that long ago) I found
  11. such information surprisingly difficult to find.
  12.  
  13. The method described by Diana Gruber in the  Action  Arcade  Adventure
  14. Set scrolls in all directions and is easy to implement. The problem is
  15. that whenever it reaches a tile boundary it  stops  to  copy  a  whole
  16. screenful  of pixels back to the center of the page. On my system this
  17. causes the normal 70 fps rate to miss a beat, producing a slight  jerk
  18. in  the  scrolling. Slowing down the framerate would cure this, but it
  19. still struck me as inefficient to move all those pixels from hither to
  20. yon.
  21.  
  22. I had high hopes for Dave Robert's PC Game Programming Explorer, which
  23. concentrates on such low-level coding. Although  it  is  an  excellent
  24. book in most respects, the scrolling method used is rather limited. It
  25. moves beautifully in the vertical direction and can be easily modified
  26. to scroll horizontally, but it can't go in both directions in the same
  27. game - not unless you are willing to give up page-flipping.
  28.  
  29. This method is derived  from  some  hints  in  the  PCGPE  which  were
  30. confirmed  by  a post here from Henric Steen. In an exchange of e-mail
  31. he gave me a few more pointers... Thanks, Henric.
  32.  
  33. It isn't perfect, though. The big problem  is  that  in  it's  present
  34. version  it is  incompatible with a split screen. If the user wants to
  35. see a status bar s/he will have to request one in a pop-up window,  as
  36. in   the  Commander  Keen  series.  Henric  says  that  it's  easy  to
  37. incorporate a split screen, but despite his help I must admit  that  I
  38. haven't caught on yet... but enough blather, on with the show!
  39.  
  40.  
  41. Not  everyone  uses  the same terminology, so I'll start by explaining
  42. some names I use and a few functions in my Mode X library.
  43.  
  44. ** Definitions **
  45.  
  46. Window - the "window" is the part of vram which is actually  displayed
  47. on the screen. In this method it is always 320x200 pixels.
  48.  
  49. Page  - this is NOT the same as the window. The width of a page is set
  50. by writing to the hardware, but the  starting  point  and  length  are
  51. simply determined by variables in the program.
  52.  
  53. Visible Page - the page in which the window is currently located.
  54.  
  55. Active Page - the page on which we are currently drawing or erasing
  56. sprites.
  57.  
  58. Background Page - holds a copy of the tiled background.
  59.  
  60. Page Flipping - making the active page visible and vice-versa.
  61.  
  62. ** Functions **
  63.  
  64. void window_at( unsigned pg_off, int x, int y )
  65.  
  66. This function writes to the Line Start and HPP registers  to  set  the
  67. visible window at x,y within the page which starts at pg_off.
  68.  
  69. void ltile_to_vram( char *tilearray, int tilenum,
  70.                     unsigned pg_off, int x, int y )
  71.  
  72. Gets a tile from a linear array of tiles and writes it at position x,y
  73. within the specified page.
  74.  
  75. void rect_vram_to_vram( unsigned src_off, unsigned dest_off
  76.                         int x, int y, int hgt, int width )
  77.  
  78. Uses  write mode #1  to copy a rectangular area of pixels from  x,y on
  79. the source page to the same position on the destination page.
  80.  
  81. ** Initialization **
  82.  
  83. By default the length of a Mode X line is 80 addresses, or 320 pixels.
  84. We want a page wide enough so that when the window is  centered  there
  85. is a buffer on each side to hold one column of tiles, which is a total
  86. width of 352 pixels or 88 addresses.
  87.  
  88. The height of a page will be 240 lines, which is 16 + 200 +  24.  When
  89. the window is in the initial position this leave a buffer for one tile
  90. row at the top. An even number of 16x16 tiles won't fit on a  200-line
  91. screen.  After  filling  the screen with 12 rows we have 8 extra lines
  92. hanging off the bottom. The 24-line buffer at the  bottom  allows  for
  93. these 8 lines plus another complete row of tiles.
  94.  
  95. Thus each page takes 240 * 88 = 21120 addresses, and three of them use
  96. 63360 addresses. You can put them adjacent to each other, but I spaced
  97. them approximately evenly in vram.
  98.  
  99. So now we need to create and initialize some global variables:
  100.  
  101. unsigned
  102.    visible_off =   360,    /* initial positions of pages */
  103.    active_off  = 22208,
  104.    back_off    = 44052;
  105.  
  106. int window_x = 16,         /* position of window within page */
  107.     window_y = 16;
  108.  
  109. int world_x = 0,           /* position of upper left tile in world */
  110.     world_y = 0;
  111.  
  112. After  setting  Mode X and the page width, fill all three windows with
  113. the initial background. The tiles will extend all the way  across  the
  114. page  (22  tiles) and down 14 rows - one at the top and 12 1/2 for the
  115. window.
  116.  
  117.   set_mode_x();
  118.   set_page_width( 88 );
  119.   set_pallette( pal );
  120.   window_at( visible_off, window_x, window_y );
  121.  
  122.   for( i=0; i<14; i++ )
  123.      put_tile_row( back_off, i, tiles );
  124.  
  125.   /* copy to other pages */
  126.   rect_vram_to_vram( back_off, active_off,  0, 0, 352, 224 );
  127.   rect_vram_to_vram( back_off, visible_off, 0, 0, 352, 224 );
  128.  
  129. When scrolling North, East or West the method is simple. First  go  as
  130. far as possible by moving the window within the page. When you run out
  131. of valid data move *all* the pages upward or downward  far  enough  to
  132. accommodate a new row or column of tiles.  Grab the tiles, copy to the
  133. other pages and reposition the window within the page.
  134.  
  135. Moving South is just slightly  different,  because  of  the  odd-sized
  136. buffer. To make it easy to grab rows or columns of tiles we would like
  137. to keep the top of the page aligned at a tile boundary. So after using
  138. eight  lines  of the buffer ( ++window_y == 20 ) we grab a new row for
  139. the bottom of the page, but don't reposition  the  pages  until  we've
  140. gone down an even 16 lines ( window_y == 33 ).
  141.  
  142. Meanwhile,  we're  flipping  pages  and handling sprites. With a clean
  143. copy of the tiled background on the background page,  sprites  can  be
  144. erased with a fast block copy, without regard to tile boundaries. This
  145. is more efficient than the "dirty tile" method, which requires copying
  146. a whole tile if a sprite overlaps even a few pixels in the corner.
  147.  
  148. By  now  it  has  probably  occurred to you that we can't do this very
  149. often before one of the pages goes right off the beginning or  end  of
  150. the video segment.
  151.  
  152. If it's the background page, ignore it. The positions of the pages are
  153. held in 16-bit  unsigned ints,  which  will  automaticly  wrap  around
  154. between  the  ends  of  the segment. All of the routines that write or
  155. copy pixels are presumably written in assembly. If you're  using  real
  156. mode  the index registers will also wrap, so that address a000:ffff is
  157. adjacent to a000:0000. In protected mode I believe there's an assembly
  158. directive  to  allow  manipulation  of  the lower 16 bits of the index
  159. registers, so you can have the same effect.
  160.  
  161. If the visible window becomes split, it's a more  serious  matter.  In
  162. most  video  cards  the hardware which paints the screen also wraps at
  163. the ends of the segment, but some SVGA cards don't do that.
  164.  
  165. The solution is simple. We are doing all of this in a loop that  looks
  166. something like this:
  167.  
  168. LOOP:
  169.   wait for retrace
  170.   swap( visible_off, active_off )
  171.   window_at( visible_off, window_x, window_y )
  172.   erase sprites from active page
  173.   check user input
  174.   do scrolling
  175.   if( active_off > 44416 )
  176.       swap( active_off, back_off )
  177.   draw sprites on active page
  178.   goto LOOP
  179.  
  180. Note  that  the  scrolling  is done after the sprites have been erased
  181. from the active page. At this point the active  and  background  pages
  182. are  pixel-for-pixel  identical,  so  if  the  scrolling has split the
  183. active page, just swap it with the background page. If  the  scrolling
  184. has  split the visible page it won't take effect until the *next* time
  185. it becomes visible. Before that it will take it's turn as  the  active
  186. page, and we'll catch it then.
  187.  
  188. ** Some Improvements **
  189.  
  190. This  is both simple and efficient, but as I've described it so far it
  191. still has one big problem. When it needs more tiles it first  consults
  192. the  world map and copies them one-by-one to the background page. Then
  193. it uses two block copies to transfer them to the other pages,  all  in
  194. one  frame.  That's a lot to do in one frame - in fact it's almost all
  195. the work the engine does. Out of curiosity I temporarily  removed  all
  196. sprite  handling  and the wait-for-retrace function, so it did nothing
  197. but scroll the screen as fast as it could. Under those conditions  the
  198. Watcom profiler said that execution time was distributed as follows:
  199.  
  200. 45.5%  ltile_to_vram()
  201. 49.9%  rect_vram_to_vram()
  202.  4.6%  everything else
  203.  
  204. Each new row or column is copied twice after it is grabbed, so getting
  205. them into vram takes about twice as long as a copy, and  between  them
  206. they account for almost all of the work of the scrolling.
  207.  
  208. So  the  first  15  pixels  require almost no time, then all this gets
  209. dumped into one frame... and that's not the worst. The worst  case  is
  210. when  it's  scrolling  diagonally and the tiles are aligned so that it
  211. needs *both* a new row and column at the same time. Let's tackle these
  212. problems one by one.
  213.  
  214. Do  we  really need to update all the pages at once? No, we don't. The
  215. copy to the visible page can be put off until the next frame, when  it
  216. will be the active page. So instead of doing two copies we can just do
  217. one and set a global variable to indicate that another is needed.  For
  218. instance, when scrolling East:
  219.  
  220.    /* grab new row of tiles */
  221.    put_tile_col( active_off, 21, tiles );
  222.  
  223.    /* copy to other pages */
  224.   rect_vram_to_vram( active_off, back_off, 336, 0, 16, 240 );
  225.   deferred_copy = COPY_RGT;
  226.  
  227. The  deferred  copy can be done right after the page flip, so the main
  228. loop starts like this:
  229.  
  230.   wait for retrace
  231.   swap( visible_off, active_off )
  232.   window_at( visible_off, window_x, window_y )
  233.   if( deferred_copy )  do copy
  234.   erase sprites from active page
  235.  
  236. That's a big improvement, but if we take the percentages of  execution
  237. time  as arbitrary units of time, the worst-case diagonal scroll still
  238. looks like this:
  239.  
  240.             frame   frame+1
  241. Horizontal:   75   |  25   |
  242. Vertical:     75   |  25   |
  243.  
  244. In the vast majority of games the  user  would  never  notice  if  the
  245. screen  moved  a bit horizontally before starting the diagonal scroll,
  246. so by deferring the vertical scroll we could spread it out to  one  of
  247. these:
  248.  
  249. Horizontal:   75  |  25  |  --  |           (better)
  250. Vertical:     --  |  75  |  25  |
  251.  
  252. Horizontal:   75  |  25  |  --  |  --  |    (best)
  253. Vertical:     --  |  --  |  75  |  25  |
  254.  
  255. To  implement  this  requires  some additions to the code. First, when
  256. scrolling diagonally the horizontal scroll must always be done  first.
  257. Then  in  the  vertical  scrolling  functions  we  need  to check if a
  258. deferred copy is pending. If it is, do not import a new row.
  259.  
  260. void scroll_north( void )
  261.   {
  262.    if( --window_y < 0 )
  263.      {
  264.       /* check if we have deferred copy pending */
  265.       if( deferred_copy )
  266.         {
  267.          ++window_y;               /* cancel move */
  268.          return;                   /* wait until next time */
  269.         }
  270.  
  271.    /* get new row, etc */
  272.   }
  273.  
  274. That will do for a one-frame delay, and it can be put off for one more
  275. by defining the flags for the copies with two flags in one int.
  276.  
  277. #define  COPY_TOP  0x11
  278. #define  COPY_BOT  0x21
  279. #define  COPY_LFT  0x31
  280. #define  COPY_RGT  0x41
  281. #define  NOT_YET   0x01
  282.  
  283.   /* in main loop */
  284.   if( deferred_copy )
  285.     {
  286.      switch( deferred_copy )
  287.        {
  288.         case COPY_TOP :
  289.           rect_vram_to_vram( back_off, active_off,  0, 0, 352, 16 );
  290.           break;
  291.  
  292.         /* other cases */
  293.  
  294.         case NOT_YET  :
  295.           deferred_copy = FALSE;
  296.           break;
  297.        }
  298.      deferred_copy &= 0x01;
  299.     }
  300.  
  301. So  the  first  time  through the loop it does the copy and clears all
  302. except the low bit. The second time that is cleared too,  opening  the
  303. door for the vertical scroll.
  304.  
  305. ** The End (finally) **
  306.  
  307. So  there  it  is. I make no claim to be the first to use this method,
  308. not by a long way. It's pretty simple and has probably  been  used  by
  309. umpteen  programmers  for  years.  The  only  problem  is  that nobody
  310. bothered to explain it in any kind of detail, or at least not where  I
  311. could find it.
  312.  
  313.   -]Frank[-                                              fobits@io.org
  314.