The development of an MMO RPG is a practical guide. Server (part 1)
- Gaming backend: what modules should it consist of?
- Calculation of character parameters: virtual methods or addition of arrays?
- The logic of behavior: at what level should it be?
- Moving characters: who should manage it?
Today we will continue to get acquainted with the development and design of an online game using the space MMO RPG “Starry Ghosts” as an example . This article will talk about the backend in C ++ and it will be thoroughly technical.
There will be many references to the functionality of Star Ghosts in the text , but I will try to present the material so that you do not need to delve into (and play) our product. However, for a better understanding of the material, it is advisable to spend a couple of minutes and see how it all looks.
In this article, we will focus on architectural solutions in relation to the backend of an MMO RPG in real time. There will not be much source code and it will certainly not contain such C ++-specific things as multiple inheritance or templates. The purpose of this article is to help design the game server and familiarize everyone with the specifics of the game backend.
The described solutions are quite versatile and quite suitable for many RPGs. As an illustration, at the end of the article I will give an example of the use of the described architecture in the game “about elves”.
Technology selection
To implement the gameplay we conceived, we needed a server with a constant socket connection and a sufficiently short response time to the action of any user - no more than 50ms, not counting ping. The choice of technologies that made it possible to satisfy such requirements is not so great. At that time, our campaign already had experience in implementing a backend in C ++ for a non-gaming project, and therefore the choice was made in favor of C ++ precisely: we had both people and experience in this technology.
Perhaps Java (or some other technology) would be the best solution, but our team did not have a strong Java developer, not to mention an architect with experience in creating server solutions. In this situation, to hire new specialists, spend months and tens of thousands of dollars to check which is better, as well as to throw out working and tested C ++ code that we could easily reuse - all this went far beyond our budget and allotted time for development.
I find it difficult to answer whether the server came out in Java (or some other technology), but in C ++ we got exactly what we needed, besides for a sane time.
General server diagram
The server consists of the following modules (see Fig. 1).

- Ship contains data on the devices and current parameters of the ship, and also deals with the calculation of these parameters in accordance with the installed devices. This is the lowest level that all other server modules rely on.
- Space is a module for describing the world and objects in the world. At this level, the objects appear their coordinates, the current motion vectors, implemented the interaction of objects (processing shots, causing damage, etc.).
- AI is a module that implements AI mobs, as well as specific NPC skills.
- Quests - an implementation of the quest system.
- Main - contains all the functionality that is responsible for interacting with the user (sockets, streams, etc.), as well as functionality specific to the character (skills, buffs, achievements, crafting, etc.).
- Packets is an auto-generated module that contains wrappers for packages and implements an RPC client <-> server.
In this part of the article, we will look at the architecture of the Ship and Space modules. The architecture of the remaining modules will be discussed in the next part of this article.
Ship Module
Prototypes of objects and objects
This module operates with material objects of the "object" type. That is, with everything that can be put in the hold, thrown into space, bought in a store or transferred to another player. This module also considers the basic and derivative parameters of the ship from the installed devices.

