Scripting Solutions for Games - Part I: Rolling your own interpreter (1)

Written by James S. J. Durtka (notthecheatr)

So you're about to write a game and you aren't sure what to do about scripting? Scripting is a nice element that is very useful in the more complex game engines to allow things to be easily changeable. Sure you can code everything into the engine, but then what if you want to add something to the game? External scripts, along with external maps, tilesets, etc., help to separate between the engine and the game, between the art (story, music, and graphics) and the part you program. More importantly, it makes your engine much more general - in theory, at least, the same engine could be used to make an entirely different game!

This is a series that will probably contain at least four parts, and possibly more depending on how much I decide to do. My goal is not to teach you everything you could possibly know about the subject, but to give you some ideas with which to work from - help you start thinking on your own so you can come up with the solution that's best for you.

The first and most obvious scripting solution for games is simply to write your own scripting interpreter. That's what this tutorial is about, and what the next one will also be about. Later we'll look at XML and Lua, a couple of very standard and useful languages. Quite possibly you'll use more than one solution in a single game, and anyways it's good to look at all the possible alternatives so you know which is best. Another thing I'd like to cover if I've got time is a practical look at using these things. The first four tutorials look at how to use various scripting languages, and when to use them. But that's all theory. If I get around to writing a fifth tutorial, I hope to cover a bit of practical use - perhaps writing a simple game that uses what we've covered so far.

Writing a simple interpreter is actually a pretty easy thing to do, thanks to FreeBASIC's nice handling of strings. Depending on your level of expertise you may be able to write something rather complicated, or not. You may just want to start with a very simple interpreter. Either way, I'll get into the basics here which will help you get started. After that, the future is yours!

So how do we write an interpreter? Well, first we have to figure out what kinds of commands we need. For an RPG, a lot of different commands might be good. You'll probably find that complex stories need a lot of commands to be executed well. Just how much you pack into your script interpreter is up to you. We'll start with a very simple PRINT command that does just what its name suggests. In an RPG scripting engine, the PRINT command might display character dialog inside a little message box. In our example, it just uses the FreeBASIC Print command to display the text.

How exactly would we implement all that? Well, you basically have to be able to look for certain strings - usually the keywords you're looking for, such as Print, Sleep, End, etc. For this we use various combinations of Left, Right, and Mid. These give you part of a string, allowing you to cut out the bits you don't need. When possible, if we only need a single character, we use string indexing with []. These treat the string as a byte pointer array, which means the first character of a string called myString would be myString[0]. In fact, it returns a number rather than a character, so you have to do conversions back and forth with Chr() and Asc(). There are also LTrim, RTrim, and Trim, which cut specific characters off the edges only. Then we have LCase and UCase. Normally we convert everything to lowercase so it's case-insensitive. Of course, if you want a case-sensitive language you can remove the uses of these, but it's pretty nice to be able to use any case you like without difficulty. Using all these string manipulation functions, we manage to parse through strings quite easily. Then we just do what is supposed to be done by the commands we find, as we find them.

Back to our interpreter - since it's to be used in a game, you can't be executing the script interpreter all the time. After all, you have to do graphics, input, AI, and all the rest. If you know how to do multithreading, you might do that. More likely you'll have a "next instruction" command for your interpreter. To make things simple, this will be a line-by-line interpreter: each instruction must be on its own line. That way, we can use the FreeBASIC Line Input command. You may have your own way to do this, but that's how I'm going to do it for this simple example. I'm going to do this with OOP because it's much simpler that way. The nice thing is that you can use more than one interpreter at the same time this way, and therefore run more than one script at a time.

Type interpreter
  Public:
    Declare Constructor (filename As String)
    Declare Destructor ()

    Declare Sub nextInstruction ()

    As uInteger ended
    As uInteger error

  Private:
    As String _filename
    As uInteger _filehandle
    As uInteger _line_number
End Type

