Simple Game Scripting Part 1: Storing/Reading Your Scripts
This article and all it's contents are (C) 2000 David Goodlad. They may not be reproduced in any form without permission from the author.
I will very gladly give permission to copy the article, though, so send me an email!
Over the past few months, many people have been asking on message boards for help with scripting in their games (especially RPG's). This topic is an extremely important part of programming your game, yet it seems to be one of the least-documented aspects out there!
This series of articles will hopefully get you to the point where you can easily use custom scripting commands in your game to control any possible event you wish to occur.
A reminder, though, is that this is not necessarily the best, nor the fastest, possible way of adding scripting functionality. The code has been written in such a way that the concept should be easily understood so that you can design your own implementation which best suits your particular game. Also, the code requires Visual Basic 6; VB5 does not have a couple of the required functions such as Split() and CallByName().
The basic idea behind a custom scripting 'language' for your game is so that you do not have to hard-code every single possible event and conversation into your game. It also avoids the need to recompile the executable for the game each time you wish to slightly alter the storyline or for example the effects of drinking Potion X on a character with a 'Good' alignment.
For this first article in the series I will be explaining the general structure of your scripts, and how to store your scripts on disk, then read from them later.
The method to read/write discussed in this article will leave your scripts easily editable by the end-user of your game. This is usually not desirable, so I suggest changing these later to implement some sort of custom resource-file setup that you should use for your graphics and sounds as well.
[size="5"][b]Script File Syntax/Format[/b][/size]
Your scripts will be plain text, formatted with a very simple syntax:
[indent][code]CommandName (Parameter1,[Parameter2],[...])[/code][/indent] Each line will have this simple format. There is an exception, though. You will want to be able to define blocks of these commands (I will go into why later). So, to define such a block:
[indent][code]*Block* BlockType BlockName
*EndBlock* BlockName[/code][/indent] For example, if there is a block type called 'Event':
[indent][code]*Block* Event Foo
*EndBlock* Foo[/code][/indent] All commands must be contained in 'Blocks', even if there is only one of them. The reasoning for this will become clear later; for now just accept my words as the truth
This is the simple structure of your scripts. It may not seem very useful/extensible right now, but later on you will understand. You will just have to trust me about this, just as with the rule about commands having to be within blocks.
[size="5"][b]Loading Your Scripts[/b][/size]
There is no real need to discuss the writing of your script files from VB, as you can simply use notepad or another plain-text editor to do this.
But, loading your script files into your game for use is another story. There are numerous questions which can be asked about how to do this, such as how the lines of 'script' can be parsed into separate pieces, and how to store the scripts while in memory.
[size="5"][b]User-Defined Types - Gotta Have 'Em![/b][/size]
The first thing to be done is define a user-defined type for a script command line. I suggest placing this into a class module, probably called something such as CScriptParser in order to allow for further expansion in later parts of this series of articles.
[indent][code]Private Type tCmdLine
Command As String
Parameters() As String
[/indent] This should be placed in the declarations section of your class module. Another useful UDT that should be created is one for an event block:
[indent][code]Private Type tCmdBlock
BlockType As String
BlockName As String
Commands() As tCmdLine
To prepare for storing your scripts, you'll need a variable array (put it into your declarations section of the class module you created earlier as well):
[indent][code]Dim CommandBlocks(1023) As tCmdBlock, iLastBlock As Integer[/code][/indent] This creates a limit of 1024 possible blocks loaded simultaneously. You can always increase this if you need to. The iLastBlock variable is simply to keep track of the last element in the array which is occupied. You should initialize this variable to -1 when an instance of the class is created, so in the Class_Initialize method of your class, put:
[indent][code]iLastBlock = -1[/code][/indent]
[size="5"][b]Actually Loading the Script File[/b][/size]
Now that we've got all of our storage structures created, we can actually load our script file. Put this function into your class module:
[indent][code]Public Function LoadScriptFile(sFileName As String) As Integer
'If the file is empty/doesn't exist, don't open it
'and exit the function with a return value of 1.
If FileLen(App.Path & "\" & sFileName) = 0 Then
LoadScriptFile = 1
'sTemp: a temporary string variable - stores the input from the file
'bEndLoop + bEndLoop2: booleans for controlling when to stop reading
'aTemp(): array of strings for splitting up parameters.
Dim sTemp As String, bEndLoop As Boolean, bEndLoop2 As Boolean
Dim aTemp() As String
'Initialize the boolean control variables
bEndLoop = False
bEndLoop2 = False
'Load up the specified script file
Open App.Path & "\" & sFileName For Input As #1
'Input a line from the file into sTemp
Line Input #1, sTemp
'This is to check whether this is the end of the file or not...
'Checks against everything before the first occurance of a 'space'
Select Case Left$(sTemp, InStr(sTemp, " ") - 1)
'If we're at the end of the file set the flag to end the reading loop
bEndLoop = True
'Otherwise we're at the start of a block...
iLastBlock = iLastBlock + 1
'Remove the "*Block* " at the beginning of the string
sTemp = Right$(sTemp, Len(sTemp) - Len("*Block*") - 1)
'Put everything before the first 'space' character into the BlockType
CommandBlocks(iLastBlock).BlockType = Left$(sTemp, InStr(sTemp, " ") - 1)
'Put everything after the first 'space' character into the BlockName
CommandBlocks(iLastBlock).BlockName = Right$(sTemp, Len(sTemp) - InStr(sTemp, " "))
'Initialize the Commands() array
'Read a command line
Line Input #1, sTemp
'If we're at the end of the block...
If sTemp = "*EndBlock* " & .BlockName Then
'Get rid of the empty element of the Commands() array
ReDim Preserve .Commands(UBound(.Commands()) - 1)
'Set the boolean control variable to exit the inner loop
bEndLoop2 = True
'Read in the command name as everything before the left bracket
.Commands(UBound(.Commands())).Command = Left$(sTemp, InStr(sTemp, " (") - 1)
'Split up everything between the left and right brackets using commas
aTemp() = Split(Mid(sTemp, InStr(sTemp, "(") + 1, Len(sTemp) - InStr(sTemp, "(") - 1), ",")
'Increase the Parameters() array's size to how many parameters there are
'Copy the parameters from the 'splitted' array
.Commands(UBound(.Commands())).Parameters() = aTemp()
'Add an empty element to the Commands() array
ReDim Preserve .Commands(UBound(.Commands()) + 1)
Loop Until bEndLoop2
'Reset the control variable
bEndLoop2 = False
'This should *never* happen because all commands
'should be contained within blocks.
LoadScriptFile = 1
Loop Until bEndLoop
'Return a value of 0
LoadScriptFile = 0
Hopefully the comments in the code for the LoadScriptFile function are sufficient to explain what the more confusing lines do. The use of string manipulation functions such as InStr, Left$, and Right$ can become very confusing if you have not used them before, or don't know how they work. Therefore, if you wish to truly understand how the string parsing works, I suggest you read up on these functions, and other related ones including Mid and Split.
[size="5"][b]Until next time...[/b][/size]
By now, you should understand how to load and store your script files on disk and in memory. This is the most important piece, and can be the most complicated. The actual loading and splitting up of the individual lines is integral to the use of your scripting engine, and thus you have to make absolutely sure that it is foolproof (or as close to this as possible!). I suggest adding error handlers, and using the return value of LoadScriptFile to check whether the file was loaded correctly when you call it.
In the next article in this series, the actual use of your scripting will be discussed. The third part of this series will then continue along that theme and go into much more depth with the implementation, including a section on interfacing the scripting engine with a tile-based system in order to create events.
Though simple, if you would like the source code to this part of the series, the class module is available at:
[url="http://blackhole.thenexus.bc.ca/articles/CScriptParser.cls"][ [/url][url="http://blackhole.thenexus.bc.ca/articles/CScriptParser.cls"]http://blackhole.the...criptParser.cls[/url] ]
[size="5"][b]Contact + Future Releases[/b][/size]
Updated versions of this article, as well as the later parts of the series as they are written, will be available from my website, the black hole.
[url="http://blackhole.thenexus.bc.ca/"][ [/url][url="http://blackhole.thenexus.bc.ca/"]http://blackhole.thenexus.bc.ca/[/url] ]
I can be reached for comments or questions at
[email="firstname.lastname@example.org"][ email@example.com ][/email]
I sincerely hope you've gained from this article, and look forward to the next part of the series!