In fig. 2 shows a class diagram (the diagram is simplified for greater clarity). You see the division of classes into two parts: the prototypes of objects below and the objects themselves above. Prototypes are completely static and featureless - they are loaded from the database, cannot be changed, and do not belong to anyone. And objects of objects (all descendants of ICargo), on the contrary, can be modified and contain a unique ID that allows you to identify a specific object and determine where it is located (hold, warehouse, container in space, store, etc.) . This approach adds flexibility and allows you to modify the functionality of objects without affecting other classes.
In our solution, most descendants of ICargo (or rather, all except TDevice and TShip) are just proxies for their prototypes. Then the question arises: were they really needed? It’s easier to create descendants of prototypes, with the addition of a unique ID for identification, and the end? No, not easier. But with this approach, firstly, we would still need two classes for the subject (prototype and descendant), and secondly, we would mix dynamic data with static data (because the prototypes are unchanged). On top of that, of course, the memory consumption and creation time of the item would increase, because it would be necessary to clone the prototype with all its fields. To confirm what I’ve said, I’ll give you an example: initially, there were no chips in the game, and when they appeared, then all the changes came down to adding a pair of TMicromodule / TMicromoduleProto classes with the addition of functionality for accounting for installed chips in TDevice. The TShip class, like all other classes, was not affected at all.
Calculation of ship and equipment parameters
in Star GhostsThere are many different types of devices (turrets, rocket launchers, radar, a camouflage system, a protective field, damage amplifiers, etc.). It would seem that for each of them it is necessary to make a stream class from TDevice and implement there specific functionality for this device. But let's take another look at the general server diagram and description of the Ship module: this module basically just provides the final calculated parameters of the ship to a higher level, while it does not perform the functions of objects. I will explain with an example. The TShip class contains the ScanningRange parameter - the radius of the radar - but it does not actually filter objects by range. And, most importantly, at the level of the Ship module, this filtering will not work, since objects have no coordinates in space. It's time to ask yourself: Does it make sense to create a couple of classes TRadarPrototype (as a descendant of TProtoBase) and TRadar (as a descendant of TDevice), a separate table in the database for this class and a page in the admin panel for only one ScanningRange field? The answer is obvious: the meaning of all these lines of code and classes is very doubtful. That is why we created one class TStaticParams, containingall parameters that any device in the game can have, as well as the TPrototypeMod class, which it can load from the TStaticParams database.
Of course, this is superfluous, but not very large: at the moment, the TStaticParams class contains only 34 fields of type int. But in return, we got some great goodies. Firstly, the ease of modification. Now you can create new types of devices and parameters without creating new classes. Secondly, the simplicity of the calculation of parameters. Simply add all the fields of the same name of all TStaticParams in the ship to get the final parameters! No virtual calls or downcasts - a simple operation "+ =" in a loop. Thirdly, we got game design flexibility. For example, we have a chip in the game that can be installed on any device, and it gives HP. Such a mechanism allows game designers to frolic as they want, while absolutely not jerking programmers for every little thing like "Rebka, write me a kaparik,
And that's not all. Since we have one class with parameters for any device, we were very able to implement randomization of parameters and sharpening. TStaticParams is an array, so in the admin panel the game designer can specify up to three parameters (indexes in the array) and the percentage of spread in these parameters when creating the device. When creating an item, TDevice primarily copies data from TPrototypeMod.TStaticParams to its instance of TStaticParams. Then he looks at the scatter indices and, if set, rolls the die and randomizes the parameters. The value of the cube is stored in the TDevice fields so that after loading from the database the parameters do not change. Sharpening is performed similarly: in the admin panel, the game designer indicates MainParam for the device. That is, the device knows the parameter index,
But there is one caveat when calculating the parameters of weapons: they can not simply be summed up with the parameters of other devices. A simple summation will lead to the fact that if you have more than one weapon installed, then you add up, including parameters such as WeaponRange of all guns on board, although this should not be. On the other hand, if it is an artifact that increases the range of a weapon, then we mustadd it to WeaponRange weapons. We solved this problem as follows: firstly, TStaticParams contains two arrays - general parameters that can always be folded safely (for example, HP, ScanningRange, etc.) and the so-called WeaponParams, which in general cannot be folded. And only if the device is not a weapon, its parameters must be added to the parameters of the weapon. It all looks like this:
void TShip::Recalc() {
m_xStatic.Set(0);
TDevice* dev = NULL;
for(unsigned i=0;iIsOnline() ) continue;
if( dev->IsWeapon() ) {
m_xStatic.AddDevice( dev->Static() );//типа HP там прибавать
} else {
m_xStatic.Add( dev->Static() );
}
}//for i
if(m_pStaticModifier) m_xStatic.Add( *m_pStaticModifier );// прибавим навыки пилота, бафы и прочее, что приходит сверху
// и вот тут ещё момент - нужно прибавить ко всему оружию параметры, которые висят на корпусе
for(unsigned i=0;iIsOnline() || !dev->IsWeapon() ) continue;
dev->SetWeapon( &m_xStatic );
}//for i
}
In the first cycle, we summarize all the parameters to the final parameters of the ship, but for weapons we add only general parameters, not weapons. Then we add the parameters of skills. And, at the very end, we give the weapon a pointer to TStaticParams from which it should add only weapon parameters.
Shot calculation
In addition to calculating the parameters of devices and checking for the possibility of installation in the slot, the TShip class performs another function - calculating the parameters of the shot. This is done by the SFireResult TShip :: Fire (NSlotPlace slot) method. This method checks the possibility of a shot (whether it’s a weapon at all, whether the device’s cooldown has ended, whether there are cartridges for a shot), calculates the damage dealt, the number of shots fired, and also rolls a dice on acceptable shot flags (such as critical hit). All parameters are written to the SFireResult structure, the device is put into cool-down, ammunition is written off, the result of the shot is returned. At the same time, TShip can neither check the range nor the parameters of the object they are shooting at (for example, if the object has protection and the damage needs to be reduced). This makes the top level of Space,
The remaining classes of the Ship module.
The TProtoBase class contains general data for any item, such as ImageID, Name, Level, etc.
ICargo contains a pointer to TProtoBase and proxies its data to the outside, and also provides a Factory for creating items. The TDeviceHandbook singleton class helps him in this, which loads all prototypes from the database and contains pointers to them.
The TCargoBay class is an object storage of type ICargo. He knows how to save his state in the database and provides a number of service functions such as: search for the nearest free slot, search for a compatible stackable item (for example, cartridges to combine with other cartridges), etc. Descendants from this class impose restrictions on the types of stored items (for example, only ships can be stored in a hangar, and everything except ships in a warehouse), and, if necessary, restrictions on the number of available storage cells.
The IShipNotifyReciver class is front-end and provides higher level connectivity. For example, sending to the Main level messages about the start of regeneration so that the corresponding packet can be sent to the client.
Space Module
This module operates with space objects (KO), such as spacecraft, asteroids, planets, etc. All KOs have current coordinates in space and a vector of their motion. The class diagram is shown in Fig. 3 (for greater clarity, the diagram is somewhat simplified).