This is the interface; the code comes later. As you see, (or not, if you don't understand OOP), if we want to run a script named "someScript.fbsc" then we would simply do this:

Dim As interpreter myInterpreter = interpreter("someScript.fbsc")

Do
  myInterpreter.nextInstruction()
Loop Until myInterpreter.ended <> 0

Sleep

End

Here's an example script:

someScript.fbsc:
'Display "Hello, World!" on the screen
Print "Hello, World!"

'Wait for a keypress
Sleep

'End the program
End

We want this to perform exactly as expected when executed by our interpreter. How do we do that?

Let's start with the constructor. That really just sets everything up in the object, including opening the file specified.

Constructor interpreter (filename As String)
  this._filename = filename
  this._filehandle = FreeFile()
  Open filename For Input As #this._filehandle
  this._line_number = 1
  this.ended = 0
  this.error = 0
End Constructor

We store a copy of the name of the script file internally, just in case we ever need it. Then we open it, storing the file handle (which is the next free file handle). We start at the first line number, and the script has neither ended nor had any errors so far.

Next the destructor. This basically undoes anything the Constructor has done, and anything that has happened in between then and now.

Destructor interpreter ()
  this.ended = Not 0
  Close this._filehandle
  this._filehandle = 0
End Destructor

Seems pretty simple. We mark ourselves ended so we don't try to execute any more instructions, close the file, and set the filehandle to null.

Now for the interpreter. As I said before, everything in the script will be pretty much the same as in FreeBASIC. Only End will be different - it will end the script, but not the main program. Print will simply Print the string, though, and Sleep will simply Sleep. Now because this is meant to introduce you to parsing, I do it in a simple and somewhat hackish way, doing everything step-by-step. Later we'll look at better ways to do things, but there's no point my giving you a solution unless you understand it.

Sub interpreter.nextInstruction()
Dim As String curLine, tmp
Dim As uInteger inQuote = 0

  If this.ended <> 0 Then Exit Sub

  If Eof(this._filehandle) Then
    this.ended = Not 0
    Exit Sub
  End If

  'Read the next line of input
  Line Input #this._filehandle, curLine

  'Trim leading and trailing whitespace off
  curLine = Trim(curLine)

  'There are no commands shorter than three characters, so quit now if we need to
  If Len(curLine) < 3 Then Exit Sub

  'If it's a comment, quit
  If curLine[0] = Asc("'") Then Exit Sub
  If LCase(Left(curLine, 3)) = "rem" Then Exit Sub

  'If the first five characters are "sleep" then sleep
  If LCase(Left(curLine, 5)) = "sleep" Then Sleep

  'If it's Print then search for the first quotation mark, then go until the second.
  If LCase(Left(curLine, 5)) = "print" Then
    'If there are no more characters afterwards, just do a linefeed
    If Len(curLine) <= 5 Then
      Print ""
    Else
      For i As uInteger = 5 To Len(curLine)-1
        'If it's a quotation mark, begin storing the characters beyond it - or stop, depending on where we are.
        If curLine[i] = Asc("""") Then
          inQuote = Not inQuote
          'If we were in a quotation before, quit the loop
          If inQuote = 0 Then Exit For
        Else
          If inQuote <> 0 Then
            tmp = tmp + Chr(curLine[i])
          End If
        End If

        'If we happen to hit the end of the line with no end quotation, raise an error.
        If i = Len(curLine)-1 And curLine[i] <> Asc("""") Then this.error = 1
      Next i

      'When it's all said and done, print the string
      Print tmp
    End If
  End If

  'If the command is to end, simply end the script.
  If LCase(Left(curLine, 3)) = "end" Then
    this.ended = Not 0
  End If
End Sub

If you don't understand it yet, keep trying. Particularly important to understand is the LCase(Left()) lines. For every possible command, we convert the first n characters of the line (after trimming whitespace off) to lowercase (that way it's a case-insensitive language, so you can write PRINT, Print, or print if you like) and check if it's that command). In this case, most things are pretty simple, though the Print command is a little bit complicated to implement. We basically search for the first quotation mark, and after that we keep track of each character. When we hit the second quotation mark, we stop. If we hit the end of the line before the second quotation mark, we note the error but don't do anything about it. After all, it's not a major error.

Here's everything we have so far:

interpreter1.bas:
Type interpreter
  Public:
    Declare Constructor (filename As String)
    Declare Destructor ()

    Declare Sub nextInstruction ()

    As uInteger ended
    As uInteger error

  Private:
    As String _filename
    As uInteger _filehandle
    As uInteger _line_number
End Type

Constructor interpreter (filename As String)
  this._filename = filename
  this._filehandle = FreeFile()
  Open filename For Input As #this._filehandle
  this._line_number = 1
  this.ended = 0
  this.error = 0
End Constructor

Destructor interpreter ()
  this.ended = Not 0
  Close this._filehandle
  this._filehandle = 0
End Destructor

Sub interpreter.nextInstruction()
Dim As String curLine, tmp
Dim As uInteger inQuote = 0

  If this.ended <> 0 Then Exit Sub

  If Eof(this._filehandle) Then
    this.ended = Not 0
    Exit Sub
  End If

  'Read the next line of input
  Line Input #this._filehandle, curLine

  'Trim leading and trailing whitespace off
  curLine = Trim(curLine)

  'There are no commands shorter than three characters, so quit now if we need to
  If Len(curLine) < 3 Then Exit Sub

  'If it's a comment, quit
  If curLine[0] = Asc("'") Then Exit Sub
  If LCase(Left(curLine, 3)) = "rem" Then Exit Sub

  'If the first five characters are "sleep" then sleep
  If LCase(Left(curLine, 5)) = "sleep" Then Sleep

  'If it's Print then search for the first quotation mark, then go until the second.
  If LCase(Left(curLine, 5)) = "print" Then
    'If there are no more characters afterwards, just do a linefeed
    If Len(curLine) <= 5 Then
      Print ""
    Else
      For i As uInteger = 5 To Len(curLine)-1
        'If it's a quotation mark, begin storing the characters beyond it - or stop, depending on where we are.
        If curLine[i] = Asc("""") Then
          inQuote = Not inQuote
          'If we were in a quotation before, quit the loop
          If inQuote = 0 Then Exit For
        Else
          If inQuote <> 0 Then
            tmp = tmp + Chr(curLine[i])
          End If
        End If
        'If we happen to hit the end of the line with no end quotation, raise an error.
        If i = Len(curLine)-1 And curLine[i] <> Asc("""") Then this.error = 1
      Next i

      Print tmp
    End If
  End If
  
  'If the command is Cls, Cls!
  If LCase(Left(curLine, 3)) = "cls" Then
    Cls
  End If

  If LCase(Left(curLine, 3)) = "end" Then
    this.ended = Not 0
  End If
End Sub

Dim As interpreter myInterpreter = interpreter("someScript.fbsc")

Do
  myInterpreter.nextInstruction()
Loop Until myInterpreter.ended <> 0

Print ""
Print "SCRIPT ENDED."

Sleep

End
If you compile this and put a script "someScript.fbsc" in the same directory with it, you'll be able to run the script. Nothing very useful can be written with this yet, but it's a start. If you were paying attention, you noticed that I added another command I didn't mention yet: Cls. Here's a demo of all the features so far:

someScript2.fbsc:
Print "Hi!  This is a script!"
Print "Press a key to clear the screen!"

Sleep

Cls

Print "Press a key to end this script!"

Sleep

End

As you can see, it's pretty easy to add simple commands, but as you add parameters to a command things become more complicated. To simplify this whole process, I created a nice object for line-by-line parsing. I call it the words_list, because that's essentially what it does - convert a string into a list of words. You can get it at http://www.freebasic.net/forum/viewtopic.php?t=10131. It won't work with anything multi-line (though with some basic modifications it probably could) but it's perfect for what we're doing here. No need to search for the quotation marks or whitespace between parameters; it does that for you! It splits strings up based on whitespace, but it handles strings correctly and ignores comments properly as it should. If you'd like, you can read through the source and try to understand how it works; it's very similar to what we do above, only a lot more complicated. Fortunately, you don't have to understand how it works to use it.

Here's how we do our nextInstruction() sub:

Sub interpreter.nextInstruction()
Dim As String curLine, tmp, thisWord

Dim As words_list wordsList = words_list()

  If this.ended <> 0 Then Exit Sub

  If Eof(this._filehandle) Then
    this.ended = Not 0
    Exit Sub
  End If

  'Read the next line of input
  Line Input #this._filehandle, curLine

  'Set the string in the wordslist to the inputted line
  wordsList.setString(curLine)

  'Get the first word, make it lowercase
  thisWord = LCase(wordsList.getWord(0))

  'Try different things depending on its length
  Select Case Len(thisWord)
    'If it's a 3-character word...
    Case 3:
      'Check if it's END or CLS
      Select Case thisWord
        Case "end":
          this.ended = Not 0
          MutexUnlock(this._info_mutex)
          Exit Sub
        Case "cls":
          Cls
      End Select

    'If it's a 5-character word...
    Case 5:
      'Check if it's SLEEP or PRINT
      Select Case thisWord
        Case "sleep":
          'Variable parameters for Sleep
          Select Case wordsList.numWords
            Case 1:
              Sleep
            Case 2:
              Sleep Val(wordsList.getWord(1))
            Case 3:
              Sleep Val(wordsList.getWord(1)), Val(wordsList.getWord(2))
          End Select

        'For Print, we just have to get the next word and trim the first quotation marks off the end.
        Case "print":
          tmp = Mid(wordsList.getWord(1), 2, wordsList.wordLength(1)-2)
          Print tmp
      End Select
  End Select
End Sub

You may be surprised to find that this is only two lines shorter than the original nextInstruction(). That's because of our extensive use of Select Case. However, you can also see how much simpler this is, and you can be sure that adding more instructions will take a lot fewer lines of code than it would have with the old method. Already we've added the two optional parameters to Sleep here. You can do the same with Print if you like.

someScript3.fbsc:
Print "10"
Sleep 500, 1
Cls
Print "9"
Sleep 500, 1
Cls
Print "8"
Sleep 500, 1
Cls
Print "7"
Sleep 500, 1
Cls
Print "6"
Sleep 500, 1
Cls
Print "5"
Sleep 500, 1
Cls
Print "4"
Sleep 500, 1
Cls
Print "3"
Sleep 500, 1
Cls
Print "2"
Sleep 500, 1
Cls
Print "1"
Sleep 500, 1
Cls
Print "0"
Sleep 500, 1

For all this, we still aren't really doing much of anything yet! What about variables?

As usual, I'm going to take the simple road. That is, only numerical variables will be allowed. In fact, all variables will be of the type Double. You're smart, you can figure out how to do other variables types if you need to.

We need some kind of object to handle variables. For every variable that is created using Dim, we need to store the name of the variable and of course the variable itself. Thus, for each variable there will be a String and a Double to deal with. If you wanted other variable types, you'd also need to store the variable type and instead of the double you'd store a Ptr to the variable, whatever it may be.

Type variable
  As String varName
  As Double varValue
End Type

Type variableSpace
  Public:
    Declare Constructor ()
    Declare Destructor ()

    Declare Sub addVariable (vname As String, initialVal As Double)
    Declare Function getVariable (vname As String) As Double
    Declare Sub setVariable (vname As String, newVal As Double)
  Private:
    As variable Ptr _variables
End Type

Of course, the first thing is simply to create the variable, using Dim. Since we're only using the Double type, there's no need to check for "As type" - that's implied. We'll use the good old fashioned QBASIC way. Thus, there are only two real complications - multiple variable declarations on a single line, and initial values. If you added types, things would be even more complicated (especially if you allowed a FreeBASIC-like open syntax, where the "As type" could come before or after variable names), since you'd have to handle all that in addition to multiple variables and initializers.

Dim a = 1, b = 2, c = 3, d = 4

Can we do all that? Sure, and it'll be a lot simpler than you think. The main things to remember:

  • We won't allow expressions to be initializers. That is, you can't initialize a variable to a combination of other variables and/or numbers. For example, you couldn't initialize a variable to 23+5*6. That would just initialize it to 23. We should be able to initialize to other variables, but no expressions. Sorry!
  • There has to be a space between everything. If not, words_list doesn't break things up separately and we have to do that ourselves and it becomes a lot more complicated. If you want you can modify words_list to do this properly - but for simplicity we'll just use spaces.

    So it's really not that hard. If the first word returned by words_list is "dim" then we look for the variable name. If the next word is an equal sign, then we take the next word and turn it into a number using Val, then assign it to the variable we create by calling varSpace.addVariable(). If either the variable another one coming so we loop through this way. If we ever expect something and don't get it, we'll flag an error, but we can probably ignore it. This being a simple interpreter, very few errors really matter in the grand scheme of things. We'll just warn and keep on going.

    variables.bas:
    Type variable
      As Double varValue
      As String varName
    End Type
    
    Type variableSpace
      Public:
        Declare Constructor (parentSpace As Any Ptr = 0)
        Declare Destructor ()
    
        Declare Sub addVariable (vname As String, initialVal As Double)
        Declare Function getVariable (vname As String) As Double
        Declare Sub setVariable (vname As String, newVal As Double)
        Declare Function accessVariable (vname As String) As Double Ptr
    
        Declare Sub destroyAllParents ()
    
      Private:
        As variable Ptr _variables
        As uInteger _num_variables
    
        As variableSpace Ptr _parent_space
    
        As Any Ptr _info_mutex
    End Type
    
    Constructor variableSpace (parentSpace As Any Ptr = 0)
      this._num_variables = 0
      this._variables = Allocate(1)
      this._parent_space = CPtr(variableSpace Ptr, parentSpace)
      this._info_mutex = MutexCreate()
    End Constructor
    
    Destructor variableSpace ()
      MutexLock(this._info_mutex)
      DeAllocate(this._variables)
      MutexDestroy(this._info_mutex)
    End Destructor
    
    Sub variableSpace.addVariable (vname As String, initialVal As Double)
      
      vname = LCase(vname)
      
      'Thread safety, as usual
      MutexLock(this._info_mutex)
    
      'Add a new variable
      this._num_variables += 1
      this._variables = ReAllocate(this._variables, (this._num_variables+1)*SizeOf(variable))
      this._variables[this._num_variables-1].varValue = initialVal
      this._variables[this._num_variables-1].varName = vname
    
      MutexUnlock(this._info_mutex)
    End Sub
    
    Function variableSpace.getVariable (vname As String) As Double
    Dim As uInteger found = 0
    
      If this._num_variables = 0 Then Return 0
    
      vname = LCase(vname)
    
      'Special variables - RND and TIMER
      If vname = "rnd" Then Return Rnd
      If vname = "timer" Then Return Timer
    
      'Thread safety, as usual
      MutexLock(this._info_mutex)
      
      'Default value
      Function = 0
    
      'Check each variable and see if it has the right value
      For i As uInteger = 0 To this._num_variables-1
        'If we find it, return its value
        If this._variables[i].varName = vname Then
          Function = this._variables[i].varValue
          found = Not 0
        End If
      Next i
    
      'If we didn't find it, check our parent space if we have one...
      If found = 0 Then
        If this._parent_space <> 0 Then
          Function = this._parent_space->getVariable(vname)
        End If
      End If
    
      MutexUnlock(this._info_mutex)
    End Function
    
    Sub variableSpace.setVariable (vname As String, newVal As Double)
    Dim As uInteger found = 0
    Dim As Double Ptr dblPtr
    
      If this._num_variables = 0 Then Exit Sub
    
      vname = LCase(vname)
    
      'If trying to set RND, set the seed
      If vname = "rnd" Then Randomize newVal
    
      'Thread safety, as usual
      MutexLock(this._info_mutex)
    
      'Check each variable and see if it has the right value
      For i As uInteger = 0 To this._num_variables-1
        'If we find it, return its value
        If this._variables[i].varName = vname Then
          dblPtr = @(this._variables[i].varValue)
          found = Not 0
        End If
      Next i
    
      'If we didn't find it, check our parent space if we have one...
      If found = 0 Then
        If this._parent_space <> 0 Then
          dblPtr = this._parent_space->accessVariable(vname)
        End If
      End If
    
      If dblPtr <> 0 Then *dblPtr = newVal
    
      MutexUnlock(this._info_mutex)
    End Sub
    
    Function variableSpace.accessVariable (vname As String) As Double Ptr
    Dim As uInteger found = 0
    
      If this._num_variables = 0 Then Return 0
    
      vname = LCase(vname)
    
      'Thread safety, as usual
      MutexLock(this._info_mutex)
    
      'Default value
      Function = 0
    
      'Check each variable and see if it has the right value
      For i As uInteger = 0 To this._num_variables-1
        'If we find it, return its value
        If this._variables[i].varName = vname Then
          Function = @(this._variables[i].varValue)
          found = Not 0
        End If
      Next i
    
      'If we didn't find it, check our parent space if we have one...
      If found = 0 Then
        If this._parent_space <> 0 Then
          Function = this._parent_space->accessVariable(vname)
        End If
      End If
    
      MutexUnlock(this._info_mutex)
    End Function
    
    Sub variableSpace.destroyAllParents()
      If this._parent_space <> 0 Then
        this._parent_space->destroyAllParents()
        Delete this._parent_space
      End If
    End Sub
    

    You may notice that we allow variable spaces to have parents. This allows for scope, so we can have local scopes. That way a local scope variable always overrides the global one with the same name. Obviously we don't have any scoping implemented yet, but we've made it possible for scoping to be implemented later. If a variable is not found in the local variable space, the next one up is checked.

    Another thing we've allowed is to access the variable wherever it is located - you can get its value, with getVariable, or you can get a pointer to it, with accessVariable. The reason for this is actually pretty obvious - your engine might need to access variables used by scripts. For example, a script might create a variable called "player.health" to store the health of the player - and of course the engine only knows what the players health is by accessing this variable directly from the variable space of the script.

    Then we have a simple method for telling a local space to destroy all its parents. The simple reason is that we only store a pointer to one local scope at a time. When the interpreter's destructor is called, it will have to destroy the current local scope as well as all the scopes above it all the way up to the global scope. If this is hard to understand, don't worry - it will become clearer.

    Notice that as usual we always convert variable names to lowercase - this ensures case-insensitivity. If you want case sensitivity (not standard for FreeBASIC or most other languages, though some interpreted languages such as Lua do it) just remove the line

      vname = LCase(vname)
    

    from the beginning of each sub. This will probably speed things up, but it makes things more complicated unless you always remember to use the same case.

    Finally, one small thing to note - notice that we have two "special" variables. These are TIMER and RND, which return the FreeBASIC functions of the name name. One thing worth noting is that if we try to set RND to something, we actually set the random seed. So once we implement variable setting, we could do

    Rnd = Timer
    

    to perform the equivalent of:

    Randomize Timer
    

    Now we have to make a couple of helper functions to determine whether a variable name is valid or not.

    Function isNumeric (testStr As String) As uInteger
      'Check if the first character is numeric or not
      For i As uInteger = 0 To Len(testStr)-1
        Select Case testStr[i]
          Case Asc("0"), Asc("1"), Asc("2"), Asc("3"), Asc("4"), Asc("5"), Asc("6"), Asc("7"), Asc("8"), Asc("9")
            Return Not 0
          Case Else:
            Return 0
        End Select
      Next i
      Return Not 0
    End Function
    
    Function isKeyword (testStr As String) As uInteger
      'Check if the variable name is a keyword
      If LCase(teststr) = "dim" Then Return Not 0
      If LCase(teststr) = "cls" Then Return Not 0
      If LCase(teststr) = "rnd" Then Return Not 0
      If LCase(teststr) = "end" Then Return Not 0
      If LCase(teststr) = "print" Then Return Not 0
      If LCase(teststr) = "sleep" Then Return Not 0
      If LCase(teststr) = "timer" Then Return Not 0
    
      Return 0
    End Function
    

    This should be pretty easy to understand. We'll just have to be sure to update the isKeyword function each time we add a new keyword.

    Now we've got some big changes to make in our interpreter.bas. First, we have to add a local and a global variable space to our interpreter object. The global space is the very top level of scope. Every scope can access variables in the global space so long as there are no local variables with the same name. So when accessing a variable, we check the local space, and it will automatically check everything all the way to the top. But the global space is special; it doesn't have a parent, and it never changes (whereas the local scope can change, at least once we add scope controls).

    #Include Once "words_list/words_list.bas"
    #Include Once "variables.bas"
    
    Enum errorType
      NONE = 0
      BAD_VAR_NAME = 1
      KEYWORD_VAR_NAME = 2
    End Enum
    
    Type interpreter
      Public:
        Declare Constructor (filename As String)
        Declare Destructor ()
    
        Declare Sub nextInstruction ()
    
        As uInteger ended
        As uInteger error
    
      Private:
        As String _filename
        As uInteger _filehandle
        As uInteger _line_number
        
        As variableSpace Ptr _global_var_space
        As variableSpace Ptr _local_var_space
    End Type
    
    Constructor interpreter (filename As String)
      this._filename = filename
      this._filehandle = FreeFile()
      Open filename For Input As #this._filehandle
      this._line_number = 1
      this.ended = 0
      this.error = NONE
      
      this._global_var_space = New variableSpace()
      this._local_var_space = this._global_var_space
    End Constructor
    
    Destructor interpreter ()
      this.ended = Not 0
      Close this._filehandle
      this._filehandle = 0
      
      this._local_var_space->destroyAllParents()
      Delete this._local_var_space
    End Destructor
    

    In the beginning, the local variable space is the global space. But if we enter a new scope, then a new local variable space is created, whose parent is the old local variable space. And when we exit the scope, that variable space is destroyed and its parent comes into scope. When the interpreter is destroyed, all scopes are destroyed at once using destroyAllParents().

    Now for our ever-growing interpreter.nextInstruction() sub. We just have to add the code for Dim (later we'll do some code for Scope...End Scope). This is quite easy. I won't even put the entire listing here, just the part that has changed. Figure out where this goes:

      'Try different things depending on its length
      Select Case Len(thisWord)
        'If it's a 3-character word...
        Case 3:
          'Check if it's END or CLS
          Select Case thisWord
            Case "end":
              this.ended = Not 0
              MutexUnlock(this._info_mutex)
              Exit Sub
            Case "cls":
              Cls
            
            Case "dim":
              'Get the first word after Dim
              tW = 1
              tWord = wordsList.getWord(1)
              
              'Continue so long as there is a comma to the right of a word
              Do
                'If the variable name is invalid, record the error but continue
                If IsNumeric(Left(tWord, 1)) Then this.error = BAD_VAR_NAME
                If IsKeyword(tWord) Then this.error = KEYWORD_VAR_NAME
    
                'If the variable already exists, record an error
                If this._local_var_space->accessVariable(tWord) = 0 Then this.error = DUPLICATED_DEF
    
                'If we find an equal sign next, get the value after the equal sign...
                If wordsList.getWord(tW+1) = "=" Then
                  tW += 2
                  'Get the next word
                  If IsNumeric(wordsList.getWord(tW)) Then
                    'If it's a number, convert it to numeric form
                    tVal = Val(wordsList.getWord(tW))
                  Else
                    'Otherwise assume it's a variable
                    tVal = this._local_var_space->getVariable(LCase(wordsList.getWord(tW)))
                  End If
                Else
                  tVal = 0
                End If
                
                'Create the variable with an initial value of the value specified or 0 if none is specified
                this._local_var_space->addVariable(tWord, 0)
                
                tW += 1
                
                'Get the next word
                tWord = wordsList.getWord(tW)
              Loop While ((Right(tWord, 1) <> ",") Or (wordsList.getWord(tW+1) = ",")) And tWord <> "" 
          End Select
    

    Now we can create a variable and assign it a value. But we can't do anything with the variable! Let's do the most obvious thing: printing.

            Case "print":
              'Start with the first word
              tW = 1
              tWord = wordsList.getWord(1)
              
              If tWord = "" Then Print ""
              
              'For each argument
              Do While tWord <> ""
                'If it ends with a ;, we trim it off and Print with ; so everything is on the same line
                If Right(tWord,1) = ";" Then
                  'If it's a string...
                  If Left(tWord, 1) = """" Then
                    'Truncate the leading and trailing " off plus the trailing ;
                    tmp = Mid(tWord, 2, Len(tWord)-3)
                  'Otherwise, assume it's a variable
                  Else
                    'Get the variable value, removing the trailing ;
                    tmp = Str(this._local_var_space->getVariable(Left(tWord,Len(tWord)-1)))
                  End If
                  Print tmp;
                'But if it doesn't end with ;, don't trim it off
                Else
                  'If it's a string...
                  If Left(tWord, 1) = """" Then
                    'Truncate the leading and trailing " off
                    tmp = Mid(tWord, 2, wordsList.wordLength(tW)-2)
                  'Otherwise, assume it's a variable
                  Else
                    tmp = Str(this._local_var_space->getVariable(tWord))
                  End If
                  Print tmp
                  
                  'Since it's the last parameter to print, we exit the loop
                  Exit Do
                End If
                
                'Next word
                tW += 1
                tWord = wordsList.getWord(tW)
                
                'Forever, until there are no parameters left.
              Loop
    
    

    Whew, how's that work?! Well, take it a thing at a time. Like the Dim, there's a loop involved here. Basically the loop only ends when there are no parameters left - either no more ; at the end or else an empty word. During this loop, we go through each parameter to Print. If the parameter ends with ;, we know more are following, so we remove the ; and print with ; so everything stays on the same line. Otherwise, we print without ; and then exit sub. If the parameter starts with a " then we assume it's a string and remove the leading and trailing "; otherwise, we assume it's a variable and remove nothing, using the entire string to get the locallest variable with that name (0 if the variable doesn't actually exist).

    It seems complicated, but if you think about it hard enough you'll get it.

    Now we'll add this sub to the interpreter object:

    Sub interpreter.accessGlobal (vname As String) As Double Ptr
      Return this._global_var_space->accessVariable(vname)
    End Sub
    

    Which allows anyone to access global variables.

    Now before we quit today, we'll add Input. That should be pretty easy, of course. Input is actually nearly identical to Print, since we print anything in quotation marks. However, variables are inputted instead of printed.

            Case "input":
              'Start with the first word
              tW = 1
              tWord = wordsList.getWord(1)
              
              'For each argument
              Do While tWord <> ""
                'If it ends with a ;, we trim it off and Print with ; so everything is on the same line
                If Right(tWord,1) = ";" Then
                  'If it's a string...
                  If Left(tWord, 1) = """" Then
                    'Truncate the leading and trailing " off plus the trailing ;
                    tmp = Mid(tWord, 2, Len(tWord)-3)
                    Print tmp;
                  'Otherwise, assume it's a variable
                  Else
                    'Get the variable value, removing the trailing ;
                    Input tVal
                    this._local_var_space->setVariable(Left(tWord,Len(tWord)-1), tVal)
                  End If
                'But if it doesn't end with ;, don't trim it off
                Else
                  'If it's a string...
                  If Left(tWord, 1) = """" Then
                    'Truncate the leading and trailing " off
                    tmp = Mid(tWord, 2, wordsList.wordLength(tW)-2)
                    Print tmp
                  'Otherwise, assume it's a variable
                  Else
                    Input tVal
                    this._local_var_space->setVariable(tWord, tVal)
                  End If
                  
                  'Since it's the last parameter to print, we exit the loop
                  Exit Do
                End If
                
                'Next word
                tW += 1
                tWord = wordsList.getWord(tW)
                
                'Forever, until there are no parameters left.
              Loop
    

    Comparing it to the code for Print, you'll see it's very similar but there are of course those few changes mentioned. And now we can do this:

    someScript5.fbsc:
    Dim someVar
    
    Input "Enter a number ";  someVar
    
    Print "You entered:  ";  someVar
    

    We've come a long way, but we've still got a ways to go. In particular, we need to be able to implement expressions so we can manipulate variables properly and so we can implement conditionals and looping. All that will be covered in the Part II of Scripting Solutions for Games. Once we finish this, we'll move on to using XML and Lua. Finally, we'll bring it all together and build a mini-game with the stuff we've learned.

    I hope you've enjoyed this. As usual, any comments, errata, questions, etc. should be sent to TheMysteriousStrangerFromMars@yahoo.com or addressed to me (notthecheatr) on the forums.

    Downloads:
    This tutorial by itself
    This tutorial and the example files included with this tutorial