Simple Game Scripting Part 2: Making Your Scripts DO Something
This is the second part in this series of articles on one of the most important parts of a game (especially an rpg): scripting. In the first part, I discussed the general concepts of scripting, and how to store/read/write your script files.
In this second part the actual implementation of the scripts will start to be discussed in some detail, paving the way for the third part where I will go much deeper into some commands that you should have in your engine no matter what type of game (ie: decision-making, loops, etc).
After much thought, two obvious options for the implementation of the scripts occurred to me. The first one is probably the simplest: use a giant Select...Case statement to choose which functions to call, etc. The problem with this method, though, is that it rapidly becomes very confusing, and much slower the more commands that you have.
The second option that I came up with uses a new method introduced in VB6: CallByName. This function will allow you to call any sub/function/property of an object by passing it's name as a string. If you think about this in the context of scripting, you will realize that we already have each command that needs to be executed for our script in memory as a string. This provides an extremely easy-to-use method to split up our commands logically, and make it *very* flexible.
How Do We Use This?
Now that we have a method that we can use to flexibly call any possible command that is named in a string, we should create a single function that will handle any command, and from there call the associated function. In your CScriptParser class module, put this function:
That ExecuteCommand function can be called whenever you want to parse ANY of your scripting commands. It is a 'generic' function that will 'wrap' around each of the other functions for the individual scripting commands.
Speaking of the individual scripting commands' functions, how do we make them? Here is a very simple example :
Basically, you create a function for each of your possible scripting commands, named the same as the command but prefixed with 'ExecuteCmd'. In the above example, the command that would call this function would be 'Foo'. By returning a value of 1, the call to ExecuteCommand would also return a value of 1. The return value gets passed down the chain, you can return values from your 'wrapped' command functions.
The only 'sad' part about using CallByName is that the functions *must* be Public, not Private. But, having those command functions public can actually be a good thing; You can use them from within your code as well. For example, say you had a tile-based game with multiple maps, and you knew that every single map would be linked in a uniform fashion to other maps on the edge tiles. You could have a scripting command 'TeleportToMap' that could not only be used in your scripts for events such as walking onto a door tile to bring you inside a building, but you could have your player movement code check all the time if the player moves off the map, and call the ExecuteCmdTeleportToMap function without the 'wrapper' ExecuteCommand function to move the player to the adjacent map. Therefore, it's not so bad to have the functions public anyways.
What About Command Blocks?
In the first part of this series, I made it clear that all 'command's must be contained in blocks. But, I never explained why. I can now begin to discuss the reasoning. In your game, you need some way of determining WHICH command(s) to execute WHEN. No commands will need to simply be called arbitrarily, without something that causes it to be called: an event. Whether this event is having the player move onto a specific tile, or when the player responds a certain way to an NPC's statement or question, it all requires something to happen, usually caused by the player's actions directly. So, you need some way of associating an event with a command (or commands).
Here is where the naming of command blocks, and having different types of blocks, comes in. A command block is almost always associated with a specific event by it's name and type. You could, for example, have a type of command block called "PlayerMoved", and for each tile/coordinate that you need a scripting command executed, write a block named by, say, the coordinates like so:
When the player moves, your game would check whether there exists a command block of type PlayerMoved named with the coordinates (in the example's case, 5,14), and call this block.
For speed and simplicity's sake, you may want to have your actual command block variable array split up into groups of special types. This would allow your game engine to locate blocks such as those in the above example much faster, without having to go through all the other different command block types mixed in there as well. There are all sorts of possible optimizations that you could add onto this general scripting structure; all it takes is some thought!
A Few Suggestions For Custom Improvements
I have kept the code and structure of this scripting engine as simple as possible so as to be able to explain the concepts much more easily. But, you will definately want to make improvements and alterations to it customized for your own game. I have a few suggestions for things that should be done for any type of game:
Firstly, split up the CommandBlocks array variable into multiple arrays, one for each 'BlockType' or associated BlockTypes. This will make a couple of the functions a little longer, but it will generate a huge speed increase because they will no longer have to search through each and every one of the commandblocks to find the one you are trying to use. Another way to avoid searching through a huge array is to use VB Collections, or Dictionaries. This way you could use the BlockName as the key for each entry.
Secondly, add error-checking code. You never know what might happen!
Third and finally, instead of using plain text, do as I mentioned in the first part of this series: integrate your script files into a custom resource file along with all of your graphics/sounds etc. This should really be done with everything in your game that you do not want people to change.
Conclusion, and What's Coming!
I sure hope that with this second part out of the way, the ideas behind the scripting engine are becoming much clearer. By actually seeing how the script files are used, you can understand much easier why things are done a certain way. But, you must remember that none of this is set in stone; feel free to play around with adding things to the engine that you feel might be useful, such as lists of items in the game and associated events for interacting with them in different ways.
As stated in the introduction, the third part of this series should discuss the actual commands that should be in the scripting engine no matter what type of game they will be used for, such as decision-making structures and loops. This will get into setting/getting flags using scripting, and other such things.
Hopefully you are well on your way, now, to creating a complete, flexible scripting engine for your game!
As before, if you would like the source code to this part in the series, the class module is available at:
Contact + Future Releases
As before, updated versions of this second part of the series, as well as later parts as they are written, will be available from my website, the black hole:
I welcome any questions or comments, as well as *constructive* criticism :) I can be reached via email at:
Thanks for reading!