Despite the algorithmic complexity, from an architectural point of view, this module is quite simple. All objects in space (ships, asteroids, planets, containers, stars) are descendants of TSpaceObject and are located in an object of type TSystem. TSpaceObject has the current coordinates, size, and two objects that control its behavior - this is FlyCommand (a descendant from ISpaceFlyTo) and Action (a descendant from ISpaceAction). FlyCommand calculates the current coordinates of the object and its current speed (at a given time). The calculation algorithm depends on the type of command: for moving in orbit, it is one, for linear movement of the other, for movement with smooth turns - the third. Action is responsible for more complex object movement algorithms. For example, TFollowShipAction pursues the specified goal. To do this, in each Update call, it checks whether the coordinates of the target have changed and if so, then replaces the FlyCommand command in Owner (with the specified new target coordinates). Introduction of Action made it possible to significantly simplify the creation of AI and avoid duplication of code, since the functionality implemented in Action is necessary for player ships and bots.
The presence of FlyCommand makes it easy to set the necessary type of motion for any object in space and transmit this command to the client in the form of coefficients of the equation of motion. This can significantly reduce the amount of data transmitted and simplify the implementation of new server-side behavior.
Dealing damage
The TSpaceObject class has two virtual methods - CorrectDamage and ApplyDamage, while the TSystem class has a DoDamage method. When an object wants to damage another object (for example, an asteroid hits another object), it tells TSystem this. The system calls CorrectDamage and, if the damage is not zero (for example, the planet is immune to any type of damage), then it sends a message about the damage “up” (to transmit to clients) and calls ApplyDamage so that the recipient performs specific actions (for example, the ship reduces HP and if HP is zero, the ship throws containers into space).
The TSpaceShip class contains the FireSlot method, which implements shooting with special abilities. It checks the allowable distance, then calls TShip :: Fire and, depending on the type of ability, performs further actions. For example, for MissileLauncher creates rockets.
The remaining classes of the Space module
The ISpaceShipNotifyReciver class is used in TSpaceShip to transmit messages like "I was attacked", "I am killed", "ready for hyper transition", etc. to the upper module.
The ISpaceSystemNotifyReciver class is used in TSystem to send upward messages about adding / removing space objects, about new FlyCommands and about causing damage.
The TGalaxy class is a singleton and contains a list of all TSystems in the Galaxy.
To be continued
In the next article in the series, we will consider the AI, Quest, Main modules, as well as some aspects of working with the database. And, of course, the promised adaptation for the game "about the elves."