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

    In previous articles, we described how a convenient and well-designed model should be arranged, which command system that performs the functions of controllers would suit her, it was time to talk about the third letter of our alternative MVC abbreviation.

    In fact, there is a ready-made very sophisticated library UniRX that implements reactivity and inversion of control for unity. But we'll talk about it at the end of the article, because this powerful, huge and RX-compliant tool for our case is quite redundant. It is perfectly possible to do everything that we need without pulling up the RX, and if you own it, it will not be difficult for you to do the same with it.

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

    When a person is just starting to write the first game, it seems logical to him the existence of a function that will draw him the whole form, or some part of it, and pull it every time something important has changed. Time passes, the interface grows in size, fomochek and parts of the molds become a hundred, then two hundred, and when the state of the wallet changes, a quarter of them have to be redrawn. And then the manager comes, and says that it is necessary “like in that game” to make a little red dot on the button if there is a section inside the button, in which there is a subsection, in which there is a button, and now you have enough resources to do something that is important. And everything sailed ...

    The departure from the concept of drawing takes place in several stages. First solved the problem of single fields. You have, for example, a field in a model, and a text field in which all its contents should be displayed. Ok, we get an object that subscribes to updates of this field, and with each update adds the results into a text field. In the code, something like this:

    var observable = new ChildControl(FCPlayerModel.ASSIGNED, Player);
    observable.onChange(i => Assigned.text = i.ToString())

    Now we don’t need to follow the redrawing, it’s enough to create this construction, and then everything that happens in the model will get into the interface. Well, but cumbersome, it contains a lot of obviously unnecessary gestures that a programmer will have to write 100,500 times with his hands and sometimes make mistakes. Wrap up these ads in the expansion function, which will hide the extra small letters under the hood.

    Player.Get(c, FCPlayerModel.ASSIGNED).Action(c, i => Assigned.text = i.ToString());

    Much better, but that's not all. Shifting the model field into a text field for so many frequent and typical operations that for it we will create a separate wrapper function. Now it turns out quite briefly and well, I think.

    Player.Get(c, FCPlayerModel.ASSIGNED).SetText(c, Assigned);

    Here I showed the main idea that I will use to guide the creation of the interface for the rest of my life: “If you had to do something for the programmer at least twice, wrap it in a special convenient and short function.”

    Garbage collection

    A side effect of reactive interface construction is the creation of heaps of objects that are signed for something and therefore will not leave memory without a special kick. For myself, back in ancient times, I came up with a method that is not so beautiful, but simple and affordable. When creating any form, a list of all controllers is created that are created in connection with this form, for brevity it is simply called “c”. All special wrapper functions accept this list as the first required parameter, and when DisconnectModel is used, it uses code in the common ancestor to go through the list of all controls and all of them mercilessly displace. No beauty and grace, but cheap, reliable and relatively practical. It is possible to have a little more security if instead of a sheet of controls you need to request an IView and give it to all these places. Essentially the same forget to fill in the same way will not work, but harder to hack. I am afraid to forget, but I am not very afraid that someone will consciously break the system, because with such clever people you need to fight with a belt and other non-software methods, so I limit myself to just c.

    An alternative approach can be learned from UniRX. Each wrapper creates a new object that has a link to the previous one that it listens to. And at the end, the AddTo (component) method is called, which assigns the entire chain of controls to some deleted object. In our example, this code will look like this:


    If this last owner of the chain decides to be destroyed, he will give the command “kill yourself about dispose if all of you other than me no longer listen to you.” And the whole chain obediently cleaned up. So of course, it’s much more concise, but from my point of view there is one important shortcoming. AddTo can be accidentally forgotten and no one will ever know about it until it is too late.

    In fact, you can use the dirty hack of Unity and do without any additional code in the View:

    publicstatic T AddTo<T>(this T disposable, Component component) where T : IDisposable {
    	var composite = new CompositeDisposable(disposable);
    		.Where(_ => component == null)
    		.Subscribe(_ => composite.Dispose())
    	return disposable;

    As you know, the link to the component or the GameObject in Unity is null. But you have to understand that this one here hakokostyl creates an Update listener for each chain of controls being destroyed, and this is already a little polite.

    Model independent interface

    Our ideal, which we, however, can easily achieve, is the situation when we can load the full GameState at any moment, both the model checked by the server and the data model for the UI, and the application will be in exactly the same state, up to the state of all the buttons. Two reasons interfere with this. The first is that some variable programmers like to store inside the controller of the form, or even in the view itself, arguing that their life cycle is exactly the same as that of the form itself. The second is that even if all the data for the form is in its model, the team itself to create and fill out the form passes as an explicit function call, also with some additional parameters, for example, on which field from the list you need to focus.

    With this you can not fight, if you do not really want the convenience of debugging. But we are not, we want to debug the interface as conveniently as the main operations with the model. To do this, the next focus. In the UI part of the model, a variable is set, for example .main, and within the command you place the model of the form you want to see into it. The state of this variable is monitored by a special controller, if, depending on its type, a model appears in this variable, it instantiates the desired form, places it where it is needed, and sends a call to it ConnectModel (model). If the variable is freed from the model, the controller will remove the form from the canvas and subdispose it. Thus, no action is taken to bypass the model, and everything that you did with the interface is perfectly visible on the ExportChanges model. And then we are guided by the principle of “everything that is done double wrap” and use exactly the same controller at all levels of the interface. If there is a place in the form for another form, then a UI model is created for it, and a variable is created in the model of the parent form. Exactly the same with lists.

    A side effect of this approach is that two files are added to any form, one with the data model for this form, and the other, usually a single item containing references to the UI elements, which, having received the model in its ConnectModel function, will create all the reactive controllers for all model fields and all UI elements. Well, it is even more compact to manage, so that it is also convenient to work with this, probably it is impossible. If you can - write in the comments.

    List controls

    A typical situation is when the model has a list of some elements. Since I want everything to be done very handy, and preferably in a single line, then for the lists I wanted to do something that would be convenient to process them. Just one line is possible, but it turns out to be uncomfortably long. It turned out empirically that almost the whole variety of cases is covered by only two types of controls. The first monitors the state of a collection, and calls three lambda functions, the first is called when an element is added to the collection, the second when the element leaves the collection, and finally the third is called when the elements of the collection change their order. The second most frequent type of control keeps track of the list, and is the source of a sub-list of it - pages with a specific number. That is, for example, It keeps track of a 102-item-long List, and gives a List of 10 elements from the 20th to the 29th. And the events are generated exactly the same as if he himself was a list.

    Of course, following the principle of “create a wrapper for everything that was done twice,” a huge number of convenient wrappers appeared, for example, one that only accepts Factory for the input, building a correspondence between the types of models and their View, and a link to the Canvas in which It is necessary to add elements. And many other similar, only about a dozen wrappers for typical cases.

    More complex controls

    Sometimes there are situations that are excessive to express through the model, as they are obvious. Here controls can come to the rescue, performing some operation on the value, as well as controls that monitor other controls. For example, a typical situation: an action has a price, and a button is active only if there is more money on the account than its price.

    item.Get(c, FCUnitItem.COST).Join(c, Player.Get(c, MONEY)).Func(c, (cost, money) => cost <= money).SetActive(c, BuyButton);

    In fact, the situation is so typical that, in accordance with my principle, there is a ready-made wrapper for it, but here I showed its contents.

    They took an item to buy, created an object that is subscribed to one of its fields, and has a value of type long. Another control was added to it, having a type too long, the method returned a control that has a couple of values, and the Changed generating event when any of them changes, then Func creates an object for any change in input computing function, and the Changed generating event if the total value counted functions changed.

    The compiler itself successfully builds the required type of control based on the types of input data and the type of the resulting expression. In rare cases where the type returned by the lambda function is not obvious, the compiler will ask you to specify it explicitly. Finally, the last call is listening to the Bulenovsky control, depending on which it turns the button on or off.

    In fact, the real wrapper in the project accepts two buttons on the input, one for the case when there is money and another when there is not enough money, and also the second button puts the command to open the modal window “Purchase additional currencies”. And all this is in one simple line.

    It is easy to see that using Join and Func you can build arbitrarily complex structures. I had a function in the code, generating a complex control, calculating for what amount a player can buy troops, given the number of players on his side, and the rule is that everyone can exceed the budget by 10% if they all together do not exceed the total budget. And this is an example of how not to do it, because how simple and easy it is to debug what is happening in models for as many times as it is difficult to catch the error in the reactive controls. You will even spend the time taking a lot of time to understand what led to it.

    Therefore, the general principle of using complex controls is as follows: When prototyping a form, you can use constructions on reactive controls, especially if you are not sure that they will become more complicated in the future, but as soon as you suspect that it breaks, you won’t understand what happened, You should immediately transfer these manipulations to the model, and the calculations that were previously done in the controls should be placed in extension methods in static classes of rules.

    This is significantly different from the “Do it right all right” principle, so beloved by perfectionists, because we live in the game-dev world, and when you start to burn a mold you absolutely cannot be sure what it will do in three days. As one of my colleagues said: “If I received five kopecks every time game designers change their mind, I would be a very rich person.” In fact, this is not bad, but even the opposite is good. The game should be developed by trial and error, because if you are not making a stupid clone, then you finally can not imagine what the players really need.

    Single data source for multiple views

    On so many archetypical case that you need to talk about it separately. It happens that the same element model as part of an interface model is drawn in different Views depending on where and in what context this happens. And we use the principle - “one type, one view”. For example, you have a weapon purchase card containing the same uncomplicated information, but in different modes of the store it should be represented by different prefabs. The solution consists of two parts for two different situations.

    The first is when this View is placed inside two different Views, for example a store in the form of a short list and a store with large pictures. In this case, two separate, differently configured factories that build type-prefab conformance come to the rescue. In the ConnectModel method of one View, you will use one and in the other. It is a completely different case if you need to show cards a little differently with absolutely identical information in one place. Sometimes in this case, an additional field appears to the element model, indicating the festive headlight of this particular element, and sometimes just an element appears in the element model that does not have any fields, and is needed only to be drawn by another prefab. In principle, nothing contradicts.

    It would seem an obvious solution, but I saw enough of someone else's code for strange dances with a tambourine around this situation, and found it necessary to write about it.

    Special case: controls with a hell of dependencies

    There is one very special case about which I want to talk separately. These are controls that monitor a very large number of elements. For example, a control that monitors the list of models and sums up the contents of some field lying inside each of the elements. If there is a large overturning in the list, for example, filling it with the data, such a control risks catching as many change events as in the list of elements plus one. Of course, counting the aggregate function as many times as a bad idea. Especially for such cases, we do the control, which we subscribe to the onTransactionFinished event, which sticks out from GameState, and as we remember, there is a link to GameState in any model. And with any change in the input data, this control will simply put a label on it, that the source data has changed, and recalculated only when it receives a message about the end of the transaction, or when it detects that the transaction has already been completed at the moment when it received the message from the input event stream. It is clear that such a control may not be protected from unnecessary messages if there are two such controls in the thread processing chain. The first one accumulates a cloud of changes, waits until the end of the transaction, launches the flow of changes further, and there is another one that has already caught a lot of changes, received an event about the end of the transaction (it was unlucky to be earlier in the list of functions subscribed to the event), counted everything, and then bang and another change event, and recount everything a second time. So maybe, but rarely, and more importantly,

    UniRX Ready Library

    And it would be possible to limit everything to what was said above, and calmly start writing your masterpiece, all the more so compared to the model and control teams it’s very simple and they’ve written everything in less than a week if the idea that you invent a bicycle hadn’t been tricked, and everything is already thought out and written before me is distributed free to anyone.

    Having uniRX uncovered, we find a beautiful and standards-compliant design that can create streams from everything in general, deftly merzhit them, filter them from the main thread to the non-main thread, or return control back to the main thread, which has a bunch of ready-made tools to send to different places and so Further. We do not have there exactly two things: Simplicity and Convenience debugging. Have you ever tried to debug some multi-storey construction on Linq by steps in the debager? So here is still much worse. At the same time, we completely lack something for which all this sophisticated machinery was created. For the sake of simplicity in debugging and reproducing states, we completely lack a variety of signal sources, everything happens in the main stream,

    In general, if you already know how to do UniRX, then I’ll do it for IObservable models, and you will be able to use the trump features of your favorite library, but for the rest I suggest not trying to build tanks from high-speed cars and cars from tanks only on that basis that both the one and the other have wheels.

    At the end of the article I have to you, dear readers, the traditional questions that are very important for me, my ideas about the beautiful, and for the prospects for the development of my scientific and technical creativity.

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

    Can you write the reactive controls described in the article?

    Will you use a fully model-based approach to building interfaces?

    What is the best way to collect garbage:

    Would you use complex compound controllers

    Do you know the UniRX library?

    Would you use an engine like mine if you could download it ready with an assembler?

    Will you try in your work to apply some of the ideas described here in the next six months:

    Also popular now: