Architectural solutions for mobile games. Part 2: Command and their queues



    In the first part of the article, we looked at how the model should be arranged in order to use it was easy, and debugging it and attaching interfaces to it is simple. In this part, we will consider the return of commands to changes in the model, in all its beauty and diversity. As before, the priority for us will be the convenience of debugging, minimizing the gestures that the programmer needs to do in order to create a new feature, as well as the readability of the code for a person.

    Architectural solutions for mobile games. Part 1: Model
    Architectural solutions for mobile games. Part 3: View on jet propulsion

    Why Command


    The Command pattern sounds loud, and in fact it is just an object into which everything necessary for the requested operation is stored and stored. We choose such an approach, at least because our teams will be sent over the network, and we will get several copies of the game-steate for ourselves, for business purposes. So when a user clicks a button, an instance of the command class is created and sent to the recipient. The meaning of the letter C in the MVC abbreviation is somewhat different here.

    Prediction of results and verification of commands over the network


    In this case, the specific code is less important than the idea. And here’s the idea:

    A self-respecting game cannot wait for a response from the server before responding to a button. Of course, the Internet is getting better and you may have a cloud of servers around the world, and I know even a couple of successful games waiting for a response from the server, one of them is even Summoning Wars, but still you don’t need to do so. Because for the mobile Internet, lag of 5-15 seconds is more the norm than the exception, in Moscow at least, the game should be really great so that the players do not pay attention to it.

    Accordingly, we have a gamestate that represents all the information required by the interface, and commands are applied to it immediately, and only after that are sent to the server. Usually hard-working java-programmers are sitting on the server, duplicating all the new functionality one-on-one in another language. On our “deer” project, their number reached 3 people, and the mistakes made during porting were a constant source of subtle joy. Instead, we can do differently. We launch on the .Net server and run the same command code on the server side as on the client.

    The model described in the last article gives us a new interesting opportunity for self-testing. After executing the command on the client, we will calculate the hash of the change in the GameState tree, and apply it to the command. If the server executes the same command code, and the hash of the changes that have occurred does not match, then something went wrong.

    First benefits:

    • Such a solution greatly accelerates the development and minimizes the number of server programmers.
    • If the programmer made errors leading to non-deterministic behavior, for example, got the first value from the Dictionary, or used DateTime.now, and generally used some values ​​not written in the command fields explicitly, then at the start of the hash server will not match, and we will find out about it.
    • Development of the client can be for the time being without a server at all. You can even go to friendly alpha without having a server. This is useful not only for indie developers who play the game of their dreams at night. When I was in Pixonik, there was a case when the server programmer wiped out all the polymers, and our game was forced to go through pre-moderation, having a gag instead of a server stupidly playing the entire gamestate once in a while.

    The disadvantage that for some reason is systematically underestimated:

    • If the client programmer did something wrong and it is imperceptible to testing, for example, the likelihood of goods in mysteryboxes, then there is no one to write the same thing the second time and find an error. Autoported code requires a much more responsible attitude to testing.

    Detailed debug information


    One of our stated priorities is the convenience of debugging. If in the process of executing a command we caught an exception, everything is clear, we roll back the gamestate, send full status to the logs and serialize the dropped command to it, everything is convenient and fine. The situation is more complicated if we have out of sync with the server. Because several other teams have already been executed on the client since, and it turns out that it is not easy to find out what state the model was before executing the team that led to the disaster, but really want to. Cloning the gamestate in front of each team is too difficult and expensive. To solve the problem, let's complicate the scheme sewn under the engine hood.

    In the client, we will have not one gamestate, but two. The first one serves as the main interface rendering interface, commands are applied to it immediately. After that, the applied commands appear in the send queue to the server. The server performs the same action on its side, and confirms that everything is good and correct. After receiving the confirmation, the client takes the same command and applies it to the second gamestate, bringing it to the state that has already been confirmed by the server as correct. At the same time, we also have the opportunity to compare the hash of the changes made to insure, and we can also compare the full hash of the entire tree on the client, which we can calculate after executing the command, it weighs little and is considered fast enough. If the server does not say that everything is fine, it requests the client details of what happened,
    The solution looks very attractive, but it gives rise to two problems that need to be solved at the code level:

    • Among the command parameters there may be not only simple types, but also references to models. In another gamestate at the exact same place are other objects of the model. We solve this problem in the following way: Before the command is executed on the client, we serialize all its data. Among them may be links to models, which we write in the form of Path to the model from the root of the gamestat. We do this before the team, because after its implementation, the paths may change. Then we send this path to the server, and the server gamestart will be able to get a link to its model on the way. Similarly, when a team is applied to a second gamestate model can be obtained from the second gamestate.
    • In addition to elementary types and models, the team may have links to collections. Dictionary <key, Model>, Dictionary <Model, key>, List <Model>, List <Value>. For all, they will have to write serializers. However, you can not rush to this, in a real project such fields occur surprisingly rarely.
    • Sending commands to the server one by one is not a good idea, because the user can produce them faster than the Internet can carry them back and forth, the pool of commands that are not worked out by the server will grow on a bad Internet. Instead of sending commands one by one, we will send them in batches of several pieces. In this case, having received a response from the server that something went wrong, you will first need to apply to the second state all previous commands from the same package that were confirmed by the server, and only then store and send a control second state to the server.

    Convenience and ease of writing commands


    The command execution code is the second in size and the first in responsibility by the code in the game. The simpler and clearer it will be, and the less a programmer needs to make too much hands to write it, the faster the code will be written, the fewer mistakes made and, very unexpectedly, the happier the programmer will be. I place the execution code directly in the team itself, except for the common pieces and functions that are located in separate static classes of rules, most often in the form of extensions to the classes of models with which they work. I will show a couple of examples of commands from my pet project, one very simple and the other a little more complicated:

    namespace HexKingdoms {
    	publicclassFCSetSideCostCommand : HexKingdomsCommand { // Выставить на какую сумму имеет право закупаться каждая из участвующих в битве сторонprotected override bool DetaliedLog { get { returntrue; } }
    		public FCMatchModel match;
    		publicint newCost;
    		protected override voidHexApply(HexKingdomsRoot root){
    			match.sideCost = newCost;
    			match.CalculateAssignments();
    			match.CalculateNextUnassignedPlayer();
    		}
    	}
    }

    And this is how the log looks like, which is left behind by this command, unless the log is turned off.

    [FCSetSideCostCommand id=1 match=FCMatchModel[0] newCost=260] Execute:00:00:00.0027546 Apply:00:00:00.0008689
    {	"LOCAL_PERSISTENTS":{
    		"@changed":{
    			"0":{"SIDE_COST":260},
    			"1":{"POSSIBLE_COST":260},
    			"2":{"POSSIBLE_COST":260}}}}

    The first time, which is indicated in the log, is the time during which all necessary changes in the model were made, and the second time, during which all changes were worked out by the interface controllers. This should be shown in the log in order not to accidentally do something terribly slow, or to notice in time if operations start to take too much time simply because of the size of the model itself.

    Apart from appeals to Persistent-objects by Id-Schnick, greatly reducing the readability of the log, which, by the way, could have been avoided here, and the command code itself, and the log that he did with the gamestate is surprisingly clear. Note that in the command text the programmer does not make any unnecessary movement. Everything you need is done by the engine under the hood.

    Now look at the example of a larger team.

    namespace HexKingdoms {
    	publicclassFCSetUnitForPlayerCommand : HexKingdomsCommand { // Игрок выбирает себе количество одного из доступных для стартовой закупки юнитовprotected override bool DetaliedLog { get { returntrue; } }
    		public FCSelectArmyScreenModel screen;
    		publicstring unit;
    		publicint count;
    		protected override voidHexApply(HexKingdomsRoot root){
    			if (count == 0 && screen.player.units.ContainsKey(unit)) {
    				screen.player.units.Remove(unit);
    				screen.selectedUnits.Remove(unit);
    			} elseif (count != 0) {
    				if (screen.player.units.ContainsKey(unit)) {
    					screen.player.units[unit] = count;
    					screen.selectedUnits[unit].count = count;
    				} else {
    					screen.player.units.Add(unit, count);
    					screen.selectedUnits[unit] = new ReferenceUnitModel() { type = unit, count = count };
    				}
    			}
    			screen.SetSelectedReferenceUnits();
    			screen.player.CalculateUnitsCost();
    			var side = screen.match.sides[screen.side];
    			screen.match.CalculatePlayerAssignmentsAcceptablity(side);
    			screen.match.CalculateNextUnassignedPlayer(screen.player);
    		}
    	}
    }

    And here is the log that the command left behind:

    [FCSetUnitForPlayerCommand id=3 screen=/UI_SCREENS[main] unit=militia count=1] Execute:00:00:00.0065625 Apply:00:00:00.0004573
    {	"LOCAL_PERSISTENTS":{
    		"@changed":{
    			"2":{
    				"UNITS":{
    					"@set":{"militia":1}},
    				"ASSIGNED":7}}},
    	"UI_SCREENS":{
    		"@changed":{
    			"main":{
    				"SELECTED_UNITS":{
    					"@set":{
    						"militia":{"@new":null, "TYPE":"militia", "REMARK":null, "COUNT":1, "SELECTED":false, "DISABLED":false, "HIGHLIGHT_GREEN":false, "HIGHLIGHT_RED":false, "BUTTON_ENABLED":false}}}}}}}

    As they say, so much clearer. Take the time to provide the team with a convenient, compact and informative log. This is the key to your happiness. The model must work very quickly, so there we used a variety of tricks with the methods of storage and access to the fields. Commands are executed in the worst case, once per frame, in fact, several times less, therefore we will do serialization and deserialization of the command fields, just through reflex. Just sort the fields by name so that the order is fixed, well, we will compile the list of fields once in a team’s life, and read-write with native C # methods.

    Model information for the interface.


    Let's take the next step in complicating our engine, a step that looks scary, but greatly simplifies the writing and debugging of interfaces. Very often, especially in the related MVP pattern, the model contains only business logic controlled by the server, and information about the state of the interface is stored inside the presenter. For example, do you want to book five tickets. You have already chosen their number, but have not yet clicked the "order" button. Information about exactly how many tickets you have chosen in the mold can be stored somewhere in the secret corners of the class that serves as a gasket between the model and its display. Or, for example, a player switches from one screen to another, and nothing changes in the model, and where the tragedy happened when the programmer involved in debugging knew only from the words of an extremely disciplined tester. The approach is simple clear, almost always used and slightly malicious, in my opinion. Because if something went wrong, the state of this Presenter, which led to an error, is absolutely no way to know. Especially if the error occurred on the combat server during the operation on $ 1000, and not at the tester in controlled and reproducible conditions.

    Instead of this usual approach, we prohibit someone other than the model to contain information about the state of the interface. This has, as usual, the advantages and disadvantages that one has to contend with.

    • (+1) The most important advantage that saves people months of programming is that if something went wrong, the programmer simply loads the gamestate before the accident and receives exactly the same state not only of the business model, but of the entire interface, up to the very last button on the screen.
    • (+2) If some command has changed something in the interface, the programmer can easily go to the log and see what exactly has changed in a convenient json form, as in the previous section.
    • (-1) A lot of extra information appears in the model, which is not needed to understand the business logic of the game and does not need the server two times.

    To solve this problem we will mark some fields as notServerVerified, it looks like this, for example:

    public EDictionary<string, UIStateModel> uiScreens { get { return UI_SCREENS.Get(this); } }
    publicstatic PDictionaryModel<string, UIStateModel> UI_SCREENS = new PDictionaryModel<string, UIStateModel>() { notServerVerified = true };

    This part of the model and everything below it will relate exclusively to the client.

    If you still remember, the flags of what is to be exported and what is not are as follows:

    [Flags]
    publicenum ExportMode {
    	all = 0x0,
    	changes = 0x1,
    	serverVerified = 0x2
    }

    Accordingly, when exporting or calculating the hash, you can specify whether to export the entire tree or only its part that is checked by the server.

    The first obvious complication that results from this is the need to create separate commands that need to be checked by the server and those that are not needed, but there are also those that need to be checked not entirely. In order not to load the programmer with unnecessary operations for setting up the command, we will again try to do everything necessary for the engine hood.

    public partial classCommand {/** <summary> Здесь делаем только изменения, затрагивающие проверяемую сервером часть модели </summary> */publicvirtualvoidApply(ModelRoot root){}
    	/** <summary> Здесь делаем только изменения остающиеся исключительно на клиенте </summary> */publicvirtualvoidApplyClientSide(ModelRoot root){}
    }

    A programmer creating a command can override one or both of these functions. All this, of course, is wonderful, but how to make sure that the programmer did not mess up anything, and if he messed up something, how can he be helped to fix it quickly and easily? There are two ways. I applied the first one, but you might like the second one more.

    First way


    We use the cool properties of our model:

    1. The engine calls the first function, after which it receives a hash of changes in the server part of the gamestay that is being checked. If there are no changes, then we are dealing with an exclusively client team.
    2. We get the model hash changes in the entire model, not only the server-checked. If it differs from the previous hash, then the programmer has nakosyachil, and changed something in the part of the model that is not checked by the server. We go around the state tree and drop out to the programmer in the form of a full list of notServerVerified = true fields and lying below the tree, which he changed.
    3. Call the second function. We get from the model hash the changes that occurred in the checked part. If it does not coincide with the hash after the first call, then in the second function the programmer has done everything. If we want to get a very informative log in this case, we roll back the entire model to its original state, serialize it to a file, the programmer will then need it for debugging, then we clone it entirely (two lines - serialization-deserialization), and now we apply the first to the clone function, then we fix the changes so that the model looks unchanged, after which we apply the second function. And then we export all the changes in the part checked by the server as JSON, and we include it into the abusive exceptions so that the shy programmer can immediately see what and where he changed, which should not be changed.

    It looks, of course, scary, but in fact it’s 7 lines, because the functions that do this here (except for traversing the tree from the second paragraph) are ready. And since this is an exception, we can not afford to act optimally.

    Second way


    Slightly more brutal, we now have one lock field in ModelRoot, but we can divide it into two, one will only lock the server the fields being checked, the other is not the server being checked. In this case, the programmer who has done something wrong will get about this exception immediately with the ration to the place where he did it. The only drawback of this approach is that if in our tree one model property is marked as not verifiable, then everything in the tree is located below it about counting hashes and controlling changes will not be examined, even if each field is not marked. And Lok, of course, will not look into the hierarchy, which means you will have to mark all the fields of the unchecked part of the tree, and you will not be able to use the same classes in the UI and the usual part of the tree. As an option, such a construction is possible (I will write it down simply):

    publicclassGameState : Model {
        public RootModelData data;
        public RootModelLocal local;
    }
    publicclassRootModel {publicbool locked { get; }
    }

    Then it turns out that each subtree has its own lok. GameState inherits the model, because it's easier than to invent for it a separate implementation of all the same functionality.

    Necessary improvements


    Of course, the manager who is responsible for processing commands will have to add new functionality. The essence of the changes will be that not all commands will be sent to the server, but only those that create the checked changes. The server on its side will not raise the entire gamestate tree, but only the part to be checked, and accordingly the hash will match only for the checked part of the state. When executing a command, only the first of the two functions of the command will be launched on the server, and when resolving references to models in the game console, if the path goes to an unchecked part of the tree, null will be placed in the command variable instead of the model. All not sent teams will honestly stand in line along with the usual ones, but at the same time be considered as already confirmed.

    There is nothing fundamentally difficult to implement. Simply, the property of each field of the model has another condition, tree traversal.

    Another necessary improvement is that you need separate Factory for ParsistentModel in the checked and untested part of the tree and NextFreeId will be different for them.

    Commands initiated by the server


    There is some problem if the server wants to push its team to the client, because the client state relative to the server state could already have gone a few steps ahead. The basic idea is that if the server needed to send its team, it sends a server notification to the client with the next answer, and writes it to itself in the field for notifications sent to this client. The client receives a notification, forms a command on its basis and puts it at the end of its turn, after those who managed to execute on the client, but have not reached the server yet. After some time, the command is sent to the server as part of the normal process of working with the model. Having received this command for processing, the server throws the notification out of the outgoing queue. If the client has not responded to the notification within the set time, with the next package, the command will be restarted. If the client received the notification has fallen off, connects later or else for some reason loads the game, the server, before giving it, the state turns all notifications into commands, executes them on its side, and only after that gives the joining client its new status. Please note that there may be a conflict state of the player with negative resources when the player managed to spend the money exactly at the moment when the server took it away from him. The match is unlikely, but with a large DAU it is almost inevitable. Therefore, the interface and game rules should not fall to death in such a situation. connects later or else for some reason loads the game, then the server, before giving it, the state turns all notifications into commands, executes them on its side, and only after that gives the joining client its new state. Please note that there may be a conflict state of the player with negative resources when the player managed to spend the money exactly at the moment when the server took it away from him. The match is unlikely, but with a large DAU it is almost inevitable. Therefore, the interface and game rules should not fall to death in such a situation. connects later or else for some reason loads the game, then the server, before giving it, the state turns all notifications into commands, executes them on its side, and only after that gives the joining client its new state. Please note that there may be a conflict state of the player with negative resources when the player managed to spend the money exactly at the moment when the server took it away from him. The match is unlikely, but with a large DAU it is almost inevitable. Therefore, the interface and game rules should not fall to death in such a situation. that a conflict state of a player with negative resources may arise when the player managed to spend the money exactly at the moment when the server took it away from him. The match is unlikely, but with a large DAU it is almost inevitable. Therefore, the interface and game rules should not fall to death in such a situation. that a conflict state of a player with negative resources may arise when the player managed to spend the money exactly at the moment when the server took it away from him. The match is unlikely, but with a large DAU it is almost inevitable. Therefore, the interface and game rules should not fall to death in such a situation.

    Commands for which you need to know the server response


    A typical mistake is to think that a random number can only be received from the server. Nothing prevents you from having the same pseudo-random number generator that runs synchronously on the client and on the server, starting from a common view. Moreover, the current seed can be stored directly in the gamestate. Some will find it problematic to synchronize the operation of this generator. In fact, it’s enough to have one more number in the stack — how many exactly numbers were received from the generator up to this point. If for some reason your generator does not converge, then you have an error somewhere and the code is not deterministic. And this fact is necessary not to hide under the carpet, but to understand and look for a mistake. For the vast majority of cases, including even mysteryboxes, this approach is sufficient.

    However, there are times when this option is not suitable. For example, you are playing a very expensive prize and do not want an abrupt comrade to decompile the game, and wrote a bot telling you that you will fall out of the diamond box if you open it right now, and what if you twist the drum in a different place before. You can store the seed for each random value separately, it will protect from frontal hacking, but it does not help from the bot telling you how many boxes the goods you need at the moment. Well, the most obvious case - you may not want to shine in the client config information about the likelihood of a rare event. In short, sometimes you need to wait for the server to respond.
    Such situations should not be solved through the additional features of the engine, but breaking the team into two - the first prepares the situation and puts the interface in a state of waiting for notification, the second is actually notification, with the answer you need. Even if you tightly block the interface between them on the client, another team may slip through - for example, a unit of energy will be restored in time.

    It is important to understand that such situations are not the rule, but the exception. In fact, most games need only one team waiting for an answer - GetInitialGameState. Another bundle of such teams is cross-player interaction in a metagame, GetLeaderboard, for example. All the other two hundred pieces are deterministic.

    Data storage on the server and the cloudy theme of server optimization


    I admit at once, I am a client, and sometimes I heard ideas and algorithms from my familiar servers that would not even have crept into my head. In a way of communicating with my colleagues, I had a picture of how my architecture should work on the server side, ideally. However: There are contraindications, it is necessary to consult with a server specialist.

    First, about storing data. It is your server part that may have additional restrictions. For example, you may be prohibited from using static fields. Further, the code of commands and models is auto-ported, but the code on the client and on the server does not necessarily have to match. Anything can be hidden there, even lazy initialization of the field values ​​from the memcache, for example. Property fields can also receive additional parameters that are used by the server, but do not affect the client’s work in any way.

    The first fundamental difference of the server: where fields are serialized and deserialized. The sensible solution is that most of the state tree is serialized into one huge binary or json field. At the same time, some fields are taken from the tables. This is necessary because the values ​​of some fields will be constantly needed for the operation of interaction services between players. For example, the icon and level are constantly twitching by various people. They are best kept in the usual base. A full or partial, but the detailed state of a person will be needed by someone other than himself very rarely, when someone decides to look at his territory.

    Further, it is inconvenient to pull the fields from the base one by one, and it may take a long time to haul everything. A very non-standard solution, available only for our architecture, may consist in the fact that the client will collect information about all fields stored separately in the tables, the getters have time to touch the team, and add this information to the team so that the server can pick up this field group. one request to the database. Of course, with reasonable restrictions, so as not to ask for DDOS caused by Krivoruk programmers who carelessly touched everything.

    With such separate storage, you should consider the mechanisms of transactionalism when one player climbs into the data of another, for example, steals money from him. But in general, this is done by us as a notification. That is, the thief receives his money immediately, and the robber receives a notification with the instruction to write off the money then when it comes to this turn.

    How commands are shared between servers


    Now the second important moment for the server. There are two approaches. At the first to process any request (or packet of requests), the entire state rises from the database or cache into memory, is processed, and then returns to the database. Operations are processed atomically on a heap of different executing servers, and they only have a common base, and this is not always the case. As a client, I’m shocked by the rise of the entire state for each team, but I’ve seen how it works with my own eyes, and it works very reliably and scalable. The second option is that the state rises once in the memory and lives there until the client falls off only occasionally adding its current state to the base. I am not competent to tell you the advantages and disadvantages of this or that method. It will be good if someone in the comments explains to me why the first has the right to life in general. The second option raises questions about how to interact between the players, by chance turned out to be raised on different servers. This can be critical, for example, if several clan members interact to prepare a joint attack. You can not show others the status of his party member with a delay of 10 saves. Unfortunately, I will not open America here, the interaction through the above-described notifications, commands from one server to another — right now, out of turn, to save the current state of the player raised there. If the servers have the same level of accessibility from different places, and you can manage the balancer, you can try quietly and imperceptibly transfer the player from one server to another. If you know the solution better - be sure to describe in the comments. by chance turned up on different servers. This can be critical, for example, if several clan members interact to prepare a joint attack. You can not show others the status of his party member with a delay of 10 saves. Unfortunately, I will not open America here, the interaction through the above-described notifications, commands from one server to another — right now, out of turn, to save the current state of the player raised there. If the servers have the same level of accessibility from different places, and you can manage the balancer, you can try quietly and imperceptibly transfer the player from one server to another. If you know the solution better - be sure to describe in the comments. by chance turned up on different servers. This can be critical, for example, if several clan members interact to prepare a joint attack. You can not show others the status of his party member with a delay of 10 saves. Unfortunately, I will not open America here, the interaction through the above-described notifications, commands from one server to another — right now, out of turn, to save the current state of the player raised there. If the servers have the same level of accessibility from different places, and you can manage the balancer, you can try quietly and imperceptibly transfer the player from one server to another. If you know the solution better - be sure to describe in the comments. You can not show others the status of his party member with a delay of 10 saves. Unfortunately, I will not open America here, the interaction through the above-described notifications, commands from one server to another — right now, out of turn, to save the current state of the player raised there. If the servers have the same level of accessibility from different places, and you can manage the balancer, you can try quietly and imperceptibly transfer the player from one server to another. If you know the solution better - be sure to describe in the comments. You can not show others the status of his party member with a delay of 10 saves. Unfortunately, I will not open America here, the interaction through the above-described notifications, commands from one server to another — right now, out of turn, to save the current state of the player raised there. If the servers have the same level of accessibility from different places, and you can manage the balancer, you can try quietly and imperceptibly transfer the player from one server to another. If you know the solution better - be sure to describe in the comments. If the servers have the same level of accessibility from different places, and you can manage the balancer, you can try quietly and imperceptibly transfer the player from one server to another. If you know the solution better - be sure to describe in the comments. If the servers have the same level of accessibility from different places, and you can manage the balancer, you can try quietly and imperceptibly transfer the player from one server to another. If you know the solution better - be sure to describe in the comments.

    Dances with time


    Let's start with a question that I really like to throw people at interviews: Here you have a client and a server, each has its own fairly accurate hours. How to find out how much they differ. An attempt to solve this problem in a coffee shop on a napkin reveals the best and worst qualities of a programmer. The fact is that the problem has no formal mathematically correct solution. But the interviewee realizes this, as a rule, on the fifth minute and only after leading questions. And how he meets this insight and what he does next - says a lot about the most important thing in his character - what this person will do when you have real problems in your project.

    The best solution I know allows me to find out not the exact difference, but to clarify the range in which it falls through a lot of request-answers, accurate to the time of the best package that ran from client to server plus the time of the best packet that ran from server to client. In sum, this will give you a few tens of milliseconds of accuracy. This is much better than the need for a mobile game metagame, here we don’t have VR multiplayer or CS, but still it’s nice that the programmer imagined the scale and nature of the clock synchronization difficulties. Most likely, it will be enough for you to know the average lag taken as a ping in half, for a long time with a cut off of deviations by more than 30%.

    The second cool situation that you will surely come across is staging the game in a slip, and translating the clock on the phone. In both cases, the time in the application will change greatly and intermittently, and this needs to be able to work correctly. At a minimum, make the game reboot, But it’s better, of course, not to reboot after each slip, so the time elapsed in the application since its launch cannot be used.

    Third, the situation, for some reason, is a problem for some programmers to understand, although there is its correct solution: Operations can not be performed by server time. For example, to start the production of goods when a request for production came to the server. Otherwise, kiss your determinism goodbye, and catch 35,000 desynchronizations per day caused by different opinions of the client and the server about whether you can already click on the reward. The correct solution is that the command records information about the time when it was executed. The server, in turn, checks whether the time difference between the current server time and the time in the team falls within the allowed interval, and if it falls, it executes the command for its part using the time declared by the client.
    Another task for the interview: Timeout after which the client will try to reboot - 30 seconds. To what extent is the temporary difference acceptable for the server then? Tip # 1: The interval is not symmetrical. Tip # 2: Re-read the first paragraph of this section again, specify how to extend the interval so as not to catch 3000 errors a day on edge effects.

    In order for this all to work beautifully and correctly, it is better to add an additional parameter to the call parameters of the command explicitly - call time. Something like this:

    public interface Command {
    	voidApply(ModelRoot root, long time);
    }

    And my advice to you, by the way, do not use the native Unity types for time in the model - you swallow. It’s better to store UnixTime in server time, always when you need handwritten conversion methods at hand, and store them in a model in a special PTime field that differs from PValue <long> only because when exporting to JSON, it adds brackets to the redundant information that is not readable Import: Time in a human-readable format. You can not obey me. I warned you.

    The fourth situation: In the game style there are situations where the team should be initiated without the participation of the player, in time, for example, energy recovery. A very common situation, in fact. I want to have a field, it is convenient to work it out. For example, PTimeOut, at which you can record a point in time after which a command should be created and executed. In code, it might look like this:

    publicclassMyModel : Model {
    	publicstatic PTimeOut RESTORE_ENERGY = new PTimeOut() {command = (model, property) => new RestoreEnergyCommand() { model = model}}
    	publiclong restoreEnergy { get { return RESTORE_ENERGY.Get(this); } set { RESTORE_ENERGY.Set(this, value); }}
    }

    Of course, when the player is initially loaded, the server must first provoke the creation and execution of all these commands, and only then give the state to the player. The underwater stone here is that it all coolly interferes with the notifications that a player could have gained during this time. Thus, it will be necessary to first unscrew the time before the time of receiving the first notification, if you need to pull a bunch of commands, then create a command from the notification itself, then unscrew the time until the next notification, then work it off and so on. If this whole celebration of life did not fit into the server timeout, and this is possible if the player has a lot of notifications, write the current state from the memory to the database and respond instead with a command to the client to reconnect.

    All of these teams must somehow learn that they need to create and go to run. My little crutch, but a convenient solution is that the model has another challenge, rolling through the entire hierarchy of the model, which twitches after each command is executed, and another timer. Of course, this is an extra overhead on tree traversal almost in an update; instead, you can subscribe or unsubscribe from the currentTime event sticking out of the gamestate with each change in this field:

    public partial classModel {publicvoidSetCurrentTime(long time);
    }
    vs
    public partial classRootModel {public event Action<long> setCurrentTime;
    }

    This is good, but the problem is that the models that are removed from the model tree forever and contain such a field will remain signed for this event and will have to work it out correctly. Before sending a command, check whether they are still in the tree and have a weak reference to this event or control inversion so that they do not remain inaccessible for the GC.

    Appendix 1, a typical case, taken from real life


    Take out the comments to the first part. In toys, very often some actions do not take place immediately after the command into the model, but after the end of some animation. In our practice, there was a case when the mystery boxes are opened, and of course the money should be changed only when the animation is finished playing to the end. One of our developers decided to simplify his life, and on command, do not change the value, but tell the server what he changed, and at the end of the animation, launch a callback that adjusts the values ​​in the model to the ones you need. Well done, in short. For two weeks he made these mystery boxes, and then we still have three very difficult errors that resulted from his activities and I had to spend three more weeks to catch them, despite the fact that we had to “rewrite as normal” no one could distinguish. What brightly follows

    So, my decision is about that. Of course, money is not in a separate field, but is one of the objects in the inventory dictionary, but this is not so important now. The model has one part, which is verified by the server, and on the basis of which the business logic works, and the other, which exists only on the client. Money in the main model is charged immediately after the decision is made, and in the second part of the “pending display” list, an element is created for the same amount, which starts the animation by the fact of its appearance, and when the animation ends, the command that deletes this element is launched. Such a purely client mark "do not show this amount yet". And the real field shows not just the field value, but the field value minus all client deferrals. The division into these two teams is done because that if the client reboots after the first team, but before the second, all the money received by the player will be in his account without any marks or exceptions. In the code, it will be something like this:

    publicclassOpenMisterBox : Command {
        public BoxItemModel item;
        publicint slot;
        // Эта часть команды выполняется и на сервере тоже, и проверяется.public override voidApply(GameState state){
            state.inventory[item.revardKey] += item.revardCount;
        }
        // Эта часть команды выполняется только на клиенте.public override voidApply(GameState state){
            var cause = state.NewPersistent<WaitForCommand>();
            cause.key = item.key;
            cause.value = item.value;
            state.ui.delayedInventoryVisualization.Add(cause);
            state.ui.mysteryBoxScreen.animations.Add(new Animation() {cause = item, slot = slot}));
        }
    }
    publicclassMysteryBoxView : View {
        /* ... */public override voidConnectModel(MysteryBoxScreenModel model, List<Control> c){
            model.Get(c, MysteryBoxScreenModel.ANIMATIONS)
                .Control(c, 
                    onAdd = item => animationFactory(item, OnComleteOrAbort => { AsincQueue(new RemoveAnimation() {cause = item.cause, animation = item}) }),
                    onRemove = item => {}
                )
        }
    }
    publicclassInventoryView : View<InventoryItem> {
        public Text text;
        public override voidConnectModel(InventoryItem model, List<Control> c){
            model.GameState.ui.Get(c, UIModel.DELAYED_INVENTORY_VISUALIZATION).
                .Where(c, item => item.key == model.key)
                .Expression(c, onChange = (IList<InventoryItem> list) => {
                    int sum = 0;
                    for (int i = 0; i < list.Count; i++)
                        sum += list[i].value;
                    return sum;
                }, onAdd = null, onRemove = null ) // Чисто ради показать сигнатуру метода
                .Join (c, model.GameState.Get(GameState.INVENTORY).ItemByKey(model.key))
                .Expression(c, (delay, count) => count - delay)
                .SetText(c, text);
            // Здесь я написал полный код, но в реальности это операция типовая, поэтому для неё, конечно же, существует функция обёртка, которая дёргается в проекте во всех случаях, выглядит её вызов вот так:
            model.inventory.CreateVisibleInventoryItemCount(c, model.key).SetText(c, text);
        }
    }
    publicclassRemoveDelayedInventoryVisualization : Command {
        public DelayCauseModel cause;
        public override voidApply(GameState state){
            state.ui.delayedInventoryVisualization.Remove(cause);
        }
    }
    publicclassRemoveAnimation : RemoveDelayedInventoryVisualization {
        public Animation animation
        public override voidApply(GameState state){
            base.Apply(state);
    		state.ui.mysteryBoxScreen.animations.Remove(animation);
        }
    }

    What do we have in the end? There are two views, in one of them a kind of animation is played, the end of which is waiting for the money to be displayed in a completely different view, which has no idea who and why wants another meaning to be shown. All reactively. At any time, you can load the full state of GameState into the game and it will start playing exactly from the place where we stopped, including the animation will start. The truth will start from the beginning, because we do not stop the animation stage, but if it is very necessary, we can even build it.

    Total


    Making the business logic of a game through models, commands and static files with rules, besieging them from all sides with beautiful detailed and automatically generated logs and enclosing informative exceptions of many common mistakes made by the programmer sawing new features, this is, in my opinion, the right way to live on white light. And not only because you will be able to file a new feature several times faster. This is still damn important, because if you easily nafigachit and debug a new feature, the game designer will have time to put in several times more experiments on gamedizan with the same programs. With all due respect to our work, the only thing that depends on us is whether the game will fail or not, but if it shoots or not it depends on the game directors, and they should be given space for experiments.
    And now I ask you to answer very important questions for me. If there are any ideas how to do what I have done poorly, or just want to comment on my answers, I am waiting for you in the comments. A proposal for cooperation and reference to numerous syntax errors, please in a personal.

    Only registered users can participate in the survey. Sign in , please.

    Do you consider it necessary to predict server responses?

    Does the project need auto-porting commands to the server?

    Do you maintain complete storage of interface information in the model?

    One server for a player or atomic operations

    Solution for commands initiated from the server are satisfied?


    Also popular now: