Optimizing the Ever-Growing Balance in an 11-Year-Old Game

Written by serjey | Published 2025/09/03
Tech Story Tags: game-development | unity-development | programming | optimization | hackernoon-top-story | 11-year-old-game | gaming | ever-growing-balance

TLDRvia the TL;DR App

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.

Over 11 years, War Robots has accumulated a ton of awesome content:

  • 95 robots
  • 21 titans
  • 175 different weapons
  • 40 drones
  • 16 motherships
  • a huge number of skins, remodels, modules, pilots, turrets, ultimate versions of content, and maps

And as you can imagine, to make all of this work we need to store information about behavior, stats, availability, prices, and much, much more.

As a result, our balances have grown to an indecent size:

Default mode

Skirmish mode

Meta balance

9.2 MB

9.2 MB

Core balance

13.1 MB

13.1 MB

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

% in balance

Usage count

String

28.478 %

164 553

Int32

27.917 %

161 312

Boolean

6.329 %

36 568

Double

5.845 %

33 772

Int64

4.682 %

27 054

Custom structures

26.749 %

Core Balance

% in balance

Usage count

String

34.259 %

232 229

Double

23.370 %

158 418

Int32

20.955 %

142 050

Boolean

5.306 %

34 323

Custom structures

16.11 %

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.

String usage count

String usage count

Before

After

Meta balance

164 553

10 082

Core balance

232 229

14 228

Not bad, right?

But that was just the beginning — we didn’t stop there.

Reworking the Data Protocol

For transmitting and processing balance structures, we had been using MessagePack.

MessagePack is a binary data serialization format designed as a more compact and faster alternative to JSON. It’s meant for efficient data exchange between applications or services, allowing a significant reduction in data size — especially useful where performance and bandwidth matter.

Initially, MessagePack came in a JSON-like format, where the data used string keys. That’s certainly convenient, but also quite space-consuming. So we decided to sacrifice some flexibility and switch to a binary byte array.

Before:

public class SomeBalanceMessage
{
  [Key("id")]
  public string Id;
  
  [Key("name")]
  public string Name;
  
  [Key("amount")]
  public int Amount;
}

After:

public class SomeBalanceMessageV2
{
  [Key(0)]
  public StringIdx Id;
  
  [Key(1)]
  public StringIdx Name;
  
  [Key(2)]
  public int Amount;
}

We also removed all empty collections — instead of sending them, we now transmit null values. This reduced both the overall data size and the time required for serialization and deserialization.

Testing the Changes

A golden rule of good development (and one that will save you a lot of nerves) is to always implement new features in a way that lets you quickly roll them back if something goes wrong. For that reason, we add all new features behind “toggles.” To make this work, we had to support two versions of balances at the same time: the old one and the optimized one.

During development, we needed to make sure that all data was transferred correctly. Old and new balances — regardless of format or structure — had to produce the exact same values. And remember, the optimized balances had changed their structure drastically, but that wasn’t supposed to affect anything except their size.

To achieve this, we wrote a large number of unit tests for each balance.

At first, we compared all fields “head-on” — checking every single one explicitly. This worked, but it was time-consuming, and even the smallest change in the balances would break the tests, forcing us to rewrite them constantly. This slowed us down and was quite distracting.

Eventually, we had enough of that and came up with a more convenient testing approach for comparing balances.

Reflection came to the rescue again. We took two versions of the balance structures, e.g. SomeBalanceMessage and SomeBalanceMessageV2, and iterated over them — comparing field counts, names, and values. If anything didn’t match, we tracked down the problem. This solution saved us a huge amount of time later on.

Optimization Results

Thanks to these optimizations, we managed to reduce both the size of the files transmitted over the network and the time it takes to deserialize them on the client. We also decreased the amount of memory required on the client side after balance deserialization.

File Size

Old balances

Optimized balances

Profit

Meta balance

9.2 MB

1.28 MB

- 86 %

Core balance

13.1 MB

2.22 MB

- 83 %

Deserialization Time

Old balances

Optimized balances

Profit

Meta balance

967 ms

199 ms

- 79 %

Core balance

1165 ms

265 ms

- 77 %

Data in Memory

Old balances

Optimized balances

Profit

Meta + Core

~ 45.3 MB

~ 33.5 MB

- 26 %

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.


Written by serjey | Unity developer
Published by HackerNoon on 2025/09/03