LoginLogin

How to use Random's Big Dumb Library: Game Engine

Root / Submissions / [.]

haloopdyCreated:
This is part 1 of a 3 part tutorial on how to make a basic game with Random's Big Dumb Library. Other parts: Part 2, Part 3 (coming soon) I assume you already know how to program (mostly). If you're a beginner, this will probably be confusing. We're going to make a game where you walk around and pick up items. It will use the Game Engine functions to do most of the work. In the next tutorial, we'll look into more library functions. For impatient people: First working code, Better map working code, First complete game code

Game Engine Basics

The Game Engine allows collision detection between any sprite and the BG, automatic directional walking/flying/whatever animation for any sprite, player controls (including the use of the cpad), and some debugging stuff. More will come in future updates. Your game's map needs to use the BG layer. Any entities need to be sprites. Automatic direction sprites need to follow the same basic patterns as built-in sprites: it needs a right-facing, down-facing, left-facing, and up-facing sprite (in that order) each with the same amount of animation frames. They can have any amount of animation frames. You also need to designate a single layer of the BG as the "collision" layer. Basically, either all solids in your game will need to occupy the same layer (as we'll do here), or you'll need to reserve a layer for collision data. Collision is detected by checking the tile ID for stuff you're walking into and blocking you if the ID is above a threshold (which you can set). Since SB can only define 1024 unique tiles but the IDs go from 0 to 4095 (and repeat every 1024), you can simply set the threshold to 1024 and add 1024 to all solid tiles. Some parts of the library (like the textboxes) make use of "dictionaries", which are string arrays that can have arbitrary key-value pairs. Don't worry too much about it: I'll point out the dictionaries when we use them and how to use them.

An Existing Example

If you want to see the most basic implementation of the game engine at work, you can run the RNDLIBTEST program from the Big Dumb Library project. Pick the "Collision" option and choose the "Game" engine (not SGE). You'll be able to walk around a very sparse map and see the walking animation and collision detection in action.

Starting

Player

First, we'll pick a sprite for our player. Remember, it has to follow the SmileBASIC standard player sprite format (so any of the built-ins will do). You should set it to the first right-facing sprite in the series (this is usually the first one). Use the Smile Tool to see all the available sprites and their IDs. Let's be the old lady, which is sprite 796. We're going to scale the sprite up to 2x so it's easier to see. Note that the library automatically handles scaled collision:
'Initial setup. You have to EXEC my library (any slot other than 0 will do)
ACLS
EXEC "PRG3:RNDLIBFULL"

