📜 ⬆️ ⬇️

Development of MMO RPG - a practical guide. Server (Part 1)

Turret annihilator
Today we will continue to get acquainted with the development and design of online games on the example of the space MMO RPG "Star Ghosts" . In this article we will talk about backend'e in C ++ and it will be thoroughly technical.

There will be a lot of references to the Star Ghost functionality in the text, but I will try to expound 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 the article we will focus precisely on architectural solutions in relation to the backend of MMO RPG in real time. The source code will not be much and it definitely will not contain such C ++ specific things as multiple inheritance or patterns. The task of this article is to help in designing the game server and to acquaint everyone with the specifics of the game backend.
')
The solutions described are quite versatile and are quite suitable for many RPGs. As an illustration, at the end of the article I will give an example of using the described architecture in the game "about elves".

Technology selection


In order to implement the gameplay we had planned, a server with a permanent socket connection and a sufficiently short response time to any user action was needed - no more than 50ms, not counting ping. The choice of technologies that allowed to meet such requirements is not so great. At that time, our campaign already had experience in implementing C ++ backend for a non-game project, and therefore the choice was made in favor of C ++: 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, let alone an architect with experience in creating server solutions. In such a situation, hiring new specialists, spending months and tens of thousands of dollars to check which is better, as well as throw out working and tested C ++ code that we could easily reuse - all this went far beyond our budget and allotted development time.

I find it difficult to answer whatever server came out in Java (or some other technology), but in C ++ we got exactly what we needed, moreover, in reasonable terms.

General scheme of the server


The server consists of the following modules (see Figure 1).

General scheme of the 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


Prototype items and items
This module operates with material objects of the type "object". That is, with everything that can be put in the hold, thrown into space, buy in a store or transfer to another player. Also, this module considers the basic and derived from the installed devices ship parameters.

Ship module class diagram

