This is part 2 of a 3 part tutorial on how to make a basic game with
Random's Big Dumb Library.
Other parts:
Part 1, Part 3 (coming soon)
This is a continuation of
How to use Random's Big Dumb Library: Game Engine. You should read over it if you haven't already. We're going to add to the final program on that page.
So we have a game; it does stuff. Let's make it better. We're going to change some parameters, give the player a follower, and use textboxes to display dialog when you find a chest.
More Game Parameters
The game engine has a lot of global parameters to change how things function. Remember how we changed the minimum collision tile with VAR("3:GAMECLSNSTART")=1? The same can be done for the following values:
GAMETILESIZE=16
GAMELAYERS=15
GAMECLSNLAYER=0
GAMECLSNSTART=1024
GAMECLSNMASK=&HFFFFFFFF
GAMEDEBUG=FALSE
GAMECPADDEADZONE#=0.15
GAMECPADMAX=0.85
GAMEPLWALKSPD#=1
GAMEPLRUNSPD#=2
GAMEPLRUNBTN=#B
GAMEPLDIAGONAL=TRUE
GAMEPLPROCESS$="GAMEPLDEFAULT"
GAMEPLCONTROL=TRUE
GAMEPLFOCUS=TRUE
We've already used some of these before: you just do the VAR thing. For instance, if you wanted to make the player run EVEN FASTER you could do:
VAR("GAMEPLRUNSPD#")=3
A follower
What's an old lady without a cat? We're going to setup a cat to run a simple follow AI with collision detection and walking animations. The cat will follow the old lady if she goes too far and will simply walk in a straight line towards her.
Generic NPC Callbacks
First, we'll need to setup the cat processing function. Unlike the player, who has a complex function automatically applied with GAMEPREPAREPLSIMPLE, all other entities must supply the functionality themselves. Don't worry though: the walking and collision functions are already programmed so we just have to call those from our function.
A game engine entity function takes no parameters. It is called once per frame. As with all other callbacks, it must be COMMON DEF.
Our cat function should update its position so it follows the player, then call the walking function so it does all the crap it's supposed to. We're going to use the DISTANCE library function to calculate the speed the cat will travel, then use the CHASECALC function to automatically determine a new position for the cat so it "chases" the player. Finally, we'll send the data off to GAMESPWALKER so the cat will perform collision detection and walking animations automatically.
Remember, we're just continuing off
this code, so think of this as an addition.
COMMON DEF CATPROCESS
VAR CX#,CY#,PLX#,PLY#
GAMEGETSPXY CALLIDX OUT CX#,CY#
GAMEGETSPXY SP OUT PLX#,PLY#
VAR DIST#=DISTANCE(CX#,CY#,PLX#,PLY#)
VAR SPD#=MIN(5,DIST#/20)
VAR CVX#,CVY#
IF DIST#>=16 THEN CHASECALC CX#,CY#,PLX#,PLY#,SPD# OUT CVX#,CVY#
VAR STEPPED,FACING
GAMETOSCREEN CALLIDX,1,CX#,CY#
GAMESPWALKER CALLIDX,CVX#,CVY#,FALSE OUT CX#,CY#,STEPPED,FACING
END
First, we get the positions of the player and the cat using the library function GAMEGETSPXY. Just pass it the sprite ID and it'll produce the world coordinates for that sprite.
Sprite callbacks set the global CALLIDX to the current sprite ID. Since this is the process for the cat, we can count on it being the cat's sprite ID. This system is builtin to SmileBASIC. We stored the player's ID in SP in previous code and use it here.
We then call the library function DISTANCE to figure out the distance between the cat and the player. The cat will walk faster as it gets farther from you: the SPD# variable is directly proportional to DIST#. The max is 5 though so it doesn't look like a cat rocket. Note: don't let NPCs go faster than 1 full tile (16 in our case), as the collision will stop working.
Then if we're sufficiently far from the cat (DIST#>=16) we run the CHASECALC library function to compute the velocities. You give it the chaser coordinates, the "destination" coordinates (the player), and a speed and the function spits out the velocities for each direction for that frame.
With all our information calculated, we make two final function calls: one to update the cat sprite's position relative to the world, and one to perform the animation and collision. GAMETOSCREEN updates the given sprite's screen coordinates based on the given layer and world coordinates. All entities use "world" coordinates so they are positioned relative to the BG, but to put them on the right place on screen, we have to convert the position to screen coordinates. The layer used for collision determines the world coordinates, so we pass 1 and the cat's coordinates so the sprite is in the right screen position BEFORE we calculate collision. This is VERY important: ALWAYS update the screen position BEFORE performing collision.
GAMESPWALKER runs walking animations and collision detection on the given sprite moving at the given velocity. The final parameter is whether or not to draw collision hitboxes. This is only useful for debugging, so we set it to FALSE. Just like the player callback is given a bunch of data, GAMESPWALKER also produces data we can use. For instance, we could do something per cat step by checking the STEPPED variable afterwards. We're all done though, so we don't use the data.
NPC Setup
Setting up an NPC is nearly the same as setting up the player. The only difference is that the preparation function takes a callback, unlike the player. There are no defaults for NPCs, which is why we had to write it ourselves.
VAR CSP=SPSET(856)
SPSCALE CSP,2,2
GAMEPREPARESP CSP,"CATPROCESS",16*X[0]+8,16*Y[0]+8,2,&HFFFFFFFF
GAMESETWALKDATA CSP,856,4,6
Follower Complete Code
We're not done yet though: we don't want the cat to be on top of the player. We can fix that with an SPOFS for the player and cat: the cat will then be forced underneath the player by using a lower Z value for the cat.
I put the cat function at the bottom with the other callbacks. The cat setup is underneath the player setup. Each one gets a Z offset with SPOFS (both are negative so they show up in front of the BG layer).
ACLS
EXEC "PRG3:RNDLIBFULL"
XSCREEN 2,100,2
VAR I
FOR I=0 TO 1
BGSCREEN I,100,100
BGSCALE I,2,2
NEXT
BGFILL 0,0,0,99,99,99
BGFILL 1,0,0,99,99,101
BGFILL 1,1,1,98,98,100
VAR SNAKEP$[0]
SNAKEP$=NEWSNAKEPATH$()
VAR X[0],Y[0]
FOR I=0 TO 15
PUSH X,10+RND(80)
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
BGPUT 1,X[I],Y[I],864
NEXT
GAMEPREPAREBG 3,1
VAR("3:GAMECLSNSTART")=1
VAR("3:GAMEPLPROCESS$")="PLPROCESS"
VAR SP=SPSET(796)
SPSCALE SP,2,2
SPOFS SP,0,0,-20
GAMEPREPAREPLSIMPLE SP,16*X[0]+8,16*Y[0]+8,1,&HFFFFFFFF
GAMESETWALKDATA SP,796,4,8
VAR CSP=SPSET(856)
SPSCALE CSP,2,2
SPOFS SP,0,0,-10
GAMEPREPARESP CSP,"CATPROCESS",16*X[0]+8,16*Y[0]+8,2,&HFFFFFFFF
GAMESETWALKDATA CSP,856,4,6
VAR CHESTS=8
WHILE TRUE
VSYNC
LOCATE 0,0
PRINT FORMAT$("%D chests left ",CHESTS)
CALL SPRITE
IF CHESTS<=0 THEN
ACLS
BGMPLAY 5
PRINT "Found all chests!"
BREAK
ENDIF
WEND
COMMON DEF CLEARPATH X,Y
BGFILL 1,X,Y,X+1,Y+1,0
END
COMMON DEF PLPROCESS SP,BGX,BGY,X#,Y#,VX#,VY#,FACING,STEPPED
IF BUTTON(1) AND #A THEN
VAR IX,IY
GAMEVECTOR FACING OUT IX,IY
IX=BGX+IX
IY=BGY+IY
IF BGGET(1,IX,IY)==864 THEN
BEEP 3
CHESTS=CHESTS-1
BGPUT 1,IX,IY,0
ENDIF
ENDIF
END
COMMON DEF CATPROCESS
VAR CX#,CY#,PLX#,PLY#
GAMEGETSPXY CALLIDX OUT CX#,CY#
GAMEGETSPXY SP OUT PLX#,PLY#
VAR DIST#=DISTANCE(CX#,CY#,PLX#,PLY#)
VAR SPD#=MIN(5,DIST#/20)
VAR CVX#,CVY#
IF DIST#>=16 THEN CHASECALC CX#,CY#,PLX#,PLY#,SPD# OUT CVX#,CVY#
VAR STEPPED,FACING
GAMETOSCREEN CALLIDX,1,CX#,CY#
GAMESPWALKER CALLIDX,CVX#,CVY#,FALSE OUT CX#,CY#,STEPPED,FACING
END
Hopefully if everything came out right, you now have a cat that follows you around! You also know how to setup NPCs and use various library functions to perform common NPC actions (like walking and collision and screen position updating).
Textboxes
My Big Dumb Library comes with a relatively extensive collection of textbox functions. They allow you to show and hide textboxes, display auto-wrapped text, ask for input from the keyboard, and can even display small menus (like the ones you see in RNDLIBTEST). Textbox visuals can be customized right down to an entirely custom drawing routine (for instance, if you want to draw on a sprite instead of on the GRP), and text within textboxes can be customized with a small markup language. You can set colors, sizes, text speed, and insert waits and linebreaks with this markup. Textboxes automatically break up lines and automatically insert scrolls when text is too long to fit into a single box, and even insert short pauses after punctuation to make it feel more like natural dialog.
Anyway, enough about all that. To use a textbox, you must first create a textbox parameter dictionary. This is a lot like the SNAKEPATH parameter dictionary: it's going to be a string array that contains the large amount of customization parameters. You can think of it like an "object" if you want: all the textbox functions will work on this "textbox object". You can reuse the same object as many times as you want.
Let's create a textbox object to tell the user how to play the game before it starts:
DIM TB$[0]
TB$=TBCREATE$()
GPRIO -30
TBSHOW TB$
TBTEXT "Find all the treasure chests! There are 8 in total!", TB$
TBHIDE TB$
Notice all the functions take the TB$ dictionary: they all need to know the parameters required to make the textbox "tick". Also notice the GPRIO -30: the textboxes are drawn purely on the GRP, but the GRP layer defaults to priority 0. Since our cat and player are -10 and -20, they'd display over the textbox, so we set GPRIO to -30.
The textbox functions are broken up like this so you have as much control as possible over textboxes. If you don't want to show/text/hide each time a textbox shows up, you can create a simple wrapper function. Something like this maybe:
DEF QUICKTB TEXT$,TB$
TBSHOW TB$
TBTEXT TEXT$,TB$
TBHIDE TB$
END
This will basically create a "popup" textbox, which may not be exactly what you want each time. Waiting for the show/hide animation each time kinda sucks; sometimes it's nice to leave the textbox on screen and continue to the next dialog without all the animations.
Anyway, using the same textbox, let's update the PLPROCESS function from before so that pressing L or R will popup a textbox telling you how many chests are left. We'll use that QUICKTB function we made up just above:
IF BUTTON(1) AND (#L OR #R) THEN
QUICKTB FORMAT$("%D chests left to find!",CHESTS),TB$
ENDIF
We COULD show a textbox every time you pick up a chest, but that's kind of annoying. Textboxes halt the gameplay, so the player wouldn't be able to move until they got rid of the textbox.
Textbox Complete Code
We're going to change a few things: we're not going to display the counter in the top left anymore. It's hard to read anyway, and the textboxes will tell us how many we have left. We're also going to change the first textbox shown to use our little "QUICKTB" function. Finally, we're going to display a special textbox at the end when you win. Here we go:
ACLS
EXEC "PRG3:RNDLIBFULL"
XSCREEN 2,100,2
VAR I
FOR I=0 TO 1
BGSCREEN I,100,100
BGSCALE I,2,2
NEXT
BGFILL 0,0,0,99,99,99
BGFILL 1,0,0,99,99,101
BGFILL 1,1,1,98,98,100
VAR SNAKEP$[0]
SNAKEP$=NEWSNAKEPATH$()
VAR X[0],Y[0]
FOR I=0 TO 15
PUSH X,10+RND(80)
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
BGPUT 1,X[I],Y[I],864
NEXT
GAMEPREPAREBG 3,1
VAR("3:GAMECLSNSTART")=1
VAR("3:GAMEPLPROCESS$")="PLPROCESS"
VAR SP=SPSET(796)
SPSCALE SP,2,2
SPOFS SP,0,0,-20
GAMEPREPAREPLSIMPLE SP,16*X[0]+8,16*Y[0]+8,1,&HFFFFFFFF
GAMESETWALKDATA SP,796,4,8
VAR CSP=SPSET(856)
SPSCALE CSP,2,2
SPOFS SP,0,0,-10
GAMEPREPARESP CSP,"CATPROCESS",16*X[0]+8,16*Y[0]+8,2,&HFFFFFFFF
GAMESETWALKDATA CSP,856,4,6
DIM TB$[0]
TB$=TBCREATE$()
GPRIO -30
CALL SPRITE
QUICKTB "Find all the treasure chests! There are 8 in total!", TB$
VAR CHESTS=8
WHILE TRUE
VSYNC
CALL SPRITE
IF CHESTS<=0 THEN
BGMPLAY 5
QUICKTB "You found them all!",TB$
ACLS
BREAK
ENDIF
WEND
COMMON DEF CLEARPATH X,Y
BGFILL 1,X,Y,X+1,Y+1,0
END
COMMON DEF PLPROCESS SP,BGX,BGY,X#,Y#,VX#,VY#,FACING,STEPPED
IF BUTTON(1) AND #A THEN
VAR IX,IY
GAMEVECTOR FACING OUT IX,IY
IX=BGX+IX
IY=BGY+IY
IF BGGET(1,IX,IY)==864 THEN
BEEP 3
CHESTS=CHESTS-1
BGPUT 1,IX,IY,0
ENDIF
ELSEIF BUTTON(1) AND (#L OR #R) THEN
QUICKTB FORMAT$("%D chests left to find!",CHESTS),TB$
ENDIF
END
COMMON DEF CATPROCESS
VAR CX#,CY#,PLX#,PLY#
GAMEGETSPXY CALLIDX OUT CX#,CY#
GAMEGETSPXY SP OUT PLX#,PLY#
VAR DIST#=DISTANCE(CX#,CY#,PLX#,PLY#)
VAR SPD#=MIN(5,DIST#/20)
VAR CVX#,CVY#
IF DIST#>=16 THEN CHASECALC CX#,CY#,PLX#,PLY#,SPD# OUT CVX#,CVY#
VAR STEPPED,FACING
GAMETOSCREEN CALLIDX,1,CX#,CY#
GAMESPWALKER CALLIDX,CVX#,CVY#,FALSE OUT CX#,CY#,STEPPED,FACING
END
DEF QUICKTB TEXT$,TB$
TBSHOW TB$
TBTEXT TEXT$,TB$
TBHIDE TB$
END
A
VERY important things to note: look at the place where we setup the textbox for the first time. Before we tell the player "Find 8 chests!", we do CALL SPRITE. This is because the game engine has never been run yet, so the sprites are not yet aligned to the BG and the BG probably isn't in the right place yet. Basically, the engine is "uninitialized"; usually this isn't a problem because the loop starts immediately and everything looks fine. However, now the textbox is inserting a pause before anything happens, and we can see the uninitialized state. If you're going to do anything BEFORE your game loop like this, it's important to either hide the game with something like VISIBLE or to run the game engine at least once so everything looks normal.
On a less critical note, I think that kind of feels like a real game now!
In the final tutorial, I'll go over textbox tweaks, more advanced terrain generation, and some final finishing touches.