Configuring via scripts instead of XML and JSON using realtime multiplayer as an example

Shortcuts: github , tiles.js tiles.groovy tiles.ruby
It is no secret that objects in games are an order of magnitude larger than their possible behaviors. When prototyping object descriptions, you can write directly in Java, C ++ or C # code, but everything will get confused pretty quickly. Then the objects are taken out to the database, either in XML or JSON config. This helps a lot, because after editing the configuration, reassembling the code is not required, and this can be done not only by programmers, but also specialists in the subject (for games, these are game designers and content designers). When a team grows or the number of objects crosses a line, programmers write a convenient editor that allows you to visually edit this JSON-config. As a result, the output is some kind of hard-supported monster.
If you are not going to hire a lot of people who do not know how to code at all, then you can try to go the other way: describe metadata using the Domain Specific Language.
What does the visual editor give, what is not when editing the / json / xml base?
- Track links between objects
- Groupings of objects are visible.
- There are no numeric IDs, the search is carried out using names.
What can give a refusal to complete visual editing in favor of a more advanced description language?
- Readable diff in version system
- Faster integration of new designs, new fields in objects
- Simplification of the description due to Convention over configuration
Purpose: get all these buns.
Speaking of numeric IDs. If in your project the numerical IDs of objects are written in the code as constants, it is better to change this, otherwise when there are dozens of them, there will be problems. Minecraft server owners have shed whole lakes of tears due to conflicts between block IDs and entities from different mods. Even if this list of constants is stored in one place, then this is also bad, because when you start writing add-ons that change this list, you will get a lot of hemorrhoids. And if you have a multiplayer game and these IDs are wired into the protocol, then get ready to burn in hell.
Result: code torn from production
github , it contains Java code with a model and a test that loads data from two scripts: tiles.js tiles.groovy tiles.ruby
The way of thinking
There is a model of tiles, for example, a couple of classes from it:
//типы тайлов
static final String[] BLOCK_NAMES = new String[] { "floor", "building", "arrow", "hideout", "abyss", "tunnel", "solid", "box"};
public class ScriptClass {
int id = -1;
String name;
}
public class Tile extends ScriptClass {
int group = -1; //номер группы
int indexInGroup; //номер внутри группы
int type = -1; //тип: константы в другом месте и их мало
int image = -1; //номер в тайлсете
int background = -1; //номер фона
int subground = -1; //если под тайлом пропасть, то отобразится эта стенка
int onDamage = -1; //во что превращается объект при взрыве
int onBomb = -1; //а если бомба прямо на нём?
int onAtomic = -1; //а если атомкой?
int direction = -1;//направление тайла, используется для стрелок и мостов
}
// промежуточное состояние для изменения тайла
public class TileChange extends ScriptClass {
public static class Variant {
int p = 1; //вероятность выбора варианта
int item = -1; //ID предмета который должен выпасть
int effect = -1; //дополнительное поле - эффект
int result = -1; //ID в который должен перейти тайл, может быть
//промежуточным состоянием
}
int totalP = 0; //сумма вероятностей вариантов
List variants; //варианты
}
And TileSet on the client:

