Hello! It is always very interesting to me to read articles about someone else's real experience, and successfully passing through a placer rake or rake. Therefore, this article I want to start sharing my modest experience from the world of game development on a unit, as well as learn more about someone else's experience with a unit.
So, in November last year, our team began to make the client session session mmoshechka - ride on the machines, shoot the enemies. I must say that the team already had experience of not a successful project for a unit, it was a 3D race for VKontakte. So the theme of the machines in the unit was already familiar and it was planned to save on this. The very first thing with which it was decided to start is to make proof of the concept as quickly as possible - a demo of the game that shows the gameplay as accurately as possible. The purpose of this event is clear - as soon as possible to cut off all that does not fit into the game. In addition, it was also necessary to choose a server engine. With the client, everything was clear right away, Unity3d is our everything, but what to choose as a server engine? That is the question. I will dwell on this in more detail.
So, the following applicants were found:
1.
Photon2.
Smartfox3.
Built in Unity Network4.
ElektroServer5.
uLink6.
CrystalEngine')
We rejected the
built-in network in Unity immediately. Despite the fact that this solution provides support for physics on the server, which means it is a fully authoritarian server, in this case, in order to launch a new match-room, you need to raise a new Unity instance on the server, which is very expensive.
ElektroServer may be good at heart, at the time of selection, it seemed not as fast developing as the other applicants. In addition, if you look at their website, the vast majority of customers ordered the services of the studio that created ElektroServer, and those who ordered their server solution alone are a minority. Therefore, we also refused from ElektroServer.
uLink is an interesting thing, but at the moment it is quite new. Moreover, as far as I remember, uLink lives inside the unit, that the client, that the server, therefore again raises the question of server performance. In principle, the solution should be good if you do so that the players themselves can raise the servers for the matches, as in a counter, for example. In appearance, all sorts of buns in uLink are much more than in the network built into the unit.
CrystalEngine is a young, developing engine. Of the benefits - at the time of creating the demo for preproduction, our team was the main developer of this engine. Of the minuses, the engine is too young, there is a lack of documentation and a lack of a large community. Therefore, this engine decided not to be laid.
So we have 2 finalists left:
Photon vs
smartfoxUnfortunately, there was no time to test both solutions during preproduction. Both engines inspire respect, have released games, documentation and live community. So, the key role in the choice of the engine was played by the fact that I was already familiar with the smartphone and knew it only from the good side. Thoughtful, easy to understand API, a huge number of running projects, work on Linux. In general, Smartfoks won. We even sketched a demo to test the server, but only ~ 700 clients were able to start from the office, after which the office network was already beginning to die, and the server was barely aware of the load. In this regard, I have a question - did anyone do load testing of game servers? If so, how was it tested?
Well, let's move on. At the beginning of the creation of demo programmers in the team were three: I, the main server, and widely known in the narrow circles of Neodrop (the founder of the Russian Internet community on Unity3d and the site
3d3d.ru ). After Smartfox was selected as a server engine, our server operator left. So I started to pretend to be a server programmer.
For a month and a half of preproduction, it turned out to release a demo in which:
1. There is a garage with common chatikom.
2. You can run the battle on 2 teams.
3. Each player rides the 1st of 4 machines:
a scout, a heavy armored car, artillery, or an engineer. All of this cheerfully drove, flipped over, shot and killed each other. Oh yeah, there was also a system of visibility like the one in World of Tanks, but not so cool, of course.
Looking ahead, it is worth noting the following fact - the creation of such a demo took less than a month and a half of pure time. Later, when we decided to rewrite everything from scratch for production, so that the code was cleaner, and in general all the necessary features became possible, such as changing modules and equipment, we could only reach the level of playability of such a demo in 2-3 months. Moreover, the program worked no longer 2 people, but 6-7. It happened due to the fact that in our demo it was impossible to change modules and equipment in runtime, and this possibility should be in the final product. So it turns out that one feature can increase labor costs at times. (Although, of course, not only is she alone. In the final product, all that can and cannot be managed through the admin area is, and this is also the fun one.)
Something turned out to hurt a lot of lyrics, try to add a little more technical details. During the project, a fairly decent number of problems and issues emerged that are of little relevance or practically irrelevant for small projects, but in larger projects it is necessary to solve them so that work does not slow down.
The very first problem we encountered was the protocol of communication between the client and the server. In the smartphone everything is arranged as follows:
1. The client code subscribes to a type event.
smartFox.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse);
2. By calling a command from the server, all subscribers on the client are jerked to the method to which parameters are transmitted from the server. Parameters from the server are transmitted as a string-name of the command and a certain
SFSObject
, which is an analogue of the hash table and contains all the data that the server sent us to the client.
Therefore, in order to execute a command from the server, we have to subscribe to server messages in the right place, and check whether the command is right for us or not, and if it is the right command, get all the necessary data from
SFSObject
and finally execute the command . Naturally, every time I didn’t want to do this with pens. The simplest thing that came to mind at once was to sign one game controller for this event, and it’s time to create an event that explicitly contains a single command, separate parameters. It turned out something like:
public class SFSExtensionsController : MonoBehaviour { public delegate void ExtensionResponceDelegate(string cmd, SFSObject parameters); public static event ExtensionResponceDelegate onExtensionResponce = null; void Start() { smartFox.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse); } static void OnExtensionResponse(BaseEvent evt) { string cmd = (string)evt.Params["cmd"]; SFSObject sfsParameters = (SFSObject)evt.Params["params"]; if (onExtensionResponce != null) { onExtensionResponce(cmd, sfsParameters); } } }
Not bad for a start, but the problem with parsing the parameters in each receiver still remains. Therefore, the next thing that has been done is that controllers that subscribe to our
onExtensionResponce
event are
onExtensionResponce
intercept a logically solid piece of commands inside themselves, for example, those responsible only for moving, or only shooting, and give out events with data ready for game logic.
There are advantages to such a solution - inside the game logic it is not necessary every time to check
SFSObject
for the presence of the required fields. But there are enough minuses - if the event is narrowly specialized and the subscriber will be only one - a whole class for the sake of this does not feel like city. Moreover, when there will be a lot of such controllers - how to find the one in which the necessary event is located? And also, do not forget that there is no automatic serializer in
SFSObject
, and so far each class has to be serialized manually, prescribing the necessary code both on the client and on the server. (Yes, there are methods in the smart phone that should serialize arbitrary classes in
SFSObject
, but we didn’t take off this functionality, but there was no time to
SFSObject
it out.) It is also necessary to maintain the identity of the string command names on the client and on the server. God forbid, someone is sealed somewhere. So it turns out that despite the power and reasonableness of Smartfox, it’s just impossible to take it and use it for your own pleasure in a project in which more than one person is engaged.
But ... with all its flaws, the system described above performed its task and the demo worked perfectly on it. However, this place was the very first to rework in the production stage. There were several tasks to be solved:
1. The identity of the commands in the data exchange protocol, and also the structures of
SFSObject
that are
SFSObject
between the client and the server.
2. Automatic serialization / deserialization of game logic data.
3. Convenient sending / receiving messages from the classes that serve the game logic.
Let's go out of order.
Point two: automatic serialization / deserialization. It would seem that it could be trivial - use reflexion, write down all the necessary additional information, such as the name of the class,
SFSObject
will be happiness in
SFSObject
. But our happiness in this case will not be complete at all, because it is a real-time toy, with a large flow of constantly changing data, such as the position of the machine, the speed, the turn of the tower, etc. etc. Many of these data are sent between the server and the client several times per second for each player, and 32 players are planned for a room, and there are at least 50 such rooms for the server so that the project does not go bust, so the solution does not work. And here we got the following idea. The code for packing / unpacking game data in
SFSObject
is trivial, and even the program can write this code for us. In total, a utility was written that accepts an xml file in the specified format as input, and produces two source files at the output, one in C # for Unity, the second in java for Smartfox. The resulting files contain classes, with two methods
SFSObject ToSFSObject()
and
void FromSFSObject(SFSObject sfsObject)
.
An example of a class generated programmatically for C #
public class StartMultipleArtilleryShootProxy : ISFSSerializable { public int userId { get; set; } public long barrelId { get; set; } public long projectile { get; set; } public Vec3fProxy position { get; set; } public List<Vec3fProxy> direction { get; set; } public float startSpeed { get; set; }
An entry about this class in an xml file:
<StartMultipleArtilleryShootProxy userId-ui="int" barrelId-bi="long" projectile-pr="long" position-p="Vec3fProxy" direction-d="Vec3fProxy array" startSpeed-s="float"/>
When changing the protocol, we change the xml file, run the utility, we get the code that can quickly serialize / deserialize
SFSObject
into the classes we need. We replace the file on the client and on the server and that's it, it's ready!
Point one would be logical to solve, developing the format of ixmelka, and storing there not only the data that will be transmitted between the client and the server, but also the names of the commands. But ... the timeline is always burning, there is no time to do such things at the moment. So the problem with typos in the names of the teams was partially solved in the process of solving paragraph 3.
The idea of the central controller to receive messages from the server was not so bad, so it was decided to leave this controller. In addition, it was decided to create a small class for each event from the server, which knows exactly which command it processes and which data it gives to the outside. We can stuff all such classes in one place, it's nice to name them and initialize them at the start of the application. Thus, client logic does not need to know anything about all sorts of smartphones, it’s enough to contact the command store:
SFSProtocol.Protocol.BATTLE.HitRivalRequest.onGetResponce += OnGetHitRivalResponce;
and the event handler, in turn, accepts the already strictly typed data necessary for the game logic:
void OnGetHitRivalResponce(HitResultProxy hitResultProxy) {
And now a little bit of magic, which is based on the fact that Unity promotes single-threaded code within itself, and events from the server are also invoked in one thread. Thanks to this single-threading, we can check in our only controller, which communicates directly with the smartphone, whether someone has processed the command from the server or not, and if no one has processed, then immediately scream about it in the logs. It looks like a trivial simple:
cmdWasCatched = false; if (onExtensionResponce != null) { onExtensionResponce(cmd, sfsParameters); } if (!cmdWasCatched) { Debug.LogError(string.Format("SFSExtensionsController.OnExtensionResponse cmd={0} was not catched!", cmd)); }
In addition, such an architecture allowed to do absolutely necessary things for client-server communication: checking the timeout for a response from the server, adding additional service fields, in addition to the game logic data (for example, the flag if the request from the client was successful or not, and if unsuccessful - description errors from the server.)
At the end of the article it is worth mentioning one more rake that we stepped on. To facilitate the creation of new commands in the protocol, a parameterizable class was made, in which we can specify the type of data that should be sent to the server, the type of data that should be received from the server, and the designer. The code that deserialized objects inside this class looked like this:
public class SFSRequest<TReqData, TRespData> : DataTransporter where TReqData : ISFSSerializable, new() where TRespData : ISFSSerializable, new() { ….......... protected override void Execute(Sfs2X.Entities.Data.SFSObject sfsParameters) { responceData = new TRespData(); responceData.FromSFSObject(sfsParameters); OnGetResponce(); } }
And everything seems to be great, but there is one big but: in this case, the constructor
new TRespData()
will use reflection. In the end, with which they fought, they ran into it: reflexion was still used, and already with 8 players in battle, braking was noticeable. To remedy the situation, 2 fields for delegates were added to the team’s class
TRespData
; they do the only thing — create
TRespData
and
TReqData
. So, the base class handler began to look like this:
protected override void Execute(Sfs2X.Entities.Data.SFSObject sfsParameters) { responceData = respDataConstructor(); responceData.FromSFSObject(sfsParameters); onGetResponce(); }
And the final class for a specific command is:
public class MultipleArtilleryShootStartRequest : SFSRequest<StartMultipleArtilleryShootProxy, StartMultipleArtilleryShootProxy> { public MultipleArtilleryShootStartRequest(string sendCommand, string receiveCommand, Func<StartMultipleArtilleryShootProxy> reqDataConstructor, Func<StartMultipleArtilleryShootProxy> respDataConstructor) : base(sendCommand, receiveCommand, reqDataConstructor, respDataConstructor) { } }
By the way, an article with a habr helped me to find this joint, a link to which I unfortunately lost.
By such, not very complicated methods, it turned out to make a fairly convenient client-server communication. If someone has questions or comments - well in the comments. For those who have read the article to the end, a small bun - a
pivot table of parameters for game engines, dug up on the Internet .