.   Hardcore Programming Section
BUILD Style Scripting

BUILD information
Duke Nukem 3d ScreenshotThe Build Engine was written by Ken Silverman in a galaxy far, far away. It also happened to be originally written in QB. But anyone who made build maps knows it for it's scripting. This isn't actual bonified Build style scripting, because few engines made in QB use sectors. However, BUILD fans will remember that all those impressive scripts were created by a mere invisible sprite with a hitag and lowtag, sitting and waiting to be triggered. This is how we're doing things in this article. Being able to bind scripts to objects makes all the work  way easier for the programmer and the map creator, because it's a simple mechanism to implement for the programmer, and a simple concept to understand for a non-programmer doing map work.
There aren't a lot of tutorials out there about scripting, and most of those just show the frame of mind, rather than actual commands, when describing the process. I'm going to try to be different. I'm going to explain how to make a scripting engine, and how you can invoke scripts in your game BUILD-Engine style, storing the locations and type of invokation using the NPC routines in your game. For the actual scripting system, I'll give code examples, but some of it will be in pseudocode.

Section 1: Commands from a file

For the longest time, I contemplated how to easily insert events into my game engine (which was fairly mature at the time, so a complete rewrite was out of the question). I didn't want to hard code them, since that would severely drive up the size of my source and executable, and make my code unbearable to work with, so I started brainstorming. My first idea was a seperate BASIC file which would be included and called upon whenever there was scripting to be done. This idea was thrown out, since the extra work wouldn't make the game any easier to work on. Then, I started thinking of a scripting engine. My first idea was to have everything in the file holding the scripts, be it locations, or other data -- all scripts would be stationary. This was a bad idea, I had decided, since I wanted moving NPCs, but I wasn't going to be able to bind scripts to NPCs, and this approach would mean a lot of hard drive activity, since the file would constantly be opened and checked for scripts every time the Player moved or pressed space. Heres my final solution:

-All scripts are in one file
-Every script has a header and a footer
-The header is numbered to allow for fast seeking
-If a script is to be run, an extra variable for each object on the screen is checked, and then the file is checked for that script number
-Extra sprites were to be created, both would be invisible, one to signify when a player walked over a location to run the script, and the other to allow a script to be run when a player hits without a visible object.

The code to read raw commands was easy. Heres an example that could easily be converted to use in a program.



sub script(scriptno%)


'This is here because any given SLEEP sucks, and if you press space to enter the script,
'the space is still in the buffer, so this gives you a chance to move your hand off the key.
FOR a = 1 TO 100
clearthebuffer$ = INKEY$
NEXT a

fileno = FREEFILE

FILE$ = "script.scr"

OPEN FILE$ FOR INPUT AS #fileno

'Finds the record first.
'searches for "entry 1" or whatever.
WHILE com$ <> "entry" + STR$(scriptno%)
INPUT #1, com$
IF EOF(fileno) THEN CLOSE : END 'If it can't find the script, it exits.
WEND

WHILE com$ <> "end" 'This is where it searches for the end of the entry.
INPUT #fileno, com$ 'a check for "entry" and "EOF" will work too.
'I check the first 5 letters of any command.
SELECT CASE LCASE$(LTRIM$(RTRIM$(LEFT$(com$, 5)))) 'Five letters of every command are read. The rest are just for
CASE "sleep" 'just sleep. 'user ease. The LTRIM and RTRIM let the scripting person use
SLEEP 'tabs in the file to follow the same spacing rules programmers use.
CASE "line1" 'line1 text string
line1$ = MID$(com$, 6)
CASE "line2" 'line2 text string
line2$ = MID$(com$, 6)
CASE "line3" 'line3 text string
line3$ = MID$(com$, 6)
CASE "line4" 'line4 text string
line4$ = MID$(com$, 6)
CASE "dialo" 'dialogbox 0 or 1
'Will fill the top of the screen with color X.
lin% = VAL(MID$(com$, 10))
line (0, 0)-(319, 90), lin%, BF


CASE "showt" 'showtext 0 or 1
IF VAL(RIGHT$(com$, 1)) = 1 THEN
line (0, 0)-(319, 90), 1, BF
locate 1,1
print line1$
Locate 2,1
print line2$
Locate 3,1

print line3$
Locate 4,1
print line4$
ELSE
line (0, 0)-(319, 90), 0, BF
END IF
CASE "pause"
otimer = TIMER
lin = VAL(MID$(com$, 6))
WHILE TIMER < otimer + lin:WEND
QBXL Extra
Picture of an RPG gameWith this information, a state-of-the-art tile engine, some nice art, dialog that doesn't make your audience want to wretch, a story which involves more than a dragon with a perchant for coming back every 1000 years to get it's ass whomped, you too can make an RPG for the ages! Look at the screenshot above. I have no idea what game that came from. I was just looking for a screenshot from an RPG. There you are.

