Script in games
As most of you probably know already games widely uses scripts to add different kind of things to their game, be it a quest, or a pattern of movement, and so on. Me and Bubba have worked out a system that is designed to be used together with the Zelda-game Bubba has talked alot about. But in order to share our ideas with you people i decided to write this little post to maybe inspire somebody to try something like this on your own :).
The focus on this type of script is for it to run as fast as possible, with as little parsing as possible at runtime. In order to do this you must basicly convert a text-file (or binary file) into Structures of some sort. We also wanted the script to behave as a state-machine. This means that you test for 1 state, and if that state is true, you do something. This can interact nicely with the message-system we have developed (might post something on this later).
The syntax is very important when you write a script and can be everything from an assembly-like syntax to a syntax that looks very similar to C++. I chose a syntax that would be easy to parse, but at the same time is somewhat familiar to the people who have programmed in C, C++ and the likes. Basicly everything is a function-call. There is no such thing like this:
variable = 10
instead it goes like this in my script system:
Notice the a infront of the actual functionname. This a shortence for 'action', which is well, something will be done here. I chose to have an a infront because it allows for easy parsing and detection of a functioncall. Consider now that some functions may start with an 'u', this denotes user-defined functions. User defined functions are just functions that group up several normal a-calls to 1 u-call. This is for easier development.
The script and the whole Zelda-engine is about messages (think windows-messages), with a fling of MFC-styles in it. Basicly we have mapped a message-id to a single function inside a class. This script sends these messages to either a Message-pump or its own message-handler, depending on what type of messages it is. The Message-pump then sends the message to its correct destination. The scripts internal message-handler (think Win32 MsgProc) deals with setting variables, and things like that.
As said above the functions is what makes this script. It is all about functions and if-calls. But problem came up here, we needed a way to externally map each function to a message-id. We also needed to know wether the arguments to a function was a normal plain number or a variable. The solution to this was a definition-file. A definition-file contains the names of the functions together with what type of arguments it expects. This definiton-file is very easy to expand and is basicly what makes this script so versatile.
Perhaps the single hardest thing to get right. Our system only handles DWORD or unsigned int types, and that is partly because if one wants float, string unsigned int, signed int and so on, it would partially be very hard to implement a system that handles that without using alot of runtime checking. And runtime checking is bad because that takes time, which is better spent on frames or rendering (remember, speed is everything here). Another reason is that we dont want the script to directly access and control what is done. Say we want an object to move through a list of waypoints, we then send the ID of that list of waypoints instead of programming the actual waypoints into the script. What I have found is worth thinking about when it comes to variables and script is this: think carefully, does one really have to have 5 different type of variables or would 1 type suffice?
Almost all parsing is done at loadtime, the only parsing that gets done at runtime is for the aSetVar function, this is because this function can take both a number, variable and the returnvalue from another function as its second parameter. Therefore a check to see what type the second parameter is has to be done. For everything else It gets converted into a structure that looks like this:
Where the most relevant pieces are msgID and pParams. You see, in order to cut down time at runtime, I store pointers to all parameters that is associated with 1 functioncall. The maximum number of parameters at the moment are 3, but those can easily be increased.
std::vector<std::vector<Action> > ifConditional;
To run the script we basicly iterate a vector that contains objects of type Action. We check if there is an if-conditional in place, if there is we check that if-conditional. If that is true we run all the ifActions inside that if-conditional. Otherwise we simply pump down to the next action in the main actionvector. So at runtime it is all about iterating, and almost nothing about parsing (I say almost because we have to parse the aSetVar function). We iterate through this vector once every frame, so therefore it is cruicial for the designer of the scripts to write them right.
Personal experience from making this script.
First id like to say that this script is in no way complete. I have yet to add support for a parser that parses pure binary files will be added aswell. The intention of this script is to have a fast and stable way to get text-files or the likes to influence the game. Would I have done something different? Sure, first I would not have skipped cruicial things like if-elseif-else but implemented them from the beginning. But I must say that it has been fun to develop this, and seeing how powerful this can be in the right hands. It has also been an enormous satisfaction to see this come to live, even though its only at a console level at the moment.
What to think of.
Think of the design, and think of its purpose. Does it have to be as fast as possible? Then you sure must think of a way to get that text-file into pure C-structures. And dont be afraid of making a very simple script the first time. Writing a parser isnt easy but not much brings more joy than seeing how you write something in a textfile and then just run a program and the result shows there.
This was just a little description of the script-system me and Bubba have worked on, and I decided to post it here to inspire and help people who might think of doing something similar.
I'm going to read that through later on when I've got time!
I'll check back when I have.
You might want to rethink the 2 variable limit (while you're still in the early development stages) if you plan on extending this to a 3d game engine. You simply won't be able to get by without some sort of floating point. I don't know what sort of interpretation you do, but in the case of binary files you can do type checking on the scoping tree pass, that won't cost much in context. If you are doing interpretation in real time it's going to come at the cost of extra branching for functions and ops, but compared to the time taken to do the scanning and parsing, that isn't a lot for the extra power you get.
Thank you for input. The first intention with this script is to have it work in a 2D environment, to see the downsides, pitfalls and what needs to be changed. So you are right, we might very well change it to use floating-point variables later on.
The reason we chose to use DWORDs (forgot to add this to my original post) is because we do not want to directly access things and change their location directly. Take for instance that we want an object to move through a set of waypoints, we do not tell the object to move through the waypoints in the script, we tell the object to use a list of waypoints, and that list is assigned an ID, and it is that ID that gets passed to the object.
About the binary files loading, that is yet in a planning stage. Must wait for some more things before an editor can be written, but i will keep your advice in mind.
Have you considered using an existing scripting language? I have a python console integrated into my engine using boost python and it's working out great. I just keep things that happen every frame in c++ and things I need to be able to rapidly develop in python.
We are using the type of language that Novalogic employs with their WAC language. For any of you that have used NILE for Joint Ops or the Black Hawk Down editor it is very similar in nature to these.
And our language can be used in 3D with no problem at all. As Shakti said nothing is ever told to go to specific coordinates. If you don't assign a waypoint or waypoint list to an object in the editor then the object will not move unless the engine takes over during a perceived threat event.
I see, I definitely failed to understand how you scripting language worked before your post.
Let me check my understanding: If use a float in the scripting language, any referencing action looks up the value associated with the passed integral id?
If this is so, it's an interesting way of doing it, I am very curious to see how it will perform (I don't expect it to perform poorly, I simply don't know what to expect).
Yah thats basicly it valis, and that is how everything is accessed, by integral id's.