Why using a scripting language?

First and foremost, what are the reason to use scripting language at all? I think it is a good question, as it may be obvious for some, it may also not be for others. After all, everything you can do with a scripting language can be done by whatever language (C++ in our case) your project is developed with, right? This is an actual statement a colleague gave me. And the answer would be yes.

For instance, if you want to develop an artificial intelligence for your video games, you can write it completely in C++. But then another question comes up, what if we want to change this artificial intelligence or add a new one? The answer would be, we can still make it in C++. But what does it implies to do so?

A recompilation of the project ; which also means making a new delivery and last but not least, a temporary outage of your service when you deploy your change. This point is really important, for example, you are scripting an NPC (non-playable character) of a game to go through a certain path, but for whatever reason, you want to change the path of this NPC. You will need to stop all activity on your game just for such a change. This is too great of an impact on all the players to even consider it.

; which also means making a new delivery and last but not least, a temporary outage of your service when you deploy your change. This point is really important, for example, you are scripting an NPC (non-playable character) of a game to go through a certain path, but for whatever reason, you want to change the path of this NPC. You will need to stop all activity on your game just for such a change. This is too great of an impact on all the players to even consider it. A code quality impact; more code means potential more bugs (which would require a new delivery to fix), and usually more code means more temptation to make a hard to understand program. If your game server is supposed to just manage the movement of characters, adding movement scripting could be very verbose and pollute the main code (which is the main purpose of the server).

But despite those technical issues that can occur if you are not using a scripting language, there are great advantages about using scripting:

High flexibility ; as the code has not to be recompiled or deployed, you can dynamically add scripts (Artificial intelligence for enemies, walking path etc…) on the fly. And an error made in the script isn’t a big issue as you can hot-fix this issue on the fly, without having to cause an outage.

; as the code has not to be recompiled or deployed, you can dynamically add scripts (Artificial intelligence for enemies, walking path etc…) on the fly. And an error made in the script isn’t a big issue as you can hot-fix this issue on the fly, without having to cause an outage. Easier to write and implement; scripting languages are usually easier to write than C++ code (to my mind it is the case for ChaiScript at least). The implementation of the scripting engine was very straightforward, I didn’t encounter any obscure API issue to map the C++ and the scripting engine.

About ChaiScript

Github description (link) ChaiScript is a scripting language developed by Jason Turner, it is one of the only embedded scripting language designed from the ground up to directly target C++ and take advantage of modern C++ development techniques, working with the developer how they would expect it to work. Being a native C++ application, it has some advantages over existing embedded scripting languages: It uses a header-only approach, which makes it easy to integrate with existing projects. It maintains type safety between your C++ application and the user scripts. It supports a variety of C++ techniques including callbacks, overloaded functions, class methods, and STL containers.

One of the reasons ChaiScipt is very interesting compared to other scripting languages such as LUA is because of the C++ binding. As ChaiScript has been specifically developed for C++, the communication between the code and the script is very easy (almost transparent). The hello world example below is proof of that, here a function is registered to chai and then used in a script. But it is as easy to give a reference to a C++ object to chai.

#include <chaiscript/chaiscript.hpp> std::string helloWorld(const std::string &t_name) { return "Hello " + t_name + "!"; } int main() { chaiscript::ChaiScript chai; chai.add(chaiscript::fun(&helloWorld), "helloWorld"); chai.eval(R"( puts(helloWorld("Bob")); )"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <chaiscript/chaiscript.hpp> std :: string helloWorld ( const std :: string &t_name ) { return "Hello " + t_name + "!" ; } int main ( ) { chaiscript :: ChaiScript chai ; chai . add ( chaiscript :: fun ( &helloWorld ) , "helloWorld" ) ; chai . eval ( R "( puts(helloWorld(" Bob ")); )" ) ; }

The documentation is unfortunately incomplete as Jason said that it is hard to maintain and that most people don’t even read it. So most of the “hidden” feature of Chaiscript are explained in the discourse forum. But there is a strong community around chaiscript, and most of the question you have certainly have been already answered there.

Compilation time can be an issue as the chaiscript language is compiled when the project compiles. In order to reduce this impact, it is preferable to compile the software piece communicating with Chaiscript in another target (binary/library) to compile it less often. When the implementation of how chaiscript is used in the application is done, it is usually not needed to recompile this part anymore, as the work is done on chaiscript side.

How ChaiScript has been implemented