VAR SP=SPSET(796)  'Define our sprite. Use dynamic sprite ID allocation for ease of use
SPSCALE SP,2,2  'Scale sprite up to 2X size
GAMEPREPAREPLSIMPLE SP,72,72,1,&HFFFFFFFF
GAMESETWALKDATA SP,796,4,8
To load my library, you EXEC it in any slot other than the currently running one. I like 3 because it's the last slot and I'm least likely to use it. We use the dynamic ID assignment version of SPSET to create our player sprite. The sprite ID is stored in SP. We then scale it (this is all normal). Now the library code: GAMEPREPAREPLSIMPLE prepares a sprite for player use. It attaches controls and sets internal data so the sprite you gave IS the player. Only call this for ONE sprite at a time: if two sprites are prepared as players, you'll probably have issues. The parameters are: SpriteID, SpawnX, SpawnY, Collision Lenience, and Collision Mask. SpawnX and SpawnY are in BG pixel units: 72,72 will put us in the middle of the 5th BG tile over and down (even though there's no BG yet). Collision lenience is how much perimeter to shave off the player's hitbox. Removing the outer 1 pixel is usually a good idea: it lets you get closer to tiles. Collision mask is the same value used in SPHITSP and SPHITRC: if a bit is set in both colliding objects' masks, they are allowed to collide. We set it to the full 32 bits to be safe. GAMESETWALKDATA prepares the sprite as a "walker". A "walker" is any sprite which can face in the 4 directions and has the same amount of animation frames per direction. It doesn't necessarily have to walk: it could fly or have just 1 frame for each direction and it would still work. The parameters are: SpriteID, Base Definition #, Animation FramesPer Direction, and Step Distance. Base definition is the sprite definition # for the first frame of the 4 direction animation set (that starts with right-facing). For instance, the King's base definition would be 576. Use the Smile Tool to see how the base definition lines up with the sprites (it's easy). Animation Frames is how many frames of walking there are per direction: the standard SB sprites all have 4 frames. Step Distance is how far the sprite has to move before it moves to the next animation frame (takes a "step"). With 8, the sprite will make 2 steps per tile (which is pretty decent). You can play around with that last parameter until you like the animation speed. Note: Any sprite (not just the player) can be a "walker". For instance, NPCs can set walk data and have the walker function applied. Walkers automatically adjust their position on screen based on their world coordinates. Walkers have collision detection which can be disabled by setting the mask to 0 (maybe you want a ghost or something).

Map

Now let's prepare the BG. We're only going to use 2 layers: one for the ground (no collision) and one for the objects (the collision and object layer). To get things started more quickly, we're ONLY going to create the perimeter of the map. This way, we'll be able to jump right in to "actually walking around". We use XSCREEN to say how many BG layers we're using. It's not exactly necessary since we're only using 2 but... might as well. We use BGSCREEN to set the layer size to 100x100 and BGSCALE to scale the map up to 2X. Note: the BG and sprite scaling don't have to match: collision detection will still work (within reason: a far-too-big player might have some issues). This is just the map code: we'll combine it with the player code later
XSCREEN 2,100,2 'Use 100 sprites and 2 BG layers (first 2 is screen mode)

VAR I
FOR I=0 TO 1
 BGSCREEN I,100,100 'Set layer size to 100x100
 BGSCALE I,2,2
NEXT

BGFILL 0,0,0,99,99,99 'Fill first layer with grass lol (grass is ID 99; first two 99's are the ends)
FOR I=0 TO 99 'Draw perimeter (use um... IDK, ugly rocks 101)
 BGPUT 1,I,0,101
 BGPUT 1,I,99,101
 BGPUT 1,0,I,101
 BGPUT 1,99,I,101
NEXT

GAMEPREPAREBG 3,1 'Use layers 0 and 1 (bitfield), and layer 1 is the collision layer
VAR("3:GAMECLSNSTART")=1 'Any tile with ID 1 and above will trigger collision
First two sections are just us setting up the map. We fill the bottom layer with grass (boring) and then draw a perimeter of rocks around the edges of layer 1 (the collision layer). GAMEPREPAREBG takes two parameters: A layers bitfield and the layer used for collision. The layers bitfield is a 4 bit value that tells the engine which layers should be automatically offset when the player moves. You don't necessarily want all the layers to move: maybe you're using a layer for rain or something and it moves independently from the map. In our case, we're using layers 0 and 1, which means we set bits 0 and 1 (comes out to 3). Setting all the layers would be 15. The engine can only use one layer for collision, so the second parameter is the collision layer ID (0-3). The last line sets a global variable in my library: normally, I expect you to use the "repeating tile highest bit" trick, so only tiles with ID>1024 will cause collision. However, to make life simpler, we'll say ANY nonzero tile causes collision. Replace the 3: in the string with whatever slot you loaded my library into.

Running

Let's put the player setup with the BG setup and see how to get the game running:
'Initial setup. You have to EXEC my library (any slot other than 0 will do)
ACLS
EXEC "PRG3:RNDLIBFULL"
XSCREEN 2,100,2 'Use 100 sprites and 2 BG layers (first 2 is screen mode)

'--Map Preparation--
VAR I
FOR I=0 TO 1
 BGSCREEN I,100,100 'Set layer size to 100x100
 BGSCALE I,2,2
NEXT

