Friday, 27 May 2011

Tech feature: Scripting upgrade

Introduction
For a couple of months now I have, on and off, worked on some basic tech aspects for the engine. Everytime I was done with one of these I thought it was among the hardest things I would do for the new engine, yet the next feature as always proved more challenging. Terrain geometry was harder to implement than sun shadows, terrain texturing harder than geometry, and so on.

Implementing the script system is no different. It is easily the hardest thing I have done so far for our new engine - HPL 3. It has had this "perfect"sort of challenge: Difficult problems to solve, much basic knowledge to wrap your head around and awfully boring and monotonous parts. I really hope this marks the end of this trend of increasing difficulty, as another proportionally large step might make my brain to melt and my fingers to crack. At least it can wait for a lil while...

Enough complaining. Now that the scripting is pretty much implemented (some engine stuff still needs to be added and some more problems remain to be solved, but it is really minor), I am extremely happy with it. I think it will help us build better games, and to finish them faster. Now let's move on to how a boring scripting system accomplishes this.


Background
Before moving on the the actual scripting I need to explain what brought on the creation of the current system. It all started with our first commercial games, The Penumbra series.

When creating the Penumbra games our tools were primitive to say the least. All maps were made in a 3D modeling program, Maya, and then exported to Collada. The game engine loaded the Collada file and built the map from it. As a 3D modeling program is meant to create 3D models, it is not really meant to make levels in. With no ways of placing entities we had to use special naming conventions to tell the game where any non-static objects in the game were located. To be able to do this properly we had to make special "instance" versions of each model meant for the game, since without this you would not be able to see how an object was placed before the game started.

Lighting was equally annoying since Maya has no support for radius on lights. This mean that you could not visually see how far a light reached, but simply entered a numerical value and hoped for the best. As this was not enough, you also needed to place portals and group meshes in order for the engine to provide occlusion culling. This could be quite tricky at times, and often you could sit a day or two simply tweaking portal setup. Added to this was also the problem that Maya often failed to show any textures, and most editing was done on grayish levels. For more info, you can just check the wiki.

The problems do not stop here though. Everytime you meant a change to the game, you had to do a complete reload. So setting up lighting in the game could be quite the effort: change light position for two seconds, load map for 2 minutes, notice it is not good, repeat. As you might figure, we got quite good at batches tasks and the phrase "it will have to do" was uttered more often than not.

For scripting it was just a grueling. Every change in the script required a full restart of the game, creating the same sort of frustration present when modeling maps. And to make scripting even more frustrating, there was no syntax checking until the entire map was loaded! This meant you could wait two minutes of loading only to find out you had forgotten a semicolon or something else trivial.

As I write this, I actually have a hard time understanding how we could have gotten anything done at all. And unsurprisingly, even though we released mod tools and documentation, not a single user map for Penumbra was ever released.

For Amnesia we knew we wanted to fix this somehow. The first step we took was to simply make our own editor where all the maps are built. Since it rendered with the same engine as the game, it made is much easier and faster to tweak entity and light placement. We instantly saw that productivity rise with this change. For scripting it was pretty much the same, but we added the extremely simple fix of compiling the script before loading the game. This removed some of the time previously spent on, in vain, looking at loading screen.

Although we had new tools all was not good. You still had to reload all level data every time you made a change to the map or script. We did not think much of this though as we were so used to doing it this way, and happy that we had all the other improvements. However, a year and a half into the development we discussed if we really needed to reload the level. I cannot recall what sparked this idea, but anyhow we figured that we did not and I added a menu with a Quick Reload button. This cached all textures, models, etc and reloaded map very quickly (usually taking but a few seconds). This increased productivity and creativity tremendously and was one of the better decisions we made during the development of Amnesia. Another sign of how much these changes improved work flow are the over a hundred of user maps created as of today.

What is so strange about the reload-feature is that is something that we could have added during the development of the first Penumbra, but for some reason we did not. It is quite frightening how often you convinces yourself that there is no better way of doing a task, and never try to improve it. We did not want to make this mistake again and started thinking of what more we could do.


Taking script to the next level
In Amnesia and Penumbra, scripting is only used to control logic flow in the levels. How enemies spawn, how puzzles work and so on. All other gameplay is hard-coded into the exe file and written in C++. Normally when I write this kind of code, rendering for instance, I can do large chunks at a time and then simply see if it works as intended. This often in small projects that are fast to reload. However, when writing gameplay and UI code this is almost never the case. Instead you constantly need to fine tune algorithms and variables until you get the expected behavior and work in large projects. Not only does this mean a level restart (with full resource reload), but the exe itself also needs to be built from code, a process that can easily take half a minute, even if the changes are minor. This means that coding gameplay can be quite a hassle at times, on par with how map building was in the Penumbra days. With the lessons learned from Amnesia fresh in mind, this felt like the obvious area of improvement.

In order to make this happen we had to move as much gameplay code as possible into the scripting. What this meant was that we needed to do some large upgrades to our current script implementations. For example, right now we only supported the most basic types (bool, int and float) together with strings in script. This already caused some issues when exposing game/engine functions and when writing scripts, for example instead having a single argument for color, you had to have four floats (one for each color and one of alpha), making code ugly and writing it more cumbersome. So just this upgrade was worth doing.

We also needed expose all engine classes, so that the script could be used pretty much as if it was normal C++ code, and achieve pretty much the same things. I was not sure exactly how much to expose but knew that the more the better.

Finally, the most important feature was to be able to reload script at any point, so it would be easy to just change a line, click a reload button and then a few seconds (or less) later see the change in-game. To get this working was the by far the most important goal with the entire upgrade. This would not be as easy as the level script was in Amnesia though, since the script system would not only take care of the code, but of part of the data as well. This meant I needed to save the state somehow, a feat I was not sure yet how to accomplish.


Implementing Classes
Before tackling the problem of script reload, I first had to make sure to add engine types to the engine. And even before doing that I needed to be sure our current scripting middleware was up to the task.

The scripting system that we were using, and have been for a long time, is a library called Angel Script. It is actually the middleware that we have used the longest, ever since end of 2004 and the Energetic project. Even though we have used it for such a long time we never really used to anywhere near its full extent and now was finally the time to make up for that. I took a day to look through the documentation and found that AngelScript could support everything that we needed, and what was even better was that it was not that difficult to add.

AngelScript is a bit different from other popular script languages (like Lua) in that it is strongly typed and very closely connected to the underlying C++ code. For one thing this makes the script quite fast (although not faster, see end of post for more info). It also meant that AngelScript could link to the classes almost directly and I only had to declare the class and link to its different parts (whatever member variables and methods that I wanted to be exposed). It also supports pointers quite easily, but makes them more secure with a script specific handle type. This requires you to keep track of some memory management for the data, but you do not need to do it, and I could very easily and quickly add support for a vast number of engine resources (textures, meshes, lights, etc).

The only thing that was a little bit trickier was supporting inheritance (when a class can build upon another class). Basically you have to redeclare methods every time you add new class that inherits from something. You also need to specify to what other classes this class can be cast to. This might sound a bit of a hassle, but the result is that you can control so the script always has a very close mapping to the code it exposes, something that I was extremely thankful for when implementing the state saving (more on that below). Also, through some use of macros, adding the implemented classes become quite easy.


Basic script layout
The next thing to figure out was the basic structure of the script code. In Penumbra and Amnesia we simply added the functions directly in a script file and then allowed that script file to represent an object. I first thought that this would be a valid design this time again, until I started thinking about how to store the data (meaning any data that should be kept between executions of the script).

In Amnesia and Penumbra the only data that is saved is simple variables like an integer for the number of times the player had stepped on button or similar. This is done through using special functions for all these variables, for example:

SetLocalVarInt("ButtonPushNum", 1);


This function saves the data to the game, and when the script is reloaded (meaning destroyed and recreated) the script can easily reference the data again by doing:

int lX = GetLocalVarInt("ButtonPushNum");


However, this time the game had to save a lot more complex data, like pointers to resources, matrices and whatnot. At first I actually considered using system with functions like this, but I figured that it would just mean a lot of extra work, and just make things more difficult. Again I thought about the lessons from Amnesia's reload feature, and thought it was worth trying to find a better solution. What I ended up doing was to force each object to be contained in class, and then let all data to be members of that class. This allowed AngelScript to make copies (needed for enemies and whatever there will be more than one instance of) and also made it was easy to keep track of the members (you simply create an object in the C++ code and can then iterate all its data).

A problem that I realized now was that I needed to have C++ functions that only worked in certain types of classes and only on data for a certain copy. For instance, an enemy class might want a function like IsInLineOfSight(...) to see if it has visual contact with something. However, there were not any functionality in AngelScript for doing this. I could give the script class a template class that forces it to implement certain functions, but I could not let the C++ code act as a base and expose specific functions from it. To solve this I had do some hacking. I ended up using global functions, and to keep track of the currently active object. (Again with macros to the rescue.) The resulting solution was not perfect though, as the global function can be used in any class and not just the one it was meant for. I am still looking into some fix for this.


Saving the state
It was now time to save the state of the script. To start this off I took the naive approach and simply saved the script data directly by copying it. This works very well for stuff like vectors, matrices, etc where it is just a matter to make a copy of the data. But it does not work that well with resources like meshes, texture or even objects like physics bodies, billboards and lights. There is way too much data in those to copy. And if I simply save the pointer I need to make sure that no data has changed when the state is loaded again, or else the saved state will not work.

At the time I was only building the system to work with a script reload only, so these issues did not pose a problem. But a major obstacle popped up when I wanted to save classes defined in scripts code. These where not possible to simply save by copying, because when you rewrite a script they can change entirely. For instance, if the class when saved consisted of two integers, and when reloaded has one string and a matrix instead the data you saved is invalid. It gets even worse if a script class has another script class as member and even more so if script classes are saved in arrays.

I figured I needed to do some kind of drastic change if this was to work. I was sketching on a few systems that could save variables in a separate structure, when it hit me. The system I was working on could not only be used to save the state, but if I implemented it correctly I could also use it for saving. For Amnesia and Penumbra I use a special serialize system that can save classes to file if initialized correctly. It is quite cumbersome, but way better than writing load / save for every single variable. However, as I was to do most gameplay code through script (the code that pretty much contain everything that needs saving), I could actually get rid of rewriting the save code for every single gameplay update. Another huge benefit of using scripting: automatic saving. This is bound to save tons of work. (It did mean a lot more work though...)

Up to that point I had looked at the scripting as separate part of in the engine. A module that all other modules could work without if removed, exactly like how most other modules work. Thinking like this had colored a lot of the design decisions I had made. Now, I instead decided that since scripting will be basically what controls the engine, I started of thinking as something that was part of all other modules. This led me to do a major rewrite of the state saving.

There is quite a lot to say about this system (and serializing in general), but I do not really have the space right now, so I will just do a quick overview. First of all, the system defines a few basic types that all other structures are built up from. These are bool, int, float, vector, matrix, color, etc and I implement very specific create/save/load methods for each of these. In AngelScript all of these are also implemented as primitives (built-in types) or data objects (which is a type where AngelScript handle all memory management). When saved the actual binary data is saved.

All other classes are implemented as references, meaning AngelScript only manages their pointers. When saving the state of these classes, each class must implement a a special method that adds all of the member variables (either basic types or other reference classes) to a list. (Basically the explicit method described here). This list is then used when transferring data to a special save buffer that contains the data of all the basic types and/or sub buffers containing data for other classes.

For certain classes, its pointer is backed up in the C++ code. So when the save data for it is loaded, the pointer to the class is first searched for and if found it is updated with the saved member data. This makes it possible to reload the script code without having to recreate all the data.

Of course, all sorts of complications arise during this, and again it would take too long to go through them all. But as I hinted before one thing that saved me is that script objects are so closely connected to the engine data. When writing code that deals with serialization one of the biggest annoyances is that that class pointers with multiple inheritance can change their address when cast. Because the script language only returns void pointers (memory address with no type specified), you must be sure of the type it returns, something AngelScript allowed me to be.

Another fun thing with serialization are all the strange macros that you have to make. For example, this one was pretty fun:

#define ClassMemberClassOffset(aClass,aMember)

( size_t(static_cast(&(( (aClass*)1)->aMember)) ) -1 )