I decided to implement a scripting engine in ChaiScript for the Arena service of FyS Online. This service is responsible for the management of battle in the game, all the possible encounters are going to be implemented via a chai file that will describe the attacks available for this enemy and its fighting strategy (who to focus, heal etc…). By implementing them in such a way, it will then be easy to add new foes that the player can encounter.

There is a class representing the layout of the battleground containing the contenders (monsters) and the ally characters (the players), those class are easy to share between ChaiScript and C++.

I decided to make scripts following specifics templates in order to ease their usage and development. Templates for monsters and attack are convenient in order to be able to create a lot of different spells and/or actions without having to care too much about how to make it interact with the other scripts. It is always possible to have a script taking more argument, implementing complex methods or usage. But it will be more error-prone and hard to use as the scriptwriter will have to know how to use a specific attack instead of having a generic way to use it.

Implement a basic chaiscript class for each monster following this implementation:

// Monster chai template class class Monster { // id of the monster (as multiple monster of the same type can exist) attr id; // level of the monster (impact on the life total of the monster, and level of the actions) attr level; // attacks available for the monster attr actions; def Sampy(contenderId, level) { this.id = contenderId this.actions = [ "baseAttack": BaseAttack( level ) , "groundStrike": GroundStrike( level ) ]; } def runScriptedAction(id) { // get the current C++ object of the monster var &thisContender = pitContenders.getFightingContender(this.id); // get its status (health, mana, etc...) var &thisStatus = thisContender.accessStatus(); // can get the players on the battlefield in order to define what action to perform, // the target and the global fighting strategy (focus the lowest character in health for example) } def defineSpawningPosition() { // ... return a position on the battleground to spawn into, // can retrieve data about where the allies spawned or if the current encounter is an ambush // in order to define a strategy on how to spawn } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // Monster chai template class class Monster { // id of the monster (as multiple monster of the same type can exist) attr id ; // level of the monster (impact on the life total of the monster, and level of the actions) attr level ; // attacks available for the monster attr actions ; def Sampy ( contenderId , level ) { this . id = contenderId this . actions = [ "baseAttack" : BaseAttack ( level ) , "groundStrike" : GroundStrike ( level ) ] ; } def runScriptedAction ( id ) { // get the current C++ object of the monster var &thisContender = pitContenders . getFightingContender ( this . id ) ; // get its status (health, mana, etc...) var &thisStatus = thisContender . accessStatus ( ) ; // can get the players on the battlefield in order to define what action to perform, // the target and the global fighting strategy (focus the lowest character in health for example) } def defineSpawningPosition ( ) { // ... return a position on the battleground to spawn into, // can retrieve data about where the allies spawned or if the current encounter is an ambush // in order to define a strategy on how to spawn } } ;

Monsters are having a set of actions, each action is also a chai script that could look like so:

// Action chai class template class Action { def Action(/*needed parameters, damage, level of the attack and so on*/) { } def requireAllyTarget() { // return 1 or 0 depending if it requires at least one ally target (heal for instance) } def requireEnemyTarget() { // return 1 or 0 depending if it requires at least one opponent target (attack for instance) } def execute(targetStatus) { // Execute action on the target (impact on the target status, life point etc...) } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // Action chai class template class Action { def Action ( /*needed parameters, damage, level of the attack and so on*/ ) { } def requireAllyTarget ( ) { // return 1 or 0 depending if it requires at least one ally target (heal for instance) } def requireEnemyTarget ( ) { // return 1 or 0 depending if it requires at least one opponent target (attack for instance) } def execute ( targetStatus ) { // Execute action on the target (impact on the target status, life point etc...) } } ;

When loading a fighting pit (a new battleground for a given player), the monsters and their attacks class definition are going to be loaded in the chaiscript engine. Instances of monsters class are going to be instantiated in the engine, the order list of characters is then instantiated. Then the game loop is started, when monsters turn come, the method runScriptedAction is executed (which is going to define the action and target if needed and run the execute method to apply the action).

Those scripts are going to have access to utility function provided from the C++ code in order to be able to get information on the game state and/or sending a message of a completed action to the players.

Conclusion

I didn’t extensively test ChaiScript performance-wise but I never experienced any latency issue so far by testing my implementation, it is good to know that other games are using chaiscript extensively in their game engine (Spiced, made by Jason Turner or LibRetro made by Rob Loach and many others) and works fine.

Chaiscript is incredibly easy to bind with C++ as it has been made for that purpose. Its syntax is very close to C++ making it easy to learn and create a sensation of total transparency between C++ and Chaiscript (when an interaction between the two languages is needed).

I would advise any developer needing a scripting language to add functionality to their application/game to use Chaiscript because of how easy it is to implement and write scripts.