Назад к блогу

Оптимизация масштабирования мобильных игр: проектирование городов для 1M+ Concurrent Players

Опубликовано 21 мая 2026 г.
Оптимизация масштабирования мобильных игр: проектирование городов для 1M+ Concurrent Players

Коротко о главном

Статья описывает комплексные стратегии оптимизации мобильных MMO для поддержки более миллиона Concurrent Players в масштабных городах. Основное внимание уделяется использованию Spatial Hashing для разгрузки CPU сервера, динамическому управлению Network Relevancy для экономии трафика и асинхронному Asset Streaming для работы в рамках лимитов RAM. Также рассматриваются преимущества использования horizOn для автоматизации сложных DevOps-задач, таких как Server Sharding и оркестрация серверов.

Каждый Multiplayer-разработчик знает тот самый момент, когда архитектура мобильной игры трещит по швам. Вы проектируете огромную, красивую городскую среду. Тестируете её локально с 10 симулированными клиентами, и билд выдает безупречные 60 FPS. Затем вы выводите её в live-окружение с 1 000 Concurrent Players, толпящихся на центральной площади. За считанные секунды бюджетные Android-устройства вылетают из-за Out-Of-Memory (OOM) exceptions, iOS Jetsam агрессивно убивает ваше приложение, а CPU вашего Dedicated Server подскакивает до 100%, пытаясь рассчитать Network Replication для тысяч перекрывающихся сущностей.

При создании мобильной MMO или крупномасштабного открытого мира, рассчитанного на миллионы активных пользователей, нельзя полагаться на стандартные настройки движка по умолчанию. Мобильное оборудование имеет строгий Thermal Throttling и жесткие лимиты памяти (часто ограничивающие вашу игру менее чем 2 ГБ доступной RAM на устройствах среднего сегмента). Одновременно с этим ваш сервер должен справляться с плотными кластерами игроков, не сгибаясь под нагрузкой.

Достижение настоящей оптимизации масштабирования мобильной игры требует трехуровневого подхода: агрессивный Spatial Partitioning на сервере, беспощадный Memory Management на клиенте и Distributed Backend Architecture для обработки огромного объема соединений. В этом пошаговом руководстве мы разберем, как именно проектировать крупномасштабные города для мобильных платформ.

Шаг 1: Server-Side Spatial Partitioning

Фундаментальный враг производительности сервера в масштабных Multiplayer-играх — это проблема O(N²). Если ваш сервер перебирает каждого игрока, чтобы проверить его расстояние относительно всех остальных игроков для определения необходимости сетевых обновлений, математика масштабируется катастрофически. 100 игрокам требуется 10 000 проверок расстояния за тику. 1 000 игрокам требуется 1 000 000 проверок. При частоте Server Tick Rate в 30 Гц это 30 миллионов проверок в секунду.

Чтобы решить эту проблему, мы должны внедрить Spatial Hashing (или систему Grid/Quadtree). Разделив город на логическую сетку (Grid), игроки проверяют Network Relevance только по отношению к сущностям в их текущей ячейке и непосредственно окружающих ячейках. Это превращает наш кошмар O(N²) в O(1) поиск по сетке с последующей сильно ограниченной локальной проверкой.