In fig. 2 shows a class diagram (the diagram is simplified for greater clarity). You see the division of classes into two parts: below are the prototypes of objects, and at the top - the objects themselves. Prototypes are completely static and faceless - they are loaded from the database, cannot change and do not belong to anyone. Objects of objects (all descendants from 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 (hold, warehouse, container in space, shop, etc.) . This approach adds flexibility and allows you to modify the functionality of objects without affecting other classes.

In our solution, the majority of descendants of ICargo (or rather, all but TDevice and TShip) are simply proxies for their prototypes. Then the question arises: were they really needed? After all, is it easier to create descendants of prototypes, with the addition of a unique ID for identification, and that’s the end of it? No, not easier. But with this approach, firstly, we would still need two classes for the subject (prototype and descendant), and secondly, we would have mixed dynamic data with static data (after all, the prototypes are unchanged). On top of that, of course, the memory consumption and the creation time of the item would increase, because it would be necessary to clone the prototype with all its fields. In confirmation of this, I will give the following example: initially we did not have chips in the game, and when they appeared, all the changes were reduced 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 parameters of the ship and equipment
In "Star Ghosts" there are many different types of devices (turrets, rocket launchers, radar, masking system, protective field, damage amplifiers, etc.). It would seem that for each of them it is necessary to make a class flow from TDevice and implement specific functionality for this device there. But let's take another look at the general scheme of the server and the description of the Ship module: this module basically simply provides the final design parameters of the ship to a higher level, while it does not perform the function of objects. Let me explain by example. The TShip class contains the ScanningRange parameter — the radius of the radar operation — but it does not actually filter objects in range. And, most importantly, at the level of the Ship module, this filtering will not work, since the 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 from TProtoBase) and TRadar (as a descendant from TDevice), a separate table in the database for this class and a page in the admin just for the sake of one ScanningRange field? The answer is obvious: the meaning of all these lines of code and classes is very dubious. That is why we created one TStaticParams class, containing all the parameters that any device in the game can have, as well as the TPrototypeMod class, which can be loaded from the TStaticParams database.

Of course, this is overkill, but not very large: at the moment the TStaticParams class contains only 34 fields of type int. But in return, we received several excellent buns. First, the ease of modification. Now you can create new types of devices and parameters without creating new classes. Secondly, the ease of counting 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, which can be installed in any device, and it gives HP. Such a mechanism allows game designers to frolic as they please, without at the same time tugging at each detail of the programmers like “children, add me a kaparik here so that I can set a dodge bonus in the disguise device”.

And that is not all. Since we have one class with parameters for any device, we very easily managed to implement parameter randomization and sharpening. TStaticParams is an array, so when creating a device, the game designer in the admin panel can specify up to three parameters (indices in the array), and the percentage of variation in these parameters. When creating an item, TDevice first copies the data from TPrototypeMod.TStaticParams into its TStaticParams instance. Then he scans the scatter indices, and if they are 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 done in the same way: in the admin area, game designer specifies MainParam for the device. That is, the device knows the parameter index, which must be increased by + 10% for each successful sharpening.
But there is one caveat in calculating the parameters of the weapon: they cannot simply be summed up with the parameters of the other devices. A simple summation will lead to the fact that if you have more than one weapon installed, then you add up, including such parameters as the WeaponRange of all the guns on board, although this should not be so. On the other hand, if it is an artifact that increases the radius of the weapon, then we must add it to the WeaponRange of the weapon. We solved this problem as follows: first, TStaticParams contains two arrays — common parameters that can always be added safely (for example, HP, ScanningRange, etc.) and so-called WeaponParams, which generally cannot be added. And only if the device is not a weapon, its parameters must be added to the parameters of the weapon. It looks like this:

void TShip::Recalc() {       m_xStatic.Set(0);       TDevice* dev = NULL;       for(unsigned i=0;i<m_vSlots.size();i++) {             dev = m_vSlots[i].Device();             if( !dev || !dev->IsOnline() ) 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;i<m_vSlots.size();i++) {             dev = m_vSlots[i].Device();             if( !dev || !dev->IsOnline() || !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 the weapon we add only the general parameters, not the weapon ones. Then we add the parameters of skills. And, at the very end, we give the weapon a pointer to the TStaticParams from which it should add only the weapon parameters.

Shot calculation
In addition to calculating device parameters and checking whether it can be installed in a slot, the TShip class performs another function — the calculation of shot parameters. The SFireResult TShip :: Fire (NSlotPlace slot) method does this. This method checks the possibility of a shot (whether it is a weapon at all, whether the device’s cooldown has ended, whether there are shot cartridges), calculates the damage inflicted, the number of shots fired, and also rolls the die for acceptable shot flags (such as critical damage). All parameters are recorded in the SFireResult structure, the device is placed in cool down, ammunition is written off, the result of the shot is returned. At the same time, TShip cannot check either the range or the parameters of the object being fired at (for example, if the object has protection and the damage needs to be reduced). This makes the upper level Space, which has all the necessary data.

Other classes of the Ship module
The TProtoBase class contains general data for any subject, such as ImageID, Name, Level, etc.

ICargo contains a pointer to TProtoBase and proxies its data to the outside, and also provides the Factory for creating objects. This is helped by the TDeviceHandbook singleton class, which loads all the prototypes from the database and contains pointers to them.

The TCargoBay class is a repository of ICargo objects. It is able to maintain its state in the database and provides a number of service functions such as: searching for the nearest free slot, searching for a compatible piece of paper (for example, cartridges to combine with other cartridges), etc. The descendants of this class impose restrictions on the types of stored items (for example, only ships can be stored in the hangar, and all but ships are in stock), and, if necessary, limits on the number of available storage cells.
The IShipNotifyReciver class is an interface and provides a link to a higher level. For example, sending a message on the start of regeneration to the Main level so that you can send the corresponding packet 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 their motion vector. The class diagram is shown in Figure 3 (for greater clarity, the diagram is somewhat simplified).

Class diagram of the Space module

Despite the algorithmic complexity, from an architectural point of view, this module is quite simple. All objects in space (ships, asteroids, planets, containers, star) are descendants of a TSpaceObject and are in an object of type TSystem. TSpaceObject has current coordinates, size and two objects that control its behavior - this is FlyCommand (descendant from ISpaceFlyTo) and Action (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 movement in orbit it is one, for linear movement another, for movement with smooth turns - the third. Action is responsible for more complex algorithms for moving an object. For example, TFollowShipAction performs the pursuit of a specified goal. To do this, in each call Update checks if the coordinates of the target have changed and, if so, replaces the FlyCommand command in the Owner (with the specified new coordinates of the target). Introduction of Action allowed to significantly simplify the creation of AI and avoid duplication of code, since the functionality implemented in Action is necessary for players' ships and bots.

The presence of FlyCommand makes it easy to specify the required type of motion for any object in space and transfer this command to the client in the form of motion equation coefficients. This allows you to significantly reduce the amount of transmitted data and simplify the implementation of new behavior on the server side.

Causing damage
The TSpaceObject class has two virtual methods, CorrectDamage and ApplyDamage, and the TSystem class has a DoDamage method. When an object wants to cause damage to another object (for example, an asteroid hit another object), it reports this to the TSystem. The system calls CorrectDamage and, if the damage is not zero (for example, the planet is immune to any kind of damage), then it sends a message about the damage “up” (to transfer 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 special shooting. 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.

Other classes of the Space module
The ISpaceShipNotifyReciver class is used in TSpaceShip to send messages like “I was attacked”, “I am killed”, “ready for hyperjunction”, etc. to the upper module.

The ISpaceSystemNotifyReciver class is used in the TSystem to send upward messages about the addition / removal of space objects, new FlyCommands, and damage.

The TGalaxy class is a singleton and contains a list of all TSystem in the Galaxy.

To be continued


In the next article of the cycle we will consider the modules AI, Quest, Main, as well as some aspects of working with the database. And, of course, the promised adaptation for the game "about elves".

Source: https://habr.com/ru/post/257985/


All Articles