Code archives/Graphics/Cache Map / Edge Update Scroller
This code has been declared by its author to be Public Domain code.
Download source code
| |||||
Okay, well most of the description is in the comments of the code but basically this is a fast bit of code which trades memory for speedy tilemap scrolling. Instead of spending a bunch of your frame time redrawing the same background as loads of tiles it uses several HUGE blits instead which is a lot faster. Also when you scroll it only updates the edges of the screen in the cached bitmap and so it maintains a very good performance. Aw hell, read the code already... | |||||
; Cache Map - Copyright 2003 - Graham Goring (AKA Dr Derek Doctors) ; For lovely smooth scrolling when using titchy tiles with none of the horrid processor overhead associated with a bazillion ; blit instructions. ; The reason I wrote this code was because I was writing a scrolling game which had 16x16 pixel tiles in it and it was ; taking a good few milliseconds just to draw a single layer of the screen. With something like 5 layers I was chewing up ; all of my frame time on something that I didn't even want to spend a tenth of that time on. And so I realised that ; I could save a lot of time at the expense of a little memory if I dumped the tiles I drew into a dummy image and then ; just drew that every frame, updating the contents of that image whenever I scrolled. However you can't have images wider ; than the screen with some video cards and so getting it all to fit into an image that's precisely the size of the screen ; when you've got fractions of tiles meant I had to do a bit of thinking and ask a few colleagues what they'd recommend. ; Anyhoo, this is what I came up with and it works a treat. During normal operation I can happily draw 4 layers of 16x16 ; tiles in about a millisecond on my home PC. It obviously slows down a bit when you scroll the screen, but unless I ; moved the screen by more than a few tiles every frame (which is VERY fast) then it didn't tick over the 2 milliseconds mark. ; And so, in a display of generosity and goodwill I share this code with everyone else in Blitz land, so that you might ; produce a game which runs happily on a P2-300 instead of an Athlon 1.2Ghz T-bird. ; The dependancies of this code is that your map array has at least 3 dimensions, those being x and y, naturally, and ; also a layer. Now it might well be that you don't want multiple map layers in your game (what? no parallax?! For shame!) ; so just DIM the array as "map (width,height,0)" and the functions won't need altering at all. Alternatively you could ; strip all the layer bumph out of this code, which should only entail buggering about with the "CMAP_fill_boxes" function. ; Note that all functions are prefixed with "CMAP_" and all arrays, constants and globals with "cm_" so that you don't ; get any conflicts. Unless you inexplicably chose to prefix your own stuff with this, too, in which case all I can say ; is "Woooh! S-s-spooky!". ; Oh, and it only uses square tiles. You could adapt it to rectangular ones I expect, but what the hell would you be using ; those for?! And you can't have maps smaller than the screen. Well, I don't think so. It'd probably fall over if you did. ; Now, to get this working you'll need to pop over to the "CMAP_fill_boxes" function and alter the bit which draws the tiles ; to the screen so that it uses the correct image handle and also the tile array conforms to the name and format of your own ; one. Also note if you're using offset tiles (ie, tiles can be drawn off-grid to allow for more organic levels) then the ; there's a little bit of overdraw built into the routine to allow for this, but you should only be offsetting your tiles ; down-right (which is easist, anyway). ; After that just replace the 6 ?'s below with suitable values or constants. ; To set up a map layer, simply call "CMAP_create_layer" followed by "CMAP_initialise_layer" with the attendant parameters. Look at ; the functions for more of an explanation. ; Then you just need to call "CMAP_scroll_display" for each layer with the distances that they've moved every frame, then ; just "CMAP_blit" each layer (note that you can supply a pair of offsets to the bltting routine if you have a panel at the top or ; left of the screen). ; Note that all the functions are called on a layer by layer basis so if you have 3 layers you'll need to call it 3 times for ; layers 0, 1 and 2 with the appropriate parameters. I didn't want to automate this at all with parallax code built in so ; that you were free to do whatever you liked with the layers. ; There are no restrictions for the use of this code, however I completely wash my hands of it if you manage to blow up ; your computer due to it's sheer excellence or bizarrely murder someone as a result of using it. You may change it as you ; wish however I ask that you give me a credit for the code if you ever release anything using it (in fact if you make ; something commercial with it I *insist* on a credit because I ain't getting any wonga out of the deal) and that you ; don't redistribute it or claim that you wrote it. If you do I'll squeeze your head until it pops like a water balloon full ; of liquefied meat. ; Oh, in case you're wondering what all the references to "offset tiles" means, it's when you have a pair of values in ; the map array for each tile which say how far it's offset from it's default position (usually it's a value from 0 to TILESIZE-1) ; on each axis. To be honest, it's not often used because it's easy to carve odd holes in your map with it, but it can ; have its applications. Const cm_map_width = ? ; width of the map array in tiles - probably be set to another constant in your own code Const cm_map_height = ? ; height of the map array in tiles - probably be set to another constant in your own code Const cm_max_cache_map_layers = ? ; this is the number of layers in the cache map minus one (because arrays start at 0) Const cm_display_width = ? ; this is the width of the display and must be a multiple of the tilesize or it all goes kerplooey Const cm_display_height = ? ; this is the height of the display and must be a multiple of the tilesize or it all goes kerplooey Const cm_tilesize = ? ; this is the width and height of the tiles in your tileset. Type cm_box Field tlx,tly,width,height,tilex,tiley End Type Type cm_cachemapglobals Field x_divider [cm_max_cache_map_layers] ; this and the below variable point to the vertical and horizontal dividers of the cache tilemap Field y_divider [cm_max_cache_map_layers] Field x_offset [cm_max_cache_map_layers] ; this and the below variable point to the position within the tilemap of the current top-left block of the image Field y_offset [cm_max_cache_map_layers] Field temp_x_offset [cm_max_cache_map_layers] ; this is used to store the old position of the offset and is very important. Least I think so... Field temp_y_offset [cm_max_cache_map_layers] Field img [cm_max_cache_map_layers] ; this is a table of pointers to images used to store the cached images Field array_start_layer [cm_max_cache_map_layers] ; this is the start layer of the map array from which the tiles for this display layer are gotten from Field array_end_layer [cm_max_cache_map_layers] ; and this is the end layer of same End Type Global cm_cmg.cm_cachemapglobals = New cm_cachemapglobals ; this is the global structure which contains all the operating variables of the cache map functions ;CMAP_create_layer (0,0,0) ; creates a layer which should be done for every layer at the start of the game taking the layer number, ; the first and last array layers which contribute to the layer and three optional values for Red, Green and Blue ; mask components. It defaults to bright pink (255,0,255) ; After calling the create layer stuff for each display layer (remember each layer uses up a fair chunk of memory - if you're running in ; 16bit mode and at 640,480 then it'll use 600K) then you'll probably want to correct each of the cmg\x_offset[layer] and cmg\y_offset[layer] ; to point to the correct place for the start of your game and then refresh the screen using the relevant function (refresh_screen (layer)) ; Then it's just a case of keeping track of how far it scrolls in any direction and pushing the image appropriately. I'd suggest ; keeping an array like layer_positions (cm_max_cache_map_layers,1,1) where you store the x and y of each layer and the previous frames x and y ; too and then see how far it's changed and push away. Function CMAP_scroll_display (x_push,y_push,layer) ; This routine is called every frame with the distances you want to scroll the screen ; on the x and y axis. It deals with horizontal and vertical movement separately to ; avoid rogue blocks appearing. It took a little while to fix that bug down despite my ; knowing exactly what was causing it from the outset. But then that's the joy of ; programming, innit? ; Oh, except don't bother calling it if you've not moved the screen as it won't create any ; boxes at all and will just be a waste of time. cm_cmg\temp_x_offset[layer] = cm_cmg\x_offset[layer] cm_cmg\temp_y_offset[layer] = cm_cmg\y_offset[layer] cm_cmg\x_offset[layer] = cm_cmg\x_offset[layer] + x_push cm_cmg\y_offset[layer] = cm_cmg\y_offset[layer] + y_push If (x_push<>0) CMAP_push_horizontal(x_push,layer) CMAP_split_boxes() CMAP_fill_boxes(layer) EndIf cm_cmg\x_divider[layer] = (cm_cmg\x_divider[layer] + x_push + cm_display_width) Mod cm_display_width cm_cmg\temp_x_offset[layer] = cm_cmg\x_offset[layer] If (y_push<>0) CMAP_push_vertical(y_push,layer) CMAP_split_boxes() CMAP_fill_boxes(layer) EndIf cm_cmg\y_divider[layer] = (cm_cmg\y_divider[layer] + y_push + cm_display_height) Mod cm_display_height End Function Function CMAP_push_horizontal (x_push,layer) ; This routine defines the necessary blocks to scroll the screen left or right. ie, ; those areas of the screen which need to be redrawn to accomodate the new position ; of the x_divider (the line which says where the left edge of the screen is in the ; image "cm_cmg\img[layer]") If (x_push>0) b.cm_box = New cm_box b\tlx = cm_cmg\x_divider[layer] b\tly = cm_cmg\y_divider[layer] b\width = x_push b\height = cm_display_height b\tilex = cm_cmg\temp_x_offset[layer] + cm_display_width b\tiley = cm_cmg\temp_y_offset[layer] EndIf If (x_push<0) b.cm_box = New cm_box b\tlx = cm_cmg\x_divider[layer] + x_push b\tly = cm_cmg\y_divider[layer] b\width = Abs (x_push) b\height = cm_display_height b\tilex = cm_cmg\x_offset[layer] b\tiley = cm_cmg\temp_y_offset[layer] EndIf End Function Function CMAP_push_vertical (y_push,layer) ; This routine defines the necessary blocks to scroll the screen up or down. ie, ; those areas of the screen which need to be redrawn to accomodate the new position ; of the y_divider (the line which says where the top edge of the screen is in the ; image "cm_cmg\img[layer]"). If (y_push>0) b.cm_box = New cm_box b\tly = cm_cmg\y_divider[layer] b\tlx = cm_cmg\x_divider[layer] b\height = y_push b\width = cm_display_width b\tiley = cm_cmg\temp_y_offset[layer] + cm_display_height b\tilex = cm_cmg\temp_x_offset[layer] EndIf If (y_push<0) b.cm_box = New cm_box b\tly = cm_cmg\y_divider[layer] + y_push b\tlx = cm_cmg\x_divider[layer] b\height = Abs (y_push) b\width = cm_display_width b\tiley = cm_cmg\y_offset[layer] b\tilex = cm_cmg\temp_x_offset[layer] EndIf End Function Function CMAP_split_boxes () ; This routine moves those boxes which are completely outside the edge of the screen ; so that they are within it, and also breaks those boxes which go over the edge of ; the screen into two new boxes. It works recursively so as to chop up every last box ; if necessary, though I suspect the recursive part of it really isn't necessary - I'm ; just too scared to take it out... ;) Repeat flag=0 For b.cm_box=Each cm_box If ( (b\tlx < 0) And (b\tlx+b\width-1 < 0) ) Or ( (b\tlx > cm_display_width-1) And (b\tlx+b\width-1 > cm_display_width-1) ) b\tlx=(b\tlx+cm_display_width) Mod cm_display_width flag=1 EndIf If ( (b\tly < 0) And (b\tly+b\height-1 < 0) ) Or ( (b\tly > cm_display_width-1) And (b\tly+b\height-1 > cm_display_width-1) ) b\tly=(b\tly+cm_display_height) Mod cm_display_height flag=1 EndIf If (b\tlx < 0) ; box starts off the left edge of screen b\tlx=b\tlx+cm_display_width ; bumps it forward so the next line catches it. Easier for me. :) EndIf If (b\tlx+b\width > cm_display_width) ; box goes off right edge of screen a.cm_box = New cm_box a\tlx = 0 a\tly = b\tly a\width = (b\tlx + b\width) - cm_display_width a\height = b\height b\width = b\width - a\width a\tiley = b\tiley a\tilex = b\tilex+b\width flag = 1 EndIf If (b\tly < 0) ; box starts off the top edge of screen b\tly=b\tly+cm_display_height ; bumps it forward so the next line catches it. Easier for me. :) EndIf If (b\tly+b\height > cm_display_height) ; box goes off bottom edge of screen a.cm_box = New cm_box a\tly = 0 a\tlx = b\tlx a\height = (b\tly + b\height) - cm_display_height a\width = b\width b\height = b\height - a\height a\tilex = b\tilex a\tiley = b\tiley+b\height flag = 1 EndIf Next Until (flag=0) End Function Function CMAP_fill_boxes (layer) ; This plonks the relevant tiles into the boxes defined by the other routines. You'll most likely need to alter ; the line "DrawImage gfx_handle,xx*cm_tilesize,yy*cm_tilesize,map(tx,ty,l,0)" unless there's been an astounding ; coincidence... SetBuffer ImageBuffer(cm_cmg\img[layer]) For b.cm_box = Each cm_box Viewport b\tlx , b\tly , b\width , b\height Cls For l=cm_cmg\array_start_layer [layer] To cm_cmg\array_end_layer [layer] ; comment out if no layers! For xx=(b\tlx/cm_tilesize)-1 To ((b\tlx+b\width-1)/cm_tilesize) For yy=(b\tly/cm_tilesize)-1 To ((b\tly+b\height-1)/cm_tilesize) tx=(b\tilex/cm_tilesize) + ( xx - (b\tlx/cm_tilesize) ) ty=(b\tiley/cm_tilesize) + ( yy - (b\tly/cm_tilesize) ) If (tx>=0) And (ty>=0) And (tx<cm_map_width) And (ty<cm_map_height) DrawImage gfx_handle,xx*cm_tilesize,yy*cm_tilesize,map(tx,ty,l,0) ; alter this line to match the graphic handle and map array of your program EndIf Next Next Next ; comment out if no layers! Delete b Next SetBuffer BackBuffer() End Function Function CMAP_refresh_tiles (x,y,width,height,layer) ; This function is for when you want to refresh part of the display without the hassle ; of re-drawing the whole caboodle - which is obviously what we wanted to avoid in writing ; this whole damn shebang. ; First of all it chops off any edges of the refreshed area that are outside the visible ; screen and then it creates a "cm_box", which is passed through the regular splitting and ; filling functions. ; A practical example of when you'd use this is when you blow up a tile in your game that's ; currently on-screen. Unless you refresh that part of the display it won't actually disappear ; despite your updating of the map array. ; In instances where you have offset tiles you'll obviously want to refresh a slightly larger box so that ; offset tiles aren't chopped off, which would be a tragedy of immense proportions, possibly leading to ; downfall of Rome (if that hasn't already happened). ; The variables passed to it are full-size world co-ordinates (ie, not divided by tilesize). If (x < cm_cmg\x_offset[layer]) ; if the box starts off the left of the screen we need to chop that edge off of it. width = width - (cm_cmg\x_offset[layer] - x) x = cm_cmg\x_offset[layer] EndIf If (y < cm_cmg\y_offset[layer]) ; if the box starts off the top of the screen, chop! height = height - (cm_cmg\y_offset[layer] - y) y = cm_cmg\y_offset[layer] EndIf If (x + width >= cm_display_width + cm_cmg\x_offset[layer]) ; if it trails off the right of the screen... width = (cm_display_width + cm_cmg\x_offset[layer]) - x EndIf If (y + height >= cm_display_height + cm_cmg\y_offset[layer]) ; if it trails off the bottom of the screen... height = (cm_display_height + cm_cmg\y_offset[layer]) - y EndIf If (width>0) And (height>0) And (x < cm_display_width + cm_cmg\x_offset[layer]) And (y < cm_display_height + cm_cmg\y_offset[layer]) ; if the box is actually anywhere on the screen b.cm_box = New cm_box b\tilex = x b\tiley = y b\tlx = (x - cm_cmg\x_offset[layer]) + cm_cmg\x_divider[layer] b\tly = (y - cm_cmg\y_offset[layer]) + cm_cmg\y_divider[layer] b\width = width b\height = height CMAP_split_boxes() CMAP_fill_boxes(layer) EndIf End Function Function CMAP_refresh_screen (layer) ; Just a shorthand to make refreshing the whole screen easier for first timers. CMAP_refresh_tiles (cm_cmg\x_offset[layer],cm_cmg\y_offset[layer],cm_display_width,cm_display_height,layer) End Function Function CMAP_blit (layer,offsetx=0,offsety=0) ; This plonks the contents of "img" to the screen at the right places, though the contents ; of the "img" drawn as-is looks kinda' odd as it will appear to have been rolled in the x ; and y axis. ; Try un-commenting the following line to see exactly how the screen display works and it'll ; help you gain a better understanding of why this method of scrolling is so fast (you'll need ; to comment out the four following lines as well or they'll just draw over it). ; DrawImageRect cmg\img[layer],0,0,0,0,screenwidth,screenheight DrawImageRect cm_cmg\img[layer] , offsetx , offsety , cm_cmg\x_divider[layer] , cm_cmg\y_divider[layer] , cm_display_width-cm_cmg\x_divider[layer] , cm_display_height-cm_cmg\y_divider[layer] ; bottom-right chunk of the screen DrawImageRect cm_cmg\img[layer] , (cm_display_width-cm_cmg\x_divider[layer])+offsetx , (cm_display_height-cm_cmg\y_divider[layer])+offsety , 0 , 0 , cm_cmg\x_divider[layer] , cm_cmg\y_divider[layer] ; top-left chunk of the screen DrawImageRect cm_cmg\img[layer] , offsetx , (cm_display_height-cm_cmg\y_divider[layer])+offsety , cm_cmg\x_divider[layer] , 0 , cm_display_width-cm_cmg\x_divider[layer] , cm_cmg\y_divider[layer] ; bottom-left chunk of the screen (I think) DrawImageRect cm_cmg\img[layer] , (cm_display_width-cm_cmg\x_divider[layer])+offsetx , offsety , 0 , cm_cmg\y_divider[layer] , cm_cmg\x_divider[layer] , cm_display_height-cm_cmg\y_divider[layer] ; top-right chunk of the screen (again, I think) End Function Function CMAP_clear_layer (layer) ; Clear the given layer SetBuffer ImageBuffer(cm_cmg\img[layer]) Cls SetBuffer BackBuffer() End Function Function CMAP_create_layer (layer,start_array,end_array,maskr=0,maskg=0,maskb=0) ; Should be called to set up the globals. If you don't call it then a horrible monster will eat your eyes out. ; layer = The raster layer number ; start_array = This is the first layer in the array where tiles for this layer are drawn from ; end_array = This is the last layer in the array where tiles for this layer are drawn from ; maskr, maskg and maskb are preset to bright pink and are the mask colours for this layer ; For instance, assume your map structure has 5 layers to it (numbered 0 to 4 naturally) and you want the third ; drawn (rastered) layer (which in an array would be number 2) to contain a composite of map layers 2 to 4, with ; preset mask colours, you'd call: ; CMAP_create_layer (2,2,4) cm_cmg\x_divider [layer] = 0 cm_cmg\y_divider [layer] = 0 cm_cmg\x_offset [layer] = 0 cm_cmg\y_offset [layer] = 0 cm_cmg\array_start_layer [layer] = start_array cm_cmg\array_end_layer [layer] = end_array cm_cmg\img [layer] = CreateImage (cm_display_width, cm_display_height) MaskImage cm_cmg\img[layer],maskr,maskg,maskb End Function Function CMAP_initialise_layer (layer,x,y) ; This will erase the contents of a layer, set it's new position, reset the divider and then fill it with tiles again. ; As with "CMAP_refresh_tiles" the co-ordinates are world co-ordinates. cm_cmg\x_divider [layer] = 0 cm_cmg\y_divider [layer] = 0 cm_cmg\x_offset [layer] = x cm_cmg\y_offset [layer] = y CMAP_clear_layer (layer) CMAP_refresh_screen (layer) End Function Function CMAP_destroy_layer (layer) ; This will free up the memory used by the cached image for this layer. Call this function after Game Over so you haven't ; got a few meg of images clogging up the RAM when you don't need them. If cm_cmg\img [layer] > 0 FreeImage cm_cmg\img [layer] cm_cmg\img [layer] = 0 EndIf End Function |
Comments
None.
Code Archives Forum