How we swung on a mobile fast paced shooter: technologies and approaches
A year ago, we had one project in our company - a mobile shooter War Robots with relatively slow, but colorful and intense battles. The game continues to evolve, it has tens of millions of installations and players around the world, updates are constantly coming out. At some point, we wanted to make a dynamic Unity shooter with speeds comparable to Overwatch, CS: GO or Quake. But to implement the plan for mobile platforms (primarily iOS and Android) based on War Robots with the current architecture and approaches was almost impossible.
We understood how to do this in theory - there are many articles, presentations on YouTube that tell you in detail about how to write a shooter, how to work with the networkwhat problems arise and how to solve them. There is no Rocket Science, all these approaches were invented 30 years ago and during this time they haven’t changed much. BUT: we had no practice.
Looking ahead, I’ll say - we were able to implement our plan. We have created a dynamic fast shooter for mobile platforms, which is now in beta testing and is being actively developed. And I would really like to share all this. This is the first, overview article with a listing and a brief description of almost everything that we use (please do not confuse it with our other project in development, the technologies and approaches in which are similar, but differ in details).
Let's start by simulating the gameplay. We decided to write it on ECS - this is a data-oriented approach in which data is separated from logic. Data is represented as Entities and Components belonging to entities. The logic is described in Systems, which usually go through the components of entities and change them, create new components and entities.
We wrote our ECS fairly quickly, because current solutions did not suit us. ECS allowed us to separate logic from data, as well as quickly serialize world data and exchange it between client and server. We have a separate project for generating ECS code in a shared library, as many methods in it are duplicated from component to component, for example, serializing data, copying, comparing, and delta compression.
The same simulation code works simultaneously on the client and server. This has given us the following benefits:
- Game features can be completely written by the client programmer - the same logic will work on the server.
- There is no delay between the player’s action and the result of the action in the game, as the client instantly processes the command locally (prediction). And only if the result of the local simulation diverged from the server one, the client takes the server state of the world as a basis and rolls back.
- Player training (tutorial) we can simulate locally on the device, without connecting to the game server.
- When developing features, a project does not need a constantly working server. To test logic, graphics, game design, etc. - You can start the match locally.
- Using a common library, we were able to quickly write bots for load testing, which in time revealed various performance and memory problems on the game server.
In addition to prediction and rollback on the client, we use interpolation. But not in the usual sense, because we simulate and draw in one frame, 30 times per second, and in fact, we do not need classical interpolation.
If possible, we interpolate the states that came from the server in the buffer, because on mobile platforms, packet loss can be significant, but we would like to have a state of the world for rendering here and now.
Client Architecture Client
code actively uses dependency injection. As a DI framework we use Zenject. Specific binding settings are described in the small Composition Root, which in Zenject are called Installer. In most cases, we try to write in such a way that by disabling a specific Installer we disable the feature / view / network interaction.
The game has several contexts and the objects there belong to a specific context, live with it:
- ProjectContext - objects that live all the time the application lives;
- MetaContext - character selection, equipment, purchases, etc .;
- MatchContext - PvP battle context.
The context of the battle
In the context of the MatchContext at the level of representation of the game mechanics, we use MVP . The model is data from ECS, presenters work with them and the Unity-view part for display. We have presenters for entities and components. At the presentation level, the UI uses MVVM , described below.
To transport data between the client and the server, the low-level Photon library acts , from which we use the udp-based protocol. We serialize the data ourselves: we wrote a custom delta compression, because the game world is large, and the volume of traffic is critical for mobile devices, and it would be nice for us to meet MTU so that the state of the world fits in one physical package.
In MetaContext for the display level, we use MVVM and DataBinding based on the Unity-Weld library . As a result, we do not have a custom MonoBehaviour class for each window with a bunch of settings and UI logic, as is usually done on Unity projects. Instead, the UI logic is described in the ViewModel of a particular window, and the View level is represented by only one class - View (inherits from MonoBehaviour) and several standard classes for data binding. In our case, the programmer writes only logic and gives the necessary data “out”, and the UI designer typeset and sets up the data binding to the display.
In the meta context, we also use the Signal-Command-Service approach, partly borrowed from the StrangeIoC librariesand IoC + . Now it is based on Zenject signals, but it is written so that at any time it can be rewritten to any other option. Thus, the event-action / service relationship is now configured at the Installer level and is described in one line.
The protocol for communicating with meta-servers is based on Protobuf ; WebSockets are used as a transport .
Third Party Solutions
In addition to the ones described above, we use on the client many other third-party solutions and plugins to speed up development:
- FMOD - for working with sound. We have a separate sound designer, he knows how to “cook” cool sounds and music in an FMOD editor.
- Volatile Physics - physics on the client and server, written in C #.
- Lunar Console - for viewing logs on the client, as well as test functionality.
- Helpshift - to communicate with our players and collect feedback.
- AppMetr is our own analytics system.
- Json.net .
- And etc.
We have a separate team that develops common solutions for projects. For example:
- we have our own PackageManager (it is better than in Unity 2018 and appeared long before it), which supports versioning and deletion. Through it we deliver to the project all our other solutions, as well as third-party plugins and libraries. We have no problem removing and updating plugins;
- own BuildPipeline - the ability to customize the assembly for different configurations and platforms, because usually the assembly and configuration steps are different + integration with TeamCity;
- client authorization module in the system;
- automatic testing system;
- third-party plugins adapted for us (logging, analytics; see above);
- general shaders;
- and etc.
We have extensive experience optimizing for mobile platforms. In particular:
- We optimize shaders.
- We significantly reduce the build size by compressing textures, assembling them into atlases, reducing the number of variants in shaders, and more.
- We use our MeshMerger to combine static objects with one material into one object, we also merge textures.
- Using the Unity built-in profiler, as well as dotTrace and dotMemory, we optimize the code.
- We use memory pools and preallocated memory to minimize garbage collection.
- We use delta compression to reduce the size of sent packets.
- Much more.
Some of this can be read in our previous articles “Post-effects in mobile games” and “Optimization of 2d applications for mobile devices in Unity3d” . The second of these two articles is already outdated, but many tips from there work now.
The game server on which the battles take place is written in C #. The udp-based Photon Network Library mentioned above is used as the network protocol. At the moment, edits to the server are made very rarely, because all game logic is written by client programmers and it is spinning on the server.
We also wrote a special tool that allows you to connect to the server via http and visually view battles in real time. In addition, you can view all the entities, their components and values, as well as record a battle in order to play it later for test purposes. We will write about the game server separately in future articles.
By meta we mean a microservice system that allows you to implement such features as: player profile, purchases, matchmaking, chat, clans, etc. All this is written by individual programmers and used together on different projects. Technologies used in development:
- Java is the main development language.
- Cassandra , postgres - to store player data.
- Consul - as a service discovery, including storage of key-value data for game and server settings.
- RabbitMQ - message queue.
- Protobuf is a protocol between services and a client.
- gRPC - for communication between services and the game server.
- As well as netty , akka , logback and more.
The game quickly grows with features and we are actively attracting new employees, but in general, the project team consists of a core and related departments.
The core includes client programmers, game designer, Project Manager, Product Owner and QA (testers). Related departments include:
- graphics programmers;
- meta server programmers;
- team developing common solutions for projects;
- community managers;
- marketing and others
How we are working
We work on Scrum . But it is worthwhile to understand that everyone has their own Scrum. We have two-week iterations, poker planning, retrospectives, demos, etc. But at the same time, our releases are not tied to the end of the sprint and there is an additional stage of release testing.
We store the code on GitHub, make pull requests and use Upsource for code review. We write the code in Rider and Visual Studio + Resharper .
To build the client, as well as deploy and deploy servers, we use TeamCity and Gradle. Client installation on a mobile device takes place in one or two clicks:
- Build a client in TeamCity by clicking Run (you can skip this step, since mostly assemblies take place automatically);
- installation using the QR code generated for the build.
Instead of a conclusion
As you can see, in our time a serious product uses a large number of approaches and technologies, as well as resources and specialists, and not everything is as simple as it might seem at first glance.
In the next article, I can talk about our ECS , how we wrote it and how it works, or write in the comments about what else from the above you would like to read in the near future.