Реализация Spatial Hash Grid (пример на C#)

Вот высокоэффективная реализация 2D Spatial Hash Grid на C#, которую вы можете адаптировать для Unity, Godot (через C#) или кастомного Backend-сервера для управления близостью сущностей без перебора всего состояния мира.

using System.Collections.Generic;
using UnityEngine;

public class SpatialHashGrid
{
    private readonly float _cellSize;
    private readonly Dictionary<Vector2Int, HashSet<uint>> _grid;

    public SpatialHashGrid(float cellSize = 50f)
    {
        _cellSize = cellSize;
        _grid = new Dictionary<Vector2Int, HashSet<uint>>();
    }

    // Convert a world position to a grid coordinate
    private Vector2Int GetCellCoordinate(Vector3 position)
    {
        return new Vector2Int(
            Mathf.FloorToInt(position.x / _cellSize),
            Mathf.FloorToInt(position.z / _cellSize)
        );
    }

    // Add or update a player's position in the grid
    public void UpdateEntityPosition(uint entityId, Vector3 oldPosition, Vector3 newPosition)
    {
        Vector2Int oldCell = GetCellCoordinate(oldPosition);
        Vector2Int newCell = GetCellCoordinate(newPosition);

        if (oldCell != newCell)
        {
            if (_grid.ContainsKey(oldCell))
            {
                _grid[oldCell].Remove(entityId);
            }
            
            if (!_grid.ContainsKey(newCell))
            {
                _grid[newCell] = new HashSet<uint>();
            }
            _grid[newCell].Add(entityId);
        }
    }

    // Retrieve all entities in the immediate vicinity (9 cells)
    public List<uint> GetEntitiesInProximity(Vector3 position)
    {
        List<uint> nearbyEntities = new List<uint>();
        Vector2Int centerCell = GetCellCoordinate(position);

        // Loop through the 3x3 grid around the player
        for (int x = -1; x <= 1; x++)
        {
            for (int y = -1; y <= 1; y++)
            {
                Vector2Int cellToCheck = new Vector2Int(centerCell.x + x, centerCell.y + y);
                if (_grid.TryGetValue(cellToCheck, out HashSet<uint> entitiesInCell))
                {
                    nearbyEntities.AddRange(entitiesInCell);
                }
            }
        }

        return nearbyEntities;
    }
}

Направляя логику Network Replication через GetEntitiesInProximity, ваш сервер рассчитывает точные расстояния только для нескольких десятков игроков, находящихся рядом друг с другом, что радикально снижает нагрузку на CPU и позволяет вашему серверу комфортно обрабатывать тысячи одновременных подключений в одном инстансе.

Шаг 2: Network Interest Management

Даже если Spatial Hashing решит проблему узкого места CPU на сервере, у вас все равно останется проблема пропускной способности. Мобильные сети (4G/5G) по своей природе нестабильны, склонны к высокому Jitter и имеют строгие ограничения Bandwidth. Отправка данных о 50 ближайших игроках в каждом тике переполнит Socket Buffer мобильного клиента, что приведет к экстремальным десинхронизациям.

Interest Management (или Network Relevancy) — это практика приоритизации того, что именно отправляется по сети. Игроку в 2 метрах от вас, участвующему в перестрелке, требуется 30 обновлений в секунду. Игроку в 40 метрах, идущему по другой улице, достаточно 2 обновлений в секунду.

Переопределение Network Relevancy (пример на Unreal Engine C++)

В Unreal Engine вы можете взять это под контроль, переопределив функцию IsNetRelevantFor. Это позволит вам агрессивно отсекать сетевой трафик на основе Line-of-Sight и уровней дистанции.

bool ACityPlayerCharacter::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
    // 1. Always relevant to ourselves
    if (RealViewer == this || ViewTarget == this)
    {
        return true;
    }

    // 2. Calculate squared distance (faster than exact distance)
    const float DistanceSquared = FVector::DistSquared(SrcLocation, GetActorLocation());

    // 3. Absolute Cull Distance (e.g., 10,000 units = 100 meters)
    const float MaxRelevancyDistSq = 100000000.0f; 
    if (DistanceSquared > MaxRelevancyDistSq)
    {
        return false;
    }

    // 4. Dynamic Network Update Frequency based on distance
    // If they are far away, we lower how often we send data
    if (DistanceSquared > 25000000.0f) // 50 meters
    {
        NetUpdateFrequency = 2.0f; // 2 updates a second
    }
    else
    {
        NetUpdateFrequency = 30.0f; // 30 updates a second
    }

    return Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
}

Динамически масштабируя NetUpdateFrequency в зависимости от расстояния, вы можете сократить исходящий трафик сервера более чем на 70%, сохраняя мобильный трафик игрока и предотвращая скачки Latency.

Шаг 3: Клиентские лимиты памяти и Asset Streaming

У серверов много RAM; у мобильных телефонов — нет. iPhone 13 имеет 4 ГБ объединенной памяти. Операционная система iOS обычно резервирует около 1.5–2 ГБ из них. Ваша игра должна полностью уместиться в оставшиеся 2 ГБ. Если вы загрузите весь масштабный город в память сразу, ОС мгновенно завершит работу приложения.

Чтобы выжить в таких условиях, ваш город должен быть разделен на чанки и загружаться асинхронно через Asset Streaming.

  • Hierarchical Level of Detail (HLODs): Вместо рендеринга 50 отдельных зданий в далеком городском квартале (что составляет 3 000 Draw Calls), вы должны запечь весь этот квартал в один Static Mesh с единым Texture Atlas. Это сокращает количество Draw Calls для удаленной геометрии с тысяч до ровно одного.
  • Addressable Asset Systems: Никогда не используйте жесткие ссылки (Hard References) в ваших основных ассетах данных. Если игрок спавнится в Районе А, клиент должен использовать асинхронную загрузку (например, Unity Addressables или Unreal PrimaryAssetLabels), чтобы загрузить только текстуры и меши, необходимые для Района А. Район Б должен быть полностью выгружен из RAM.
  • Texture Compression: Полагайтесь исключительно на ASTC (Adaptive Scalable Texture Compression) для мобильных устройств. Это позволяет гибко настраивать размер блоков, давая детальный контроль над соотношением памяти и визуального качества для каждой текстуры.

Шаг 4: Distributed Backend Architecture и Server Sharding

Огромный мегаполис не может работать на одной физической машине. При проектировании города масштаба MMO мир должен быть физически разделен между несколькими инстансами серверов (Shards или ноды). Когда игрок пересекает мост из центрального района в трущобы, его соединение и состояние мира должны плавно передаваться (Hand-off) между двумя совершенно разными серверными процессами.

Самостоятельная разработка этого требует настройки Kubernetes-кластеров под управлением таких систем, как Agones, шардирования баз данных с помощью Redis для передачи состояния игрока между нодами и кастомных UDP Load Balancers для бесшовной передачи соединений. Надежная реализация этого процесса, при которой игроки не теряют предметы во время перехода, — это масштабная задача, требующая 4–6 месяцев целенаправленной DevOps-работы опытной команды инженеров.

Если вы не обеспечите правильную обработку RPC-очередей и записей в базу данных во время этих переходов, вы неизбежно столкнетесь с повреждением данных. Ранее мы рассматривали механику исправления проблем с репликацией RPC в Unreal Engine, нарушающих состояние, и те же самые принципы применимы к пространственной передаче между нодами сервера.

Именно здесь раскрываются преимущества платформенных решений. В horizOn эти высоконагруженные Backend-сервисы, синхронизация баз данных в реальном времени и оркестрация Dedicated Servers уже преднастроены. Вместо того чтобы тратить бюджет на проектирование и отладку сетевых правил Kubernetes, вы можете сосредоточиться исключительно на геймплейных циклах вашего города и клиентской оптимизации.

Best Practices для Worldbuilding мобильного города

Чтобы ваш город безупречно масштабировался до миллионов пользователей, сохраняя высокую частоту кадров на бюджетных устройствах, строго придерживайтесь следующих архитектурных правил:

  1. Агрессивный Instance Pooling: Никогда не используйте Instantiate() или SpawnActor для временных объектов, таких как транспорт, пешеходы или снаряды во время геймплея. Мобильные CPU сильно перегружаются при выделении памяти и Garbage Collection. Заранее подготавливайте пулы объектов во время экрана загрузки и циклически используйте их.
  2. Texture Atlasing для городских кварталов: Draw Calls — главный убийца мобильных GPU (которые полагаются на Tile-Based Deferred Rendering). Объединяйте текстуры всех общих пропсов (мусорные баки, скамейки, фонари) в один большой атлас. Это позволит движку объединять рендеринг сотен пропсов в один Draw Call.
  3. Строгие Polycount Budgets на чанк: Вводите жесткие лимиты. Один чанк мобильного города (например, область 100x100 метров) в идеале должен содержать менее 300 000 видимых треугольников. Полагайтесь на Normal Maps, а не на геометрию для симуляции архитектурных деталей.
  4. Внедрение Server-Side Hibernation: Запуск Dedicated Server для огромного города, где 80% карты в данный момент пусты — это прямой путь к банкротству студии. Вам нужно агрессивное управление инстансами, вдохновленное предложением по оптимизации серверов Fortnite (Hibernation), чтобы отключать неиспользуемые координаты сетки и мгновенно «пробуждать» их при приближении игрока.
  5. Разделение коллизий и визуального меша: Никогда не используйте сложные визуальные меши для расчетов коллизий на сервере. Сервер должен видеть город только как набор низкополигональных примитивов (боксы, капсулы, сферы). Это сводит к минимуму потребление памяти сервером и делает физические расчеты сверхбыстрыми.

Распространенные ошибки, которых следует избегать

  • Ловушка переполнения RPC: Разработчики часто запускают RPC от сервера к клиенту для визуальных эффектов (например, искры при столкновении машин). Не делайте этого. Сервер должен реплицировать только состояние машины (например, bIsCrashed = true). Клиент должен самостоятельно отследить это изменение состояния через OnRep/property hook и запустить эффект искр локально. Это экономит огромный объем сетевого трафика.
  • Утечки памяти при переходах между зонами: При выгрузке чанка города на мобильном устройстве убедитесь, что вы принудительно вызываете Garbage Collection или вручную выгружаете Asset Bundles. Если оставлять даже несколько мегабайт «осиротевших» текстур в памяти при каждом перемещении игрока между зонами, игра неизбежно вылетит через 20 минут геймплея.

Заключение

Достижение настоящей оптимизации масштабирования мобильной игры — это искусство баланса. Оно требует борьбы за каждый мегабайт клиентской RAM, строгого регулирования Network Relevancy и распределения серверной нагрузки между масштабируемыми нодами Backend. Внедряя Spatial Hashing, динамическую частоту обновлений и асинхронный Asset Streaming, вы сможете создавать огромные живые города, которые плавно работают даже на мобильном железе многолетней давности.

Однако создание масштабируемой инфраструктуры для маршрутизации тысяч одновременных соединений и управления бесшовной передачей данных между серверами зачастую сложнее, чем создание самой игры. Готовы масштабировать свой Multiplayer Backend без кошмаров с DevOps? Попробуйте horizOn бесплатно или изучите API docs, чтобы увидеть, как мы обрабатываем высоконагруженную архитектуру «из коробки».


Источник: Designing Large-Scale Mobile Game Cities: Production, Optimization, & Worldbuilding Expertise