It is naive. Super: code and architecture of a simple game

We live in a complex world and, it seems, have begun to forget about simple things. For example, about Occam’s razor, the principle of which is: "What can be done on the basis of a smaller number should not be done on the basis of a larger one." In this article I will talk about simple and not the most reliable solutions that can be used in the development of simple games.



By the autumn DotNext in Moscow, we decided to develop a game. It was an IT variation of the popular Alchemy. Players had to collect 128 concepts related to IT, pizza and Dodo from the available 4 elements. And we needed to realize this from an idea to a working game in a little more than a month.

In a previous article, I wrote about the design component of the work: planning, fakapy and emotions. And this article is about the technical part. There will even be some code!

Disclaimer: The approaches, code, and architecture that I write about below are not complicated, original, or reliable. On the contrary, they are very simple, sometimes naive and not designed for heavy loads by design. However, if you have never made a game or application that uses logic on the server, then this article can serve as a starting impetus.

Client Code


In a nutshell about the architecture of the project: we had mobile clients for Android and iOS on Unity and a backend server on ASP.NET with CosmosDB as storage.

A client on Unity represents only interaction with the UI. The player clicks on the elements, they move around the screen in a fixed way. When a new element is created, a window appears with its description.


The game

process This process can be described by a fairly simple state machine. The main thing in this state machine is to wait for the animation of the transition to the next state, blocking the UI for the player.



I used the very cool UnitRx library to write Unity code in a completely asynchronous style. At first I tried to use my native Task, but they behaved unstable on builds for iOS. But UniRx.Async worked like a clock.

Any action that requires animation is called through the AnimationRunner class:

public static class AnimationRunner
   {
       private const int MinimumIntervalMs = 20;
     public static async UniTask Run(Action<float> action, float durationInSeconds)
       {
           float t = 0;
           var delta = MinimumIntervalMs / durationInSeconds / 1000;
           while (t <= 1)
           {
               action(t);
               t += delta;
               await UniTask.Delay(TimeSpan.FromMilliseconds(MinimumIntervalMs));
           }
       }
   }

This is actually replacing the classic coroutine with UnitTask. Additionally, any call that should block the UI is invoked through a HandleUiOperationglobal class method GameManager:

public async UniTask HandleUiOperation(UniTask uiOperation)
       {
           _inputLocked = true;
           await uiOperation;
           _inputLocked = false;
       }

Accordingly, in all controls, the value of InputLocked is first checked, and only if it is false does the control react.

This made it easy enough to implement the state machine depicted above, including network calls and I / O, using the async / await approach with nested calls, like in a Russian doll.

The second important feature of the client was that all providers that received data on the elements were made in the form of interfaces. After the conference, when we turned off our backend, it allowed literally in one evening to rewrite the client code so that the game became completely offline. This version can be downloaded now from Google Play .

Client-back interaction


Now let's talk about what decisions we made while developing the client-server architecture.



Pictures were stored on the client, and the server was responsible for all the logic. At startup, the server read the csv file with id and descriptions of all the elements and stored them in its memory. After that, he was ready to go.

API methods was a necessary minimum - only five. They implemented all the logic of the game. Everything is quite simple, but I’ll tell you about a couple of interesting points.

Authentication and Starter Elements


We abandoned any complicated authentication system and generally any passwords. When a player enters a name on the start screen and presses the "Start" button, a unique random token (ID) is created in the client. However, it is not attached to the device. The player’s name along with the token are sent to the server. All other requests from the client to the back contain this token.



The obvious disadvantages of this solution are:

  1. If the user demolishes the application and re-installs it, he will be considered a new player, and all his progress will be lost.
  2. You cannot continue the game on another device.

These were deliberate assumptions, because we understood that people would play on the confe just from just one device. And they will not have time to switch to another device.

Thus, in the planned scenario, the client called the server method AddNewUseronly once.

When loading the game screen GetBaseElements, the method was also called once , which returned id, the name of the sprite and description for the four basic elements. The client found the necessary sprites in its resources, created the objects of the elements, wrote them to itself locally and painted on the screen.

Upon repeated launches, the client no longer registered on the server and did not request start elements, but took them from the local storage. As a result, the game screen immediately opened.

Merge Elements


When a player tries to connect two elements, a method is called MergeElementsthat either returns information about the new element or reports that these two elements are not collected. If the player has collected a new element, information about this is recorded in the database.



We applied the obvious solution: to reduce the load on the server, after the player tries to add two elements, the result is cached on the client (in memory and in csv). If a player tries to re-stack items, the cache is checked first. And only if the result is not there, the request is sent to the server.

Thus, we know that each player can make a limited number of interactions with the back, and the number of entries in the database does not exceed the number of available elements (of which we had 128). We were able to avoid the problems of many other conference applications, which after a large and simultaneous influx of participants often backed up.

High score table


Participants played our "Alchemy" not just like that, but for the sake of prizes. Therefore, we needed a table of records, which we displayed on the screen at our stand, and also in a separate window in our game.



To form a table of records, the last two methods are used GetCurrentLadderand GetUser.there is also a curious nuance. If a player is in the top 20 results, his name is highlighted in the table. The problem was that the information of these two methods was not directly related.

The method GetCurrentLadderaccesses the collection Stats, gets 20 results and does it quickly. The method GetUseraccesses the collection.Usersby UserId and does it too fast. The merging of the results is already on the client side. That's just that we did not want to shine UserId in the results, so they are not there. The comparison took place by the name of the player and the number of points scored. In the case of thousands of players, collisions would inevitably be. But we counted on the fact that among all the players there are unlikely to be players with the same names and points. In our case, this approach is fully justified.

Game over


Our initial task was to assemble a game for a two-day conference in a month. All those decisions that we embodied in architecture and code have fully justified themselves. The server did not lie down, all requests were processed correctly, and the players did not complain about bugs in the application.

By the next Moscow DotNext, we are most likely to stir up another game, because it has now become our good tradition ( CMAN-2018 , IT-Alchemy-2019 ). Write in the comments for which time killer you are ready to exchange hardcore reports from development stars. :)
For the same naive and interested, we have posted the client code of IT alchemy in the public domain .

And look into the telegram channel , where I write all about development, life, mathematics and philosophy.

All Articles