This one is used to make sure that class member offset address points correctly. It is one of the many things that make sure multiple inheritance problems, as explained above, does not screw things up. I got an entire header file just filled with fun stuff like this!


Closing notes
Once I had the system working it was quite an effort to add all the classes. There are countless classes that all need to be set up in specific way and everything to be saved needs to be initialized. Sitting several days in a row just coding stuff like this can really tear on the psyche. Fortunately, almost everything is added now, so the worst part should be over.

Another nice addition to the new script system is that it can auto generate the API function file for NotePad++. This allows NP++ to autocomplete any text that you write and you no longer need to keep function names or arguments list in your head (and skip the manual look-ups). What is annoying though is that it only works with global functions and NP++ cannot recognice what class a certain variable is in and show all its members (like visual assist can for C++). Does anybody know an editor that can manage this?

One thing that I was really eager to get working was threading. In Amnesia we use tons of special timer callbacks to set up events, and it would have been so nice to do something like this instead:

...
PlaySound("x");
Sleep(10)
PlaySound("y");
WaitUntilTriggered("a")
...

AngelScript does support this sort of thing, but there is currently no way of saving the state. So if the player where to save during a sleep, the game would be different when loaded. This is unfortunate, but I am looking into some other solutions instead. If anyone has ideas on this please share!

Right now I have only tried this system in some tests, but it still feels really good and works exactly as I want. It is just so great to be able to reload any code changes in a fraction of second. It allows for rapid iteration, makes one more productive and generally just makes it so much more enjoyable to code. It is also so gratifying to have an idea and then successfully implement it to the sort of quality you hoped for.


Addendum:

I earlier wrote that AngelScript was faster than Lua, which is not the case when it comes to code solely executed in the script (see here). So obviously my initial statement was not correct. However, I still think AngelScript must be faster in calling implemented c++ functions/types as there is pretty much no overlay involved. I do not have any data to back this up though, so do not take my word for it :)


Found this irresistibly interesting? We are hiring!


