RPG Creation 101 - Part 1
Unlike most RPG tutorials I'm gonna skip the long part of creating a story, and characters, etc. Now just because I'm skipping that doesn't mean it's not important! Before going in to create an RPG you should sit down and think about that, maybe jot down a few ideas you have. Next comes planning the engine for your RPG. Here are some things you should take into account:
These are just some ideas to think about. And hopefully this series will cover them all! (Though don't expect it to show you how to setup every thing mentioned above heheh.) This month I'll show you how to create a pixel-by-pixel scrolling engine, which'll use two map layers which can give you some neat effects, such as bridges, etc. We'll also touch on some map format things. Two things I'm going to leave for you to decide is the method you use to load tiles/tilesets, and the palette. These things are both quite programmer specific and is not really that much of a trouble to set up.
So lets get into making an engine which is basically the shell for the RPG. This engine which I'm going to help you build is by no means carved in stone. Feel free to change things around and modify it to your needs. You might even know of a few ways to speed it up/enchance it in some way! (If you do then e-mail me and I'll put it in a future article.) Also the graphics routines used are not actually real. They are just put in so you know what it should be. The graphics routines should be in assembly. Maybe sometime in the future I'll tell you how to make some custom routines.
So the basic subs we'll want for now is:
'$DYNAMIC DEFINT A-Z DECLARE SUB DrawMap () 'Draws our map DECLARE SUB LoadMap (FileName$) 'Loads the map and processes it DECLARE SUB MoveUp () 'These next subs will handle moving up, down, left, and right DECLARE SUB MoveDown () DECLARE SUB MoveLeft () DECLARE SUB MoveRight ()
Those subs are the ones we'll use for this part of the series. We'll add on to them eventually. So next we need to set up our data types and global variables:
TYPE EngineType 'This'll hold our players info and some other things x AS INTEGER 'Player X position y AS INTEGER 'Player Y ... MaxX AS INTEGER 'Maximum X position in map MaxY AS INTEGER 'Maximum Y ... Direction AS INTEGER 'Current direction Animate AS INTEGER 'Animation frame tracker Action AS INTEGER 'Our action tracker MaxNPCs AS INTEGER 'Maximum amount of NPCs END TYPE TYPE MapType 'This'll hold the map data. tile AS INTEGER 'Tile number in map tile2 AS INTEGER 'Tile number for layer 2 Collision AS INTEGER 'Collision data in map END TYPE CONST South = 1, North = 2, West = 3, East = 4 'Constant values for directions CONST True = -1, False = 0 'True & false (duh!)
DIM SHARED Engine AS EngineType 'Main engine variable DIM SHARED map(0 TO 63, 0 TO 63) AS MapType 'Map data. 'Here we're gonna dimension our tileset arrays and our offscreen video buffer. DIM SHARED tilearray(1, 1), PlayerGFX(1, 1), buffer(31999)
These are the data structures we'll be using for right now. Notice we're gonna use a 64x64 map. If we were using XMS to store the map data we could have a much larger map. Some things like Engine.MaxNPCs we'll leave for now. So now we need to load in the tiles, palettes, maps, and setup the screen mode to SCREEN 13:
'320x200x256 resolution SCREEN 13 DEF SEG = &HA000 'Video segment is default segment LoadPalette "yourpal.pal" 'Load our palette LoadTiles "tiles.dat", tilearray() 'Load the tiles LoadTiles "npcs.dat", PlayerGFX() 'Load the NPCs LoadMap "yourmap.map" 'Load the map
The routines to load the palette and tiles are not real, they are just shown so you know where to put the call to your routines. You will notice that when I load the tiles it is actually loading a tileset. So now we're ready to draw our first view and let the player move around the map. How are we gonna do that? Well we'll create a loop that'll contiually check if a key is being pressed and we'll update the screen no matter if a key is being pressed or not. This allows us to check the FPS and it'll be easier when we implement NPC's and other things like animations. So here's out main loop:
DrawMap 'Draw the map VideoCopy VARSEG(buffer(0)), VARPTR(buffer(0)) 'Copy it to video memory DO 'Start the loop k = INP(&H60) 'Read keyboard status. Faster than INKEY$ SELECT CASE k 'Now we're gonna find out what key was pressed CASE 72 'The up arrow key MoveUp 'Move player up Engine.Action = True CASE 80 'The down arrow key MoveDown 'Move player down Engine.Action = True CASE 75 'The left arrow key MoveLeft 'Move player left Engine.Action = True CASE 77 'The right arrow key MoveRight 'Move player right Engine.Action = True CASE 1 'The ESC key END 'Stop the program END SELECT IF Engine.Action = True THEN 'If the user moved we'll call the script interpreter Engine.Action = False 'The script interpreter will be put in later =) END IF DrawMap 'Update the screen RPGVideoCopy VARSEG(buffer(0)), VARPTR(buffer(0)) fps = fps + 1 'Update FPS counter IF starttime& + 1 < TIMER THEN 'If one second has elapsed then we'll show the FPS fps2 = fps fps = 0 'Set it back to 0 starttime& = TIMER END IF LOCATE 1, 1: PRINT fps2 'Display the frame rate LOOP
Notice that we have put in a FPS counter. This should be removed when we actually make a game. But we'll leave it in for now. I think that was pretty self-explanatory especially with the comments. The code itself is not to hard too understand I think.
Now then, we want to add the MoveXXXX routines to let the player stretch his legs a bit right? Now this is pretty simple in itself. I think we'll make a pixel-by-pixel scroller with Zelda-style free movement! This is actually pretty easy to do excluding the collision detection. Our collision detection is gonna be "map-grid-perfect" which will make it hard to walk through areas with small openings all around. This is easily fixed once we have a multiple keypress handler. So I'm gonna show you the MoveXXXX routines now. Read the comments if you don't understand it:
SUB MoveDown Engine.Direction = South 'We're moving south. 'What we want to do is get the collision status of two parts of the tile below us. Since our engine deals 'with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT(Engine.x \ 16), INT((Engine.y + 16) \ 16)).Collision tile2 = map(INT((Engine.x + 15) \ 16), INT((Engine.y + 16) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.y = Engine.y + 1 'Set to something different for faster movement END IF END SUB SUB MoveLeft Engine.Direction = West 'We're moving west 'What we want to do is get the collision status of two parts of the tile to the west of us. Since our engine 'deals with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT((Engine.x - 1) \ 16), INT((Engine.y) \ 16)).Collision tile2 = map(INT((Engine.x - 1) \ 16), INT((Engine.y + 15) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.x = Engine.x - 1 'Set to something different for faster movement END IF END SUB SUB MoveRight Engine.Direction = East 'What we want to do is get the collision status of two parts of the tile to the east of us. Since our engine 'deals with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT((Engine.x + 16) \ 16), INT((Engine.y) \ 16)).Collision tile2 = map(INT((Engine.x + 16) \ 16), INT((Engine.y + 15) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.x = Engine.x + 1 'Set to something different for faster movement END IF END SUB SUB MoveUp Engine.Direction = North 'What we want to do is get the collision status of two parts of the tile to above us. Since our engine 'deals with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT(Engine.x \ 16), INT((Engine.y - 1) \ 16)).Collision tile2 = map(INT((Engine.x + 15) \ 16), INT((Engine.y - 1) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.y = Engine.y - 1 'Set to something different for faster movement END IF END SUB
Hopefully that isn't to hard to understand. =) Read the comments, they explain it all. Now for the drawing of the map screen. We're gonna have a camera in our RPG which can also be used for cutscenes. Here's the code:
SUB DrawMap CameraX = Engine.x - 160 'Get the upper left map position on the CameraY = Engine.y - 96 'the screen. xtile = INT(CameraX \ 16) 'The the actual map gird position. ytile = INT(CameraY \ 16) IF CameraX < 0 THEN CameraX = 0 'If the Camera pos is less then 0 make it 0 IF CameraY < 0 THEN CameraY = 0 'If the Camera position is greater then the screen boundaries make it equal 'the screen boundaries IF CameraX > Engine.MaxX - 320 THEN CameraX = Engine.MaxX - 320 IF CameraY > Engine.MaxY - 200 THEN CameraY = Engine.MaxY - 200 FOR x = 0 TO 21 '22 Columns FOR y = 0 TO 13 '14 Rows tile = map(x + xtile, y + ytile).tile 'Get layer 1 tile from map buffer tile2 = map(x + xtile, y + ytile).tile2 'layer 2... Xpos = CameraX MOD 16 'Get offset into the screen to Ypos = CameraY MOD 16 'draw at. 'Now draw layer 1 which is not drawn with transparency PutSolid VARSEG(buffer(0)), (x * 16) - Xpos, (y * 16) - Ypos, tilearray(0, tile) 'Now we're gonna draw the second layer IF tile2 <> BlankTileNum THEN 'We don't want to draw a blank tile 'If the tile isn't blank then draw the second layer tile with transparency Put VARSEG(buffer(0)), (x * 16) - Xpos, (y * 16) - Ypos, tilearray(0, tile2) END IF NEXT y NEXT x 'Here is where you draw the NPC's including the player. Xpos = Engine.x - CameraX 'Now get the player's coords Ypos = Engine.y - CameraY 'to draw at on screen PlayerPic = ((Engine.Direction * 2) - 1) 'Get frame to draw player at Put VARSEG(buffer(0)), Xpos, Ypos, PlayerGFX(0, PlayerPic) 'Draw it END SUB
Well that will draw two layers onto the off screen buffer. The routine which copies the buffer to video memory should be an asm routine. For right now to see it work you can do this: change all the VARSEG(buffer(0)) to &HA000, then it'll draw the sprites to video memory. But also you'll notice the put routine used is also from a library! Well, I didn't want to get too library specific so I made up some hypothetical graphics routines. Use DirectQB or Future.Library or something for now...
Lastly I'll show you a map loading routine I use. The map data is stored like this:
Map data (layer 1 tile number, layer 2 tile number, collision number)
It's pretty simple. Throughout this series we'll modify this to accomodate NPC's and other things. Here's my map loading routine:
SUB LoadMap (FileName$) IF INSTR(FileName$, ".") = 0 THEN 'Add a .MAP extension if there FileName$ = FileName$ + ".MAP" 'isn't one END IF OPEN UCASE$(FileName$) FOR INPUT AS #1 'Open it INPUT #1, ImageFile$ 'Read tileset file INPUT #1, PalName$ 'Palette... INPUT #1, StartX, StartY 'Starting coords INPUT #1, Engine.MaxX, Engine.MaxY 'Map size 'Set maximum row and column values to 1 less, since the map buffer starts at 0 not 1 Engine.MaxY = Engine.MaxY - 1 Engine.MaxX = Engine.MaxX - 1 'Load map data into 2 layers FOR i = 0 TO Engine.MaxY FOR J = 0 TO Engine.MaxX INPUT #1, map(J, i).tile, map(J, i).tile2, map(J, i).Collision NEXT J NEXT i 'Calculate the map size in pixels. Engine.MaxX = Engine.MaxX * 16 Engine.MaxY = Engine.MaxY * 16 CLOSE #1 END SUB
The map data could also have been stored in binary too, but regardless of format type, it works. Notice that you don't have to specify an extension on the filename if it ends in .map =) Well that's it for this part of the tutorial and I'd have to say it's pretty big! Next time I'll cover stuff like NPC's and animation. I also might include a demo program. If you don't understand something here then please e-mail me (address is at the top).