BGFILL 0,0,0,99,99,99 'Fill first layer with grass lol (grass is ID 99; first two 99's are the ends)
FOR I=0 TO 99 'Draw perimeter (use um... IDK, ugly rocks 101)
 BGPUT 1,I,0,101
 BGPUT 1,I,99,101
 BGPUT 1,0,I,101
 BGPUT 1,99,I,101
NEXT

GAMEPREPAREBG 3,1 'Use layers 0 and 1 (bitfield), and layer 1 is the collision layer
VAR("3:GAMECLSNSTART")=1 'Any tile with ID 1 and above will trigger collision

'--Player Preparation--
VAR SP=SPSET(796)  'Define our sprite. Use dynamic sprite ID allocation for ease of use
SPSCALE SP,2,2  'Scale sprite up to 2X size
GAMEPREPAREPLSIMPLE SP,72,72,1,&HFFFFFFFF
GAMESETWALKDATA SP,796,4,8

WHILE TRUE
 VSYNC
 CALL SPRITE 'Run all sprite attached functions. Our player is one
WEND
That last WHILE loop is the entire game loop. That's literally it. The game library uses sprite callbacks to make everything tick, so calling GAMEPREPAREPLSIMPLE is actually attaching a complex function to your player sprite. CALL SPRITE will run every defined sprite's callback, so that's... all you need to do. Now run it. If I didn't make any typos (and you didn't either), you should be able to walk around a big empty field with a wall of rocks around it. Wow! OK it's not that cool, so let's make it like... do something.

Better Map

The map is empty: let's fill it up with trees and paths. We'll BGFILL the entire map with solid trees, then use the SNAKEPATH library function to generate paths between a series of points. The SNAKEPATH function creates straight-line paths that zigzag randomly to connect two given points. The amount and nature of the snaking depends on the snaking data, which is given as a dictionary of values. SNAKEPATH uses a callback function so you can define how to fill the path. This means that for each location SNAKEPATH chooses to be part of the path, it will call your function with the location parameters so you can do what you want there. Note that due to this, SNAKEPATH doesn't necessarily have to be used on the BG: you can create paths on the GRP or do anything you want. We're going to fill the map with trees, generate a series of 16 points, then connect each point together with SNAKEPATH. We'll define the callback function to REMOVE trees along the path. Here's the code for generating such a thing (it should go after map generation):
'The path clearing function. It HAS to be COMMON so my library can use it
COMMON DEF CLEARPATH X,Y
 BGPUT 1,X,Y,0 'Remove the tile from X/Y (SNAKEPATH will call this for each X/Y on the path)
END

BGFILL 1,1,1,98,98,100 'Fill the inner map with trees (exclude perimeter)

'Yes, these have to be two lines. This is just how SB works.
VAR SNAKEP$[0] 'The snakepath parameter dictionary
SNAKEP$=NEWSNAKEPATH$() 'Create the snakepath parameter dictionary
VAR X[0],Y[0] 'The locations for each point (we'll use these later)
FOR I=0 TO 15
 PUSH X,10+RND(80) 'Random point at least 10 inside the walls
 PUSH Y,10+RND(80)
 IF I>0 THEN SNAKEPATH X[I-1],Y[I-1],X[I],Y[I],SNAKEP$,"CLEARPATH"
NEXT
First, we define the callback function. We won't be using it ourselves: the library will use it to generate the path the way you want it to. The function MUST be COMMON defined so my library can read it. It also must have two parameters which SNAKEPATH will pass the current path location to. We simply want to remove trees along the path, so we BGPUT tile 0 there. If you're confused about the idea of callback functions, see https://en.wikipedia.org/wiki/Callback_(computer_programming) Next, we fill the collision layer with trees using BGFILL. Now we start snaking paths. Remember, SNAKEPATH requires a bundle of parameters in one of my "dictionaries". My dictionaries are string arrays: create a 0 length string array first, then generate the parameter bundle with NEWSNAKEPATH$() as you see above. Now SNAKEP$ contains a bundle of parameters which defines path generation. We'll use all defaults here. Also, you only need to generate the parameters once: you can continue using the same parameters by using the same bundle/dictionary. We create two arrays to hold the locations of all the points we're snaking between. Later, we'll use these to place items and spawn the player (since they can't necessarily spawn at BG tile 5,5 anymore). Then we create 16 points at least 10 within the edges of the map (to account for path straying) and connect each point. The parameters for SNAKEPATH are: X1, Y1, X2, Y2, Path Generation Parameters, Callback. The X/Y are the two locations to connect with a path. The generation parameters are the dictionary we made earlier. Finally, the callback is the name of the function used to create paths as a string. Ours is "CLEARPATH".

Better Map Complete Code

To complete the code, we'll insert that snaking path stuff after generating the perimeter. I'm also shortening the perimeter code: since we're filling the inside with trees anyway, we can just BGFILL with the perimeter tile and the inside will get overwritten with trees. I'm going to put the CLEARPATH function at the bottom because I like it there better. Finally, we update the player spawn position to be the first generated point.
'Initial setup. You have to EXEC my library (any slot other than 0 will do)
ACLS
EXEC "PRG3:RNDLIBFULL"
XSCREEN 2,100,2 'Use 100 sprites and 2 BG layers (first 2 is screen mode)

'--Map Preparation--
VAR I
FOR I=0 TO 1
 BGSCREEN I,100,100 'Set layer size to 100x100
 BGSCALE I,2,2
NEXT

BGFILL 0,0,0,99,99,99 'Fill first layer with grass lol (grass is ID 99; first two 99's are the ends)
BGFILL 1,0,0,99,99,101 'Fill the collision layer with rocks (will become perimeter)
BGFILL 1,1,1,98,98,100 'Fill the inner map with trees (exclude perimeter)

'-Path Generation-
VAR SNAKEP$[0] 'The snakepath parameter dictionary
SNAKEP$=NEWSNAKEPATH$() 'Create the snakepath parameter dictionary
VAR X[0],Y[0] 'The locations for each point (we'll use these later)
FOR I=0 TO 15
 PUSH X,10+RND(80) 'Random point at least 10 inside the walls
 PUSH Y,10+RND(80)
 IF I>0 THEN SNAKEPATH X[I-1],Y[I-1],X[I],Y[I],SNAKEP$,"CLEARPATH"
NEXT

GAMEPREPAREBG 3,1 'Use layers 0 and 1 (bitfield), and layer 1 is the collision layer
VAR("3:GAMECLSNSTART")=1 'Any tile with ID 1 and above will trigger collision

'--Player Preparation--
VAR SP=SPSET(796)  'Define our sprite. Use dynamic sprite ID allocation for ease of use
SPSCALE SP,2,2  'Scale sprite up to 2X size
GAMEPREPAREPLSIMPLE SP,16*X[0]+8,16*Y[0]+8,1,&HFFFFFFFF
GAMESETWALKDATA SP,796,4,8

WHILE TRUE
 VSYNC
 CALL SPRITE 'Run all sprite attached functions. Our player is one
WEND

'The path clearing function. It HAS to be COMMON so my library can use it
COMMON DEF CLEARPATH X,Y
 BGPUT 1,X,Y,0 'Remove the tile from X/Y (SNAKEPATH will call this for each X/Y on the path)
END
Notice the player spawn location is multiplied by 16: the locations we generated were BG tile locations, but the player's spawn location is in world coordinates. Each tile is 16 wide, so we multiply by 16. We add 8 so we're in the middle of the tile. If you run this code, you should be able to walk around a much more interesting map. Use the circle pad if it's hard to fit into tight spaces.

Actual Gameplay

