Scrolling and PanningAll right, I guess that I think scrolling is easy because I never really put it in my books. (But in my defense, I did put page scrolling in Sams Teach Yourself Game Programming in 21 Days, and I put full layered and playfield scrolling in The Black Art of 3D Game Programming.) Scrolling games are in a class all their own, and really explaining all the 2D scrolling techniques would take a good chapter or two. Instead, I want to talk to you in a more abstract way about each scrolling method and then show you some demos. Page Scrolling EnginesPage scrolling basically means that as the player moves around on the screen and crosses some threshold, the entire screen is updated as if the player has walked into another room. This technique is very easy to implement and can be coded in a number of ways. Referring to Figure 8.43, you see a typical game universe consisting of a 4x2 matrix of full screens at 640x480 pixels each. Hence, the entire universe is 2560x960. Figure 8.43. A page scrolling universe setup.The rendering logic is simple for this setup. You load the first screen into memory, and then you can load all adjacent screens into RAM or virtualize them on disk. Either way you do it, the scrolling works the same. As the player's character moves around the screen, you test it for some boundary condition (maybe the screen edges). When the boundary condition is met, you advance to the next "room" or screen and move the player's character to the appropriate position. For example, if you were walking from left to right and you hit the right edge of the screen, the new page would be displayed with the character at the left side of the screen. Of course, this is a bit crude, but it's a start… For a demo of this technique, take a look at DEMO8_10.EXE|CPP and DEMO8_10_16b.EXE|CPP (the 16-bit windowed version, so make sure you are in 16-bit mode) on the CD. It basically creates a 3x1 universe and lets you move a little character around with the arrow keys. When you hit a screen edge, the image is updated. Note that I'm using bitmaps for the screen images, but there's no reason why you couldn't use vector images or a mixture. Also, I'm cheating a little on this demo by using functions from the final T3DLIB1.CPP file at the end of this chapter, but you can always look at the source if you want to. I simply needed more power than what we have so far to make a decent demo! Finally, make sure to take a look at the "terrain following" code in the demo. The character follows the floor as you move right to left by scanning for a specific color index representing the floor (color index 116, I think for the 8-bit mode, and the actual RGB value is used in the 16-bit demo). When the scanner detects the color, the character is pushed up a little, keeping it above the floor line. Homogeneous Tile EnginesThe example of scrolling wasn't really scrolling in the sense of a side-scrolling platform game. That kind of scrolling is a bit smoother—the entire screen image isn't warped page to page, but is smoothly scrolled up, down, left, or right. Using DirectX, there are a number of ways to achieve this effect. You could create a large surface and then display only a portion of it on the primary display surface, as shown in Figure 8.44. Figure 8.44. Using a large DirectDraw surface to achieve smooth scrolling.However, this only works on DirectX 6.0 (and later) and needs heavy acceleration. A better approach is to break up your worlds into tiles and then represent each screen by a square matrix of tiles or cells, where each cell represents a bitmap(s) to be displayed at that position. Figure 8.45 shows this setup. Figure 8.45. Using a tile-based data structure to represent a scrolling world.For example, you might decide to make all your tiles 32x32 pixels and to run in a 640x480 mode, which means that the a single screen will require a tile map of (640/32) x (480/32) = 20x15. Or if you decide to go 64x64, you would need a tile map of (640/64) x (480/64) = 10x7.5 or, rounding down, 10x7 (7x64 = 448; the last 48 pixels at the bottom of the screen you'll leave for a control panel). To make this work, you'll need a data structure, like an array or matrix of integers, or maybe structures that each hold the bitmap information (just a pointer or an index) along with anything else you might need. Here's an example of how you might create a tiled image: typedef struct TILE_TYP { int x,y; // position of tile in matrix int index; // index of bitmap int flags; // general flags for the cell } TILE, *TILE_PTR; Then, to hold one screen of information, you would do something like this: typedef struct TILED_IMAGE_TYP { TILE image[7][10]; // 7 rows by 10 columns } TILED_IMAGE, *TILE_IMAGE_PTR; And finally, here's a world that's 3x3 of these large tiled images: TILED_IMAGE world[3][3]; Or you might decide to just create a tile array large enough to hold 3x3 screens, 30x21, and forget the array, like this: typedef struct TILED_IMAGE_TYP { TILE image[21][30]; // 21 rows by 30 columns } TILED_IMAGE, *TILE_IMAGE_PTR; TILED_IMAGE world; NOTE You can design the data structure either way, but the single large array is easier to work with because you don't have to deal with jumping screen maps as you scroll past each 10x7 tile map. So how do you draw each screen? First, you need to load your bitmaps into a large array of 64x64 surfaces. You may have one or more tiles, and some may be repeatable, such as ships, edges, water, and so on. Figure 8.46 shows an example tile set. Figure 8.46. Bitmap template of a typical tile set.Then you write a tool, or just use an ASCII editor with some conversion software, so you can generate your tile maps. For example, you might decide to use ASCII data along with a bit of conversion software, so the numbers 0–9 may be used to indicate tiles 0 to 9 in the tile set. Given that, you would need to define a tile set composed of a 30x21 set of cells. Here's what I would do: // use an array of string pointers, could have used an // array of chars or int, but harder to initialize // the characters '0' – '9' represent bitmaps 0-9 in some texture memory char *map1[21] = { "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", "000000000000000000000000000000", }; During runtime, you scan the map information into your main structure, and then you're ready to render. To render, you must first have a viewport setup or a mxn window that the user is currently viewing. In most cases, this window is the same size as the screen—640x480, 800x600, and so forth—but not necessarily. You may have a control panel to the right or something that doesn't scroll. In any case, assuming that the whole screen scrolls and it's 640x480, you must take a couple of things into consideration:
Let's try and figure out what I'm talking about here. (I think it may help me too—I'm confusing myself!) All right, imagine that the viewport is at (0,0) in the uppermost tile map, as shown in Figure 8.47. Figure 8.47. Boundary problems with scrolling tile maps.In this case, you must draw only the tiles from map[0][0] to map[6][9]. But the second the map scrolls to the right or down, you're going to have to draw some of the edge tiles from the tile map to the right and directly under the current viewport. Then, as you scroll one entire cell (64x64), you won't draw an entire row and/or column of tile map[0][0]. So, you can see that any time you're going to draw a rectangular collection of tiles that's always 10x7, those tiles will come from one or more tile maps. Moreover, as you scroll +/- from positions that are multiples of 64, you'll only see part of the edge tiles, so clipping is involved. Luckily, DirectDraw clips all bitmap surfaces for you, so if you draw a bitmap that's partially off the screen, it will just be clipped. Hence, your final algorithm only needs to determine the tiles to render, look up the bitmap surfaces that each tile represents, and then send them to the blitter. For a demo of this, check out DEMO8_11.EXE|CPP and DEMO8_11b.EXE|CPP (the 16-bit version, make sure the desktop is in 16-bit mode) on the CD. They create the exact world just discussed and allow you to move around in it. Sparse Bitmap Tile EnginesThe only problem with tile engines is that there are a lot of bitmaps to draw. Sometimes you may want to make a scrolling game but you don't have a ton of graphics to scroll around. Moreover, you might not want to make all the tiles the same size. This is true for a lot of space shooters, because those games are mostly blank space. For those types of worlds, you want to create a universe map that's very large (as usual)—let's say 4x4 (or 40x40) screens for argument's sake. Then, instead of having tile maps for each subscreen, you simply place each object or bitmap at any location in world coordinates. Using this method, not only can each object bitmap be any size, but it can be placed anywhere. The only problem with this scheme is efficiency. Basically, for any position where the current viewport resides, you must find all the objects that are within it so they can be rendered. If you have a small universe with a small number of objects, testing 100 or so objects for inclusion in the current windows isn't going to kill you. But if you have to test thousands of objects, it just might kill you! The solution to the problem is to sectorize the game universe. In essence, you create a secondary data structure that tracks all the objects and their relations to a number of cells that you break the universe up into. But wait a minute, aren't you just using a tile map set again? Yes and no. In this case, the sectors can be any size and don't really have any relationship to the screen size. The selection of their size is more related to collision detection and tracking. Take a look at Figure 8.48, which shows the data structures and their relationships to the screen, world, and viewport. Figure 8.48. Sparse scrolling engine data structures.Note that this is only one method of solving the problem; there are many more, of course. Nevertheless, the point is that you have a collection of objects that are spaced out in a universe that is much larger than the screen—maybe 100000x100000—and you want to be able to move around in it. No problem, just position all the objects with their real-world coordinates and then, based on where the viewport window is (which is 640x480), map or translate all the objects in the window to the screen or video buffer. Of course, I'm again assuming that you have good clipping, because many objects will partially extend off the video display surface when drawn. As an example of sparse scrolling, I've created a space demo (this isn't a high-budget production, you know) that allows you to move around in a starfield along with a number of stellar objects. In this demo, there isn't very much data structure support for the sectorizing, and the objects themselves are placed randomly. In real life, you would of course have world maps, sectorizing for collision and rendering optimization, and so on. I've written a demo of sparse scrolling named DEMO8_12.CPP|EXE along with the 16-bit version DEMO8_12_16b.CPP|EXE (as always you must have the desktop in 16-bit mode). Again, they make use of the T3DLIB1.CPP|H files, so make sure to include them in your project. DEMO8_12.EXE/DEMO8_12_6B.EXE basically loads a number of bitmap objects and then randomly places them in a space that's 10x10 screens in size. Then you navigate around the world with the arrow keys as usual. The beauty of this type of scrolling is that the entire screen doesn't need to be rendered. Only the bitmaps that are visible or partially visible are rendered. Hence, there's a clipping phase in this demo where each object is tested, and if it's totally invisible, it isn't sent to the bitmap rendering code. |