There is also a code that acts like an automaton, when according to the data from these objects it determines which tile to put in the place of the blown up and which prize to create.
Editing this as tables in SQL or as a JSON file with a list of entities would be very inconvenient, you need something more powerful.
Let's try to describe the configuration not as a list of objects, but as a tree. The vertices will be of different types - somewhere a new tile is declared, somewhere a new group, and somewhere it is described what the tile turns into if it is hit by an explosion. In this case, we need the following:
- Parser creating this tree from text
- A piece that goes around the tree and for the vertex calls a method corresponding to its type that does something in the model
- Logic for Convention over configuration: filling in blank fields according to the agreed logic, for example, you can take a value from the corresponding field from the group
If the parser can be taken as usual - JSON, XML then what you have to write in the second paragraph yourself. Or not? This is where the secret lies: you can immediately use the scripting engine, you just need to create bindings so that it calls methods to construct the model.
The result is a language that is more suitable for the field, Domain Specific Language.
Here is a description of the most common tile - a brick wall, from which prizes may fall out with different probabilities.
It should be noted that there will be less brackets on groovy or coffeescript brackets.
newTile("brick", {
type: "solid",
image: 44,
// сейчас будем описывать что будет если блок снесут
onDamage: newChange("prise_in_brick", {
"variants": [
// Прописываем выпадение предметов
{ p : 60, item : getSlot("bomb")},
{ p : 40, item : getSlot("power")},
{ p : 40, item : getSlot("scate")},
{ p : 10, item : getSlot("kick")},
{ p : 10, item : getItem("random")},
// Тут даже можно описать некоторый предмет, который будет превращаться во что-то другое когда его подберут
{ p : 10, item : newItem("surprise", {image: 11, effect: ITEM_EFFECT_SANTA_CANT_TAKE, variants: [
{p: 5, slot: getSlot("bomb") },
{p: 5, slot: getSlot("power") },
{p: 4, slot: getSlot("scate") },
{p: 4, slot: getSlot("kick") },
{p: 3, slot: getSlot("jelly") },
{p: 5, slot: getSlot("detonator") },
{p: 6, slot: getItem("ball") },
{p: 0, slot: getSlot("money"), count:20}
]})
},
{ p : 10, item : getSlot("heart")},
{ p : 430 }
],
// В любом случае надо снести этот блок
result: getChange("destroy_rocky") //оно ещё не объявлено, но это не мешает на него сослаться
})
});
newChange("destroy_rocky", {effect: BLOCK_EFFECT_DESTROY_BLOCK, variants: [
{p:2, result: getTile("grass")},
{p:1, result: getTile("grass2")},
{p:1, result: getTile("grass3")},
{p:1, result: getTile("rocky")}
]});
Implementation
Functions of type newXXX and getXXX create and search for entities by returning a pair (object id, object type). newXXX also manages to check whether the types of the fields of the object. All get-s are carried out by name, and return an object even if it has not yet been created - this ensures modularity, that is, the config can be split into different files corresponding to different aspects of the game. In this case, it will not matter what exactly the order of execution of these scripts will be.
As a result of the configuration, ScriptClassTable tables are created with objects inherited from ScriptClass. In this case, the object IDs are generated automatically during the execution of the script. Only IDs are transmitted over the network, without object names, and they are the same on the client and on the server, since one script was executed both there and there.
At first I used JavaScript, since the browser client is built under JS. The server used the Rhino engine. The abstraction was carried out due to the different implementation of the ScriptUtils interface, which does not know about the game model at all, but knows that there are types of "Object", "String", "List" and "Number" in the script, and that the script can call methods.
Later, I began to transfer tables over the network, while the script began to be executed only on the server. Now I am translating the configuration to groovy so that I can describe not only data to it, but also use closures to set the behavior of some special entities. Of course, I could do it through JS as well, but calling the code from the groovy script in java is much simpler, because it is compiled into binary directly in runtime. Groovy syntax is also sweeter, in many cases the brackets can be omitted. Although, for this it was possible to switch to coffee.
Due to the chosen abstraction, the transfer from Rhino to Groovy took 20 minutes: it was enough to register another implementation of the ScriptUtils class. The script itself has changed little: curly brackets have changed to square in the description of objects, and the syntax for the description of functions. After the transfer, it immediately went into production.
Bootstrap on the server:
- The server finds out which configuration to use, maybe even takes it via http
- The script is executed, tables of object types are generated
- A map generator is created, he gets from the table the tiles he needs by their name.
- The rest of the controllers start, everyone requests and remembers the types of objects that they need
- The game begins
- Each logged-in user is given those tables and only those fields of objects that the game client needs
Fortunately, loading the game takes quite a short time, which allows you to quickly debug the configuration. Visual tools are still present in the game itself, but with their help it is not yet possible to edit the config.
It is worth noting that some objects from this configuration describe the analytics format that will be collected for players: if there is a “money” slot, then the items collected per round that increase this “money” also influence the fact that it will go to the statistics database in the “money” column ".
Look like that's it.
Of course, this DSL does not yet take advantage of the delights of the language on which it is based. It is possible to make the configuration not only build the model, but also describe the generation of the map and structures on it, injecting the code into the corresponding controllers. Also, a few constants like BLOCK_XXX and EFFECT_XXX can be passed to the script through bindings.
Those who ask good questions will give access to the skins in the game if you write your login :) And do not be shy in criticism.
PS While writing this article, I had to interrupt to go to the Topcoder Open onsite
PS Thanks akzhan , now there is also JRuby