Let's actually add some gameplay. Our game will be simple: we're going to hide 8 items around the map (one in every other path point). The goal is to walk around and collect them all. Wow. First, our paths are a little narrow. We can remedy that by changing the CLEARPATH function: instead of BGPUT to clear a single tile, we can BGFILL to clear a 2x2 block of tiles. This way, all the paths are at least 2 wide:
COMMON DEF CLEARPATH X,Y
 BGFILL 1,X,Y,X+1,Y+1,0
END
Next, we'll need to place objects around the map. Hmmm... let's collect treasure (tile 864). Remember: SNAKEPATH destroys tiles to make the paths, so we should place our items AFTER the paths are generated:
FOR I=1 TO 15 STEP 2 'Start at 1 so we don't put a treasure right under the poor old lady
 BGPUT 1,X[I],Y[I],864 'See, the locations were useful
NEXT

Player Actions

Now for the slightly hard part. We need to be able to pick up the treasure chests. We will need to add a callback function to the PLAYER so we can monitor what they're doing and where they are per frame. We can set the player callback function in the engine. The callback function accepts a LOT of parameters and is called every time the engine processes the player. You will receive all the information you need to figure out the chest pickup in the callback. Note: The player callback parameters may change in future updates, so if you update my library you may have to update your callbacks. This is what the callback function for picking up chests might look like. This is just an example: we're going to change it later.
VAR CHESTS=8
'Remember: COMMON for callbacks
COMMON DEF PLPROCESS SP,BGX,BGY,X#,Y#,VX#,VY#,FACING,STEPPED
 'Player is standing on BGX/BGY. Together with FACING, we figure out which tile we're looking at.
 IF BUTTON(1) AND #A THEN 'They pressed the A button
  VAR IX,IY
  GAMEVECTOR FACING OUT IX,IY 'Convert FACING into unit offsets
  IX=BGX+IX
  IY=BGY+IY 'Now IX/IY hold the location the player is facing
  IF BGGET(1,IX,IY)==864 THEN
   BEEP 3 'Might as well give feedback
   CHESTS=CHESTS-1
   BGPUT 1,IX,IY,0
   IF CHESTS<=0 THEN PRINT "YOU WON!"
  ENDIF
 ENDIF
END

'This is how you'd register the callback so the library calls your function
VAR("3:GAMEPLPROCESS$")="PLPROCESS"
Your callback will be called every time the engine updates the player with the following information: The ID of the player sprite, the BG tile location of the player, the absolute X and Y world position of the player, the velocity of the player, the direction they're facing (as an #UP/#DOWN/#LEFT/#RIGHT bitfield) and whether or not they stepped this frame (useful for footstep sound effects). You don't have to use all this data, but you MUST accept it all anyway. In your callback, you handle player actions like button presses. In ours, we check to see if the player pressed A, then we see if they pressed A on a chest. If they did, we decrement the chest count and remove the item. If they found them all... they win! We use another library function called GAMEVECTOR to calculate which BG tile the player is facing. They can't be standing ON the chest, so we have to look in front of them. GAMEVECTOR converts the FACING bitfield into X and Y tile offsets. If we add these to the player's BGX/BGY location, we get the location of the tile they're looking at. The rest is simple. Just check if the tile is a chest, then remove. Also, check if they won and print a lame message.

Actual Gameplay Complete Code

To make the game like... stop when we pick up all the chests, we're going to change a little bit in the player callback. Check it out:
'Initial setup. You have to EXEC my library (any slot other than 0 will do)
ACLS
EXEC "PRG3:RNDLIBFULL"
XSCREEN 2,100,2 'Use 100 sprites and 2 BG layers (first 2 is screen mode)

'--Map Preparation--
VAR I
FOR I=0 TO 1
 BGSCREEN I,100,100 'Set layer size to 100x100
 BGSCALE I,2,2
NEXT

BGFILL 0,0,0,99,99,99 'Fill first layer with grass lol (grass is ID 99; first two 99's are the ends)
BGFILL 1,0,0,99,99,101 'Fill the collision layer with rocks (will become perimeter)
BGFILL 1,1,1,98,98,100 'Fill the inner map with trees (exclude perimeter)