19 comments:

  1. I've been, for a long time, in the process of hardcoding every event for my 2d-shooter, all in c++. I have created a script-class with the basic level-functions used by all levels. Foreach level I then create a unique script-class inheriting that basic class.

    Let me tell you! It's not a good way to do it. I should give up and try to implement some scripting language.

    So, it was an interesting read, indeed.

    ReplyDelete
  2. Code::Blocks is my favorite C++ IDE right now. It's cross-platform and it's pretty easy to point it at the root of a source tree, have it generate a project, and get all the code-completion you could need. :)

    It does screw up sometimes and insist that some symbol isn't defined/implemented/etc., but it has some other tools to help whenever that happens.

    ReplyDelete
  3. About Notepad++: It has several forms of autocompletion, as described in their documentation.
    There's one based on the file that lists the keywords (Ctrl+Space), and another one based on recognition of all the names written in the current file (Ctrl+Enter).

    So basically, to get autocompletion for predefined member functions, put them in the API file, and just use Ctrl+Enter for the ones defined in the script.

    I believe that you can find more advanced info at the same site.

    Also: take a look at this.

    ReplyDelete
  4. Thomas said:
    "A problem that I realized now was that I needed to have C++ functions that only worked in certain types of classes and only on data for a certain copy. For instance, an enemy class might want a function like IsInLineOfSight(...) to see if it has visual contact with something. However, there were not any functionality in AngelScript for doing this. I could give the script class a template class that forces it to implement certain functions, but I could not let the C++ code act as a base and expose specific functions from it. To solve this I had do some hacking. I ended up using global functions, and to keep track of the currently active object. (Again with macros to the rescue.) The resulting solution was not perfect though, as the global function can be used in any class and not just the one it was meant for. I am still looking into some fix for this."

    If I understood you well, you have predefined in-engine C++ functions that do something, and you want them to work with in-script instance variables?
    If so, why is that a problem? I haven't studied AngelScript in the amount of detail you have, but it does have some pretty decent support for classes. If so, isn't it possible to somehow "wrap" the C++ function - to call it internally? All it would take is to pass it a pointer to the object. As long as it is not directly exposed to the script, it would act as an instance function.
    Anyway, could you describe the problem in more detail? Maybe we can help.

    ReplyDelete
  5. Regarding the class function problem:

    First of all, as I was writing this article, I noticed how it just kept growing and at the same time i could only cover subjects in a very shallow fashion (still hope it is of some interest). That is why this problem is so badly defined. So here some more in-depth info:

    What I want to do:

    -----------------
    C++ psuedo code:
    class cCompiledClass {
    void FuncToUseInScript()

    ScriptObject * mpObject:
    }

    Angel script pseudo code
    class cScriptClass : cCompiledClass {
    void SomeUpdate() {
    ...
    FuncToUseInScript();
    }
    ...
    void FunctionInOtherClass() {
    ...
    cScriptClass@ pX = GetScriptClassInstance(x);
    pX.FuncToUseInScript();
    }
    -----------------

    Now, cCompiledClass would contain instance of cScriptClass as its ScriptObject* member variable. And it would also have a member function, FuncToUseInScript, that could act on data in an instance of cCompiledClass and be used as function of cScriptClass. As far as I know, this is not possible in Angel Script.

    What I do now instead, is to do a global function:
    cCompiledClass_FuncToUseInScript();
    That when class in cScriptClass will be able to work on instance specifc data of cCompiledClass (this through some tricks). However this function is global and also accessible by other script classes (which results in invalid data access). Also the above example:
    pX.FuncToUseInScript();
    Does not work with this.
    I guess I could fix the access problem with some kind of usage of the group functionality*, so might not be that much of a problem. But it all still feels kind of hackish.


    *Edit: Ha! Sometimes you just code too much. It turns I DO use the group feature! However the problem still kinda remains, as the function can still be used in other classes, but these classes must be defined in the same scrit file as the owner class (cScriptClass in the above example). But then again, that actually works in some wierd way, as all classes inside this script file will almost always have been created, and only accessed through, an instance of the owner class (cScriptClass). Sorry for the tangent, but just want to keep the record straight :)

    ReplyDelete
  6. Regarding the class function problem:

    I was reading AngelScript documentation, and I'm under the impression that it is possible to use RegisterObjectType() to expose your C++ types, and then member functions via RegisterObjectMethod(), and even non member functions as member methods of the script class.

    Looks like you have full control of what is exposed, so you can have a neat, well designed script class on one side, and u bunch of auxiliary member functions on the C++ side.

    I believe you're just overthinking it. Why do you feel the need to derive your cScriptClass from it's C++ counterpart?
    You could just expose it, as you would normally do.
    If further specialization is required on the script side, I believe that you can derive a new class from the exposed one (all in-script). I know AngelScript supports inheritance, I'm not sure if there are any restrictions regarding derivation from the exposed classes...

    The factory function registered for the object can (and should) return the instance of the exposed C++ class. As far as I can see, all the member methods exposed to the script-side equivalent essentially delegate their responsibility to the registered C++ methods, so everything should behave as your standard C++ object in "normal" programming.

    Of course, in the case you described, if the function you need exposed doesn't belong to the class that you want exposed, maybe you'll need to adjust the signature it's signature slightly (to accept a reference to the instance). If that is not possible for some reason, maybe you can wrap it into another function (free or class member) that would adapt it's signature to what is required by the script engine, and expose that instead.

    ReplyDelete
  7. "AngelScript is a bit different from other popular script languages (like Lua) in that it is strongly typed and very closely connected to the underlying C++ code. For one thing this makes the script quite fast."

    AngelScript may be "fast" for a scripting language, but it's not faster than Lua, and much much slower than LuaJIT. :)

    ReplyDelete
  8. class function problem:
    Problem is that AngelScript does not let classes declared in script inherit from classes I implement in C++ code.

    Regarding LUA being faster:
    Any idea on how much slower it is? Has there been any good tests? I have to admit my LUA knowledge is not that good, so AngelScript's major advantage might be its easy to implement. I kinda assumed that since the script is so close to engine types, there would be extremely little overhead when script calls "compiled code".

    ReplyDelete
  9. Regarding the class function problem:

    OK, I guess I couldn't really present my point in totally abstract terms like that, so I went and downloaded the AngelScript SDK, compiled the thing and set up a small test console application.

    This way I was able to put my ideas to the test. So, I think I have a reasonable approach, and I've posted a relatively lengthy post about it, with code, at your forums for convenience.
    My point was, why do you think you need to derive a script class from the C++ class? You can just expose it. See the forum post for a more detail explanation on what i mean. In case inheritance is for some reason required, I also present an alternate way to extend the functionality of the exposed class in the script. I think what I have in mind is the natural way to go.

    P.S. The other anonymous that talked about Lua may be basing his/hers claim on facts, but maybe he/she is just biased... You know how people like to glorify the languages they use. :D

    P.P.S. What is wrong with Blogger, takes me ten tries to post this... Damned spam filter or something.

    ReplyDelete
  10. God damn spam filter removed my reply or something!
    (And after letting me post it in several tries!)

    Oh, well...

    OK - about the class function problem:
    I think you didn't catch the point I was making. Just to make sure I'm on track, I downloaded AngelScript and setup a small Win32 console app, so that I could put my reasoning to the test.
    I understand what you're trying to do, and I think that what I'm suggesting is a pretty natural approach.

    I've posted the explanation, along with some example code, at your forums, for convenience.

    I'm not the anonymous that talked about Lua, but just a thought: although his or hers statement may be based on facts, it may also be biased... You know how people like to glorify the language they love and use.

    ReplyDelete
  11. Sorry: at your forums - here.

    (I just noticed you've seen it, but I'll provide the link for anyone else interested).

    ReplyDelete
  12. Regarding your question about threading: in Unity3D they do such things with the help of coroutines. It seems that Angelscript has coroutines too, so take a look at their usage example

    http://unity3d.com/support/documentation/ScriptReference/index.Coroutines_26_Yield.html

    ReplyDelete
  13. R-ride:
    Thanks for the info! Will check it out and see what can be implemented.

    ReplyDelete
  14. Hi,
    I'm glad to see that you're still working on making greater and greater games. :) I'm looking forward to know more about the next one, because your work on terrain geometry, grass, sun… makes my mouth water.

    Now, I have a question concerning HPL2 and in particular the level editor. Why didn't you choose to use pre-existing libraries to make the GUI? Or did you? It seems that you made everything from scratch. Not only this must have took a certain time to code, but also usability is very bad: very few shortcuts, Alt-F4 and close button don't work, the window can't be resized nor maximized, keyboard navigation is barely possible.
    [I cannot test it right now because it won't start (maybe since last update) so what I say here may not be correct, sorry.]

    Thanks again for Penumbra and Amnesia. ;)

    ReplyDelete
  15. Niavlys/Delhovlyn:
    Because the system was at first use in-game in Penumbra Black Plague and then grew for there. The reason to use then because we had that good base and was possible easily work with other rendering. Our first goal was for the editor to be fully integrated into game (a key press and editor pops up with WYSIWYG interface) and an in-engine system makes that possible very easily. We dropped the in-game editor thingie (although current code makes it almost possible to implement), but I still think it makes WYSIWYG interface possible.

    Shortcuts, keyboard nav, etc is not really limits of the system, but just that we have not implemented em.

    ReplyDelete
  16. Is it possible to release some sort of editor SDK, in a form of a core library that can be extended (without revealing the source code), so that the us with dev-skills in the community could re-create the editor UI with some other UI framework (Qt, or MFC, or .NET Windows Forms, or even WPF)?

    The goal would be to make them a bit more customizable and user friendly, easier to use, while completely relying on the provided library to provide all engine-related functionality.

    This way you could concentrate on working on your new game/engine, and the modding community could benefit from the revamped editors.

    ReplyDelete
  17. Thanks Thomas for your explanation, I understand your reasons. (Can we hope for a better GUI for the HPL3 level editor?)

    I second Anonymous's proposition (though I'm not qualified to do that by myself).

    ReplyDelete
  18. Hey Thomas, here is a short game you will like

    http://gamejolt.com/freeware/games/adventure/which/1523/

    It's a artsy black and white horror game, its free btw.
    There are no jump sequences, it does not rely on terror but it attacks you with depression and feeling of hopelessness. if you know how to beat it, you can pass it in 3-4 minutes.. please check it out :)

    ReplyDelete
  19. Were I not still an undergraduate in Computer Science, I would totally apply!

    ReplyDelete

SiteMeter