As always, remember that Ass is Ass. It's ass in QB, it's ass in C, it's ass in Pascal(though in C, it's probably crashy ass, where in QB it's probably just ass). It takes balls of steel to stay away from that which could be called "ass",  Remember that through hard work, tons of beta testing, and the balls to admit that something you wanted to do sucks ass, you too can make a game which isn't ass.

Some anonymous chickNow for a random picture of some anonymous chick. Who is she? I have no idea. It certainly breaks up the monotony of this long article though, doesn't it? Lets all give it up for google and anonymous chicks!!!(yay!)

END SELECT
WEND

CLOSE

END SUB


I'll dissect the routine for you:

-Opens file and searches for string
-Checks first five letters of every line for a command.
-if the letters spell sleep, if it reads that, it just runs the command.
-for line1,2,3,4, it changes the string variable to that exists after the first 5 lines.
-Dialogbox will draw that infamous blue box at the top of the screen. Be afraid. Be very afraid.
-Showtext writes the text in line1,2,3,4 if the param is 1, otherwise it redraws the blue box.
-Pause will just wait for X seconds.

An entry in Script1.SCR would look something like this:

entry 1

line1 This is a test
line2 of the emergency broadcast system.
line3 if this was more than a test
line4 You'd be in a bunker, not reading this tutorial.

dialogbox 1
showtext 1

end

Section II

This section should be shorter, since the concept is so simple.

All you need to do here, is check the variables for all the objects you have on-screen
and if you have an invisible sprite for placing both types of script (one for activated by space, and another for scripts objects activated by walking over it). All objects can have scripts bound the them if they are activated by pressing space, but the clear one does come in handy.

I'm only going to cover the easiest way to do it, using a small piece of pseudocode/code.


'this is just a description of the structure. I can't remember how to do this in QB since most of the structs I've been doing lately have been in C(my latest engine in QB uses a 2d array to simulate a struct, and there are too many refrences to it to change that now...), so I may not get this right...
type objects

X as integer
Y as integer
sprite as integer
scriptbinding as integer

end type

dim objs(1 to 100) as objects
dim myX%
dim myY%

'just a regular script check when we hit spacebar.
'since this is just a generic routine, direction checking isn't done.
'Therefore, you must be standing directly over the sprite to activate it.
if inkey$ = " " then'just spacebar checking.
for a = 1 to 100 'This would be in the player movement routine.
if objs(a).sprite > 0 OR objs(a).sprite = -2 then 'Checks for the transparant
if objs(a).X = myX% and objs(a).Y = myY% then 'object placement sprite
if objs(a).scriptbinding <> 0 then
script(objs(a).scriptbinding)
end if

end if
end if

next a
end if
'It checks through the array to see if you are on any of them.

'Checks for scripts using that sprite we defined as 'walkoverable' and runs the correct script
for a = 1 to 100
if objs(a).sprite = -1 then 'This number is just any number we chose to represent walk
'detection sprites. You touch em, they go off.
if objs(a).X = myX% and objs(a).Y = myY% then
if objs(a).scriptbinding <> 0 then
script(objs(a).scriptbinding)
end if
end if
end if
next a
'Same here. It just checks the entire array for a match.

This code should be almost self-explainitory. Most of it is just the collision detection any game engine should already have. Since the script bindings are held in the objects memory structures, it's a simple matter to work with them. The only difference between the two algorithms above is why the scripts are invoked (one is invoked because the user presses space, the other is invoked when the user walks over an object set to the -1 sprite).

That's it for this tutorial. For my next game engine, I'm going to try to implement storage, decisions, mathematics, and loops in the scripting engine. If I achieve those goals. I'll write a tutorial on it. It would be easy to use named scripts (instead of script X%, it would be script text$, making it easier to work with the large file in huge projects). To implement that with the code I provided would be rather easy, just change the data types in the struct and on the script sub for the script, and remove the STR$() in the script sub, but keep in mind that working with strings is generally slower than working with integers.

Later.

Update: It turns out that there is one restriction on this code -- if you call the scripting routines using certain variables, you cannot change those variables using their names. EG. if you call a sub foo(x), and you call it foo bar, you won't be able to make any changes to foo, you'll have to change x. reply if that doesn't make sense, but I believe the feature is due to QBs recursion support...


-SJ Zero doesn't recycle his old articles often, but with chicks, explosions and other cool shit, this time it's ok.




You can write to QB Accelerator at