'-Path and Chest Generation-
VAR SNAKEP$[0] 'The snakepath parameter dictionary
SNAKEP$=NEWSNAKEPATH$() 'Create the snakepath parameter dictionary
VAR X[0],Y[0] 'The locations for each point (we'll use these later)
FOR I=0 TO 15
 PUSH X,10+RND(80) 'Random point at least 10 inside the walls
 PUSH Y,10+RND(80)
 IF I>0 THEN SNAKEPATH X[I-1],Y[I-1],X[I],Y[I],SNAKEP$,"CLEARPATH"
NEXT

FOR I=1 TO 15 STEP 2 'Start at 1 so we don't put a treasure right under the poor old lady
 BGPUT 1,X[I],Y[I],864 'See, the locations were useful
NEXT

GAMEPREPAREBG 3,1 'Use layers 0 and 1 (bitfield), and layer 1 is the collision layer
VAR("3:GAMECLSNSTART")=1 'Any tile with ID 1 and above will trigger collision
VAR("3:GAMEPLPROCESS$")="PLPROCESS" 'This is how you'd register the callback so the library calls your function

'--Player Preparation--
VAR SP=SPSET(796)  'Define our sprite. Use dynamic sprite ID allocation for ease of use
SPSCALE SP,2,2  'Scale sprite up to 2X size
GAMEPREPAREPLSIMPLE SP,16*X[0]+8,16*Y[0]+8,1,&HFFFFFFFF
GAMESETWALKDATA SP,796,4,8

VAR CHESTS=8
WHILE TRUE
 VSYNC
 LOCATE 0,0
 PRINT FORMAT$("%D chests left   ",CHESTS) 'Tell player how many are left.
 CALL SPRITE 'Run all sprite attached functions. Our player is one
 'The main loop is the place to put win condition checking. Sprite callbacks can't halt this loop.
 IF CHESTS<=0 THEN
  ACLS
  BGMPLAY 5
  PRINT "Found all chests!" 'Hooray, you won!
  BREAK
 ENDIF
WEND

'The path clearing function. It HAS to be COMMON so my library can use it
COMMON DEF CLEARPATH X,Y
 BGFILL 1,X,Y,X+1,Y+1,0
END

'Player processing function
COMMON DEF PLPROCESS SP,BGX,BGY,X#,Y#,VX#,VY#,FACING,STEPPED
 'Player is standing on BGX/BGY. Together with FACING, we figure out which tile we're looking at.
 IF BUTTON(1) AND #A THEN 'They pressed the A button
  VAR IX,IY
  GAMEVECTOR FACING OUT IX,IY 'Convert FACING into unit offsets
  IX=BGX+IX
  IY=BGY+IY 'Now IX/IY hold the location the player is facing
  IF BGGET(1,IX,IY)==864 THEN 'Player clicked A on chest
   BEEP 3 'Might as well give feedback
   CHESTS=CHESTS-1
   BGPUT 1,IX,IY,0 'Remove chest
  ENDIF
 ENDIF
END
We moved the "win" stuff out into the main game loop so we can stop the loop if they win. I guess we could call STOP in the sprite callback, but I didn't want to. I also added a little counter at the top left so we always know how many chests are left. Run it and check it out: you have a working game (if you and I didn't make any mistakes). In the next tutorial, we're going to make the game a bit nicer and go over more library functionality.

nice, just gunna show support, good job on rbl, i havnt read through this yet but im sure i will find it useful.

Replying to:rando
nice, just gunna show support, good job on rbl, i havnt read through this yet but im sure i will find it useful.
Thanks; I hope if you make something out of this it works nicely.

Replying to:rando
nice, just gunna show support, good job on rbl, i havnt read through this yet but im sure i will find it useful.
your welcome!

Hey, btw, part 3 is out. You may want to update that in the top where it has the link to p2 and the coming soon on part 3.