Hello! My name is Sergey Kachan, and I’m a client developer on the War Robots project.
War Robots has been around for many years, and during this time the game has accumulated a huge variety of content: robots, weapons, drones, titans, pilots, and so on. And for all of this to work, we need to store a large amount of different types of information. This information is stored in “balances.”
Today I’m going to talk about how balances are structured in our project, what’s happened to them over the past 11 years, and how we’ve dealt with it.
Balances in the Project
Like any other project, War Robots can be divided into two parts: meta and core gameplay.
Meta gameplay (metagaming) is any activity that goes beyond the core game loop but still affects the gameplay. This includes purchasing and upgrading game content, participating in social or event activities.
Core gameplay (core gameplay loop) is the main repeating cycle of actions that the player performs in the game to achieve their goals. In our case, it’s robot battles on specific maps.
Each part of the project needs its own balance, so we also split balances into two categories — meta and core.
War Robots also has so-called Skirmish modes, which require their own separate balances.
A Skirmish mode is a modification of existing modes or maps with different characteristics or rules. Skirmish modes are often event-based, available to players during various holidays, mainly for fun. For example, players might be able to kill each other with a single shot or move around in zero gravity.
So in total, we have 4 balances: 2 for the default mode and 2 for the Skirmish mode.
After some quick calculations, we found that a player would need to download 44.6 MB. That’s quite a lot!
We really didn’t want to force players to download such large amounts of data every time a balance changed. And distributing that much data via CDN isn’t exactly cheap either.
Just to remind you: War Robots has reached 300 million registered users. In 2024, our monthly active audience was 4.7 million people, and 690 thousand players logged in every day.
Now imagine the amount of data. A lot, right? We thought so too. So, we decided to do everything we could to cut down the size of our balances!
Hunting Down the Problem
The first step was to analyze the balances and try to figure out: “What’s taking up so much space?”
Manually going through everything was the last thing we wanted to do — it would’ve taken ages. So, we wrote a set of tools that collected and aggregated all the information we needed about the balances.
The tool would take a balance file as input and, using reflection, iterate through all the structures, gathering data on what types of information we stored and how much space each one occupied.
The results were discouraging:
Meta Balance:
After analyzing the situation, we realized that strings were taking up far too much space, and something had to be done about it.
So, we built another tool. This one scanned the balance file and generated a map of all the strings along with the number of times each one was duplicated.
The results weren’t encouraging either. Some strings were repeated tens of thousands of times!
We had found the problem. Now the question was: how do we fix it?
Optimizing the Balances
For obvious reasons, we couldn’t just get rid of strings altogether. Strings are used for things like localization keys and various IDs. But what we could do was eliminate the duplication of strings.
The idea was as simple as it gets:
- Create a list of unique strings for each balance (essentially, a dedicated storage).
- Send this list along with the data.
public class BalanceMessage
{
public BalanceMessageData Data;
public StringStorage Storage;
public string Version;
}
StringStorage is essentially a wrapper around a list of strings. When we build the string storage, each balance structure remembers the index of the string it needs. Later, when retrieving data, we just pass the index and quickly get the value.
public class StringStorage
{
public List<string> Values;
public string GetValue(StringIdx id) => Values[id];
}
Instead of passing the strings themselves inside the balance structures, we began passing the index of where the string is stored in the string storage.
Before:
public class SomeBalanceMessage
{
public string Id;
public string Name;
public int Amount;
}
After:
public class SomeBalanceMessageV2
{
public StringIdx Id;
public StringIdx Name;
public int Amount;
}
StringIdx is basically just a wrapper around an int. This way, we completely eliminated direct string transfers inside the balance structures.
public readonly struct StringIdx : IEquatable<StringIdx>
{
private readonly int _id;
internal StringIdx(int value) {_id = value; }
public static implicit operator int(StringIdx value) => value._id;
public bool Equals(StringIdx other) => _id == other._id;
}
This approach reduced the number of strings by tens of times.
Deserialization Time
Conclusions
The results of the optimization fully satisfied us. The balance files were reduced by more than 80%. Traffic went down, and the players were happy.
To sum it up: be careful with the data you transmit, and don’t send anything unnecessary.
Strings are best stored in unique storages to avoid creating duplicates. And if your custom data (prices, stats, etc.) also contains a lot of repetition, try packing those into unique storages as well. This will save you many megabytes — and a lot of money on maintaining CDN servers.
SOCIAL SHARE CARD GENERATOR