ブログに戻る

モバイルゲームのスケーリング最適化:100万同時接続プレイヤーを支える都市設計のアーキテクチャ

公開日 2026年5月21日
モバイルゲームのスケーリング最適化:100万同時接続プレイヤーを支える都市設計のアーキテクチャ

要点まとめ

100万人以上の同時接続プレイヤーを支えるモバイル向け大規模都市の構築には、サーバー側のSpatial Partitioning、クライアント側の徹底したメモリ管理、そして分散型バックエンドの3本柱が不可欠です。本記事では、O(N²)問題を解決するSpatial Hashingや、モバイル端末の厳しいRAM制限を回避するAsset Streaming、効率的なNetwork Interest Managementの実装手法を具体的に解説します。horizOnのようなプラットフォームを活用することで、DevOpsの複雑さを回避しながら、スケーラブルなマルチプレイヤー・インフラを迅速に構築することが可能です。

マルチプレイヤー開発者なら誰しも、モバイルゲームのアーキテクチャが限界を迎える瞬間を知っています。広大で美しい都市環境を設計し、ローカル環境で10台のシミュレーションクライアントを使ってテストすれば、ビルドは完璧な60 FPSで動作します。しかし、1,000人の同時接続プレイヤー(CCU)が中央広場に集まるライブ環境にデプロイした途端、状況は一変します。数秒以内に、ローエンドのAndroidデバイスはOut-Of-Memory (OOM)例外でハードクラッシュし、iOSのJetsamはアプリケーションを強制終了させ、Dedicated ServerのCPUは数千のエンティティのNetwork Replicationを計算しようとして100%にスパイクします。

モバイル向けのMMOや、数百万人ものアクティブユーザーを想定した大規模なオープンワールドを構築する場合、エンジンのデフォルト設定に頼ることはできません。モバイルハードウェアには厳しいサーマルスロットリングとメモリ制限があり(中価格帯のデバイスではゲームが使用できるRAMが2GB未満に制限されることも少なくありません)、同時にサーバーは高密度のプレイヤー集団を支えなければなりません。

真のモバイルゲームのスケーリング最適化を実現するには、「サーバー側の強力なSpatial Partitioning」、「クライアント側の徹底したメモリ管理」、そして膨大な接続数を処理するための「分散型バックエンドアーキテクチャ」という3本柱のアプローチが必要です。このチュートリアルでは、モバイルプラットフォーム向けに大規模な都市を構築するためのアーキテクチャをステップバイステップで解説します。

ステップ 1: サーバー側の Spatial Partitioning

大規模マルチプレイヤーゲームにおけるサーバーパフォーマンスの根本的な敵は「O(N²)問題」です。サーバーが全プレイヤーをループし、他の全プレイヤーとの距離をチェックしてネットワーク更新が必要かどうかを判断しようとすると、計算負荷は破滅的に増加します。100人のプレイヤーなら1チックあたり10,000回の距離チェックで済みますが、1,000人なら1,000,000回になります。サーバーのチックレートが30Hzなら、毎秒3,000万回のチェックが発生することになります。

これを解決するには、Spatial Hashing(あるいはGrid/Quadtreeシステム)を実装する必要があります。都市を論理的なグリッドに分割することで、プレイヤーは現在のセルとその周囲のセルに存在するエンティティに対してのみネットワークの関連性をチェックすればよくなります。これにより、O(N²)の悪夢をO(1)のグリッドルックアップと、制限されたローカルチェックにまで削減できます。

Spatial Hash Gridの実装(C#の例)

以下は、UnityやGodot(C#経由)、あるいは独自のバックエンドサーバーに適用できる、効率的な2D Spatial Hash Gridの実装例です。ワールド全体のステートをループすることなく、エンティティの近接性を管理できます。

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)は本質的に不安定で、ジッターが発生しやすく、帯域制限も厳しいものです。近くにいる50人のプレイヤーのデータを毎チック送信すれば、モバイルクライアントのソケットバッファを圧迫し、極端な同期ズレ(Desync)を引き起こします。

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%以上削減でき、プレイヤーのモバイルデータプランを保護しつつ、レイテンシのスパイクを防ぐことができます。

ステップ 3: クライアント側のメモリ制限と Asset Streaming

サーバーには十分なRAMがありますが、スマートフォンにはありません。iPhone 13のユニファイドメモリは4GBですが、iOSが通常そのうち約1.5GBから2GBを予約します。ゲームは残りの2GBというフットプリント内に完全に収める必要があります。大規模な都市を一度にメモリにロードすれば、OSは即座にアプリケーションを終了させます。

この環境で生き残るには、都市をチャンク化し、非同期でストリーミングする必要があります。

  • Hierarchical Level of Detail (HLODs): 遠くの街区にある50個の建物を個別にレンダリング(3,000ドローコールに相当)する代わりに、街区全体を単一のTexture Atlasを持つ単一のスタティックメッシュとしてベイクします。これにより、遠景のドローコールを数千から正確に1つに削減できます。
  • Addressable Asset Systems: 主要なデータアセットでハードリファレンスを使用しないでください。プレイヤーが「地区A」でスポーンした場合、クライアントは非同期ロード(UnityのAddressablesやUnrealのPrimaryAssetLabelsなど)を使用して、「地区A」に必要なテクスチャとメッシュのみをダウンロードまたはロードし、「地区B」はメモリから厳密にパージする必要があります。
  • Texture Compression: モバイルではASTC (Adaptive Scalable Texture Compression) を専ら利用してください。ブロックフットプリントを柔軟に変更できるため、テクスチャごとにメモリ消費とビジュアル品質を細かく制御できます。

ステップ 4: 分散型バックエンドアーキテクチャと Server Sharding

巨大なメトロポリスを単一の物理マシンで動かすことは不可能です。MMO規模の都市を設計する場合、ワールドを複数のサーバーインスタンス(ShardまたはNode)に物理的に分割する必要があります。プレイヤーがダウンタウン・ノードからスラム・ノードへ橋を渡る際、クライアントの接続とワールドステートは、2つの全く異なるサーバープロセス間でシームレスにハンドオフ(引き継ぎ)されなければなりません。

これを自前で構築するには、AgonesのようなシステムでオーケストレーションされたKubernetesクラスター、サーバーノード間でプレイヤーのステートを受け渡すためのRedisによるデータベース・シャーディング、そしてシームレスな接続引き継ぎのためのカスタムUDPロードバランサーのセットアップが必要です。これを堅牢に設計し、遷移中にプレイヤーのアイテムが消失しないようにするのは、シニアエンジニアチームにとっても4〜6ヶ月の専任DevOps作業を要する大仕事です。

これらのハンドオフ中にRPCキューやデータベースへの書き込みを適切に処理しないと、必然的にステートの破損(State Corruption)が発生します。以前、Unreal EngineのRPCリプリケーション問題がステートを破壊する現象の修正方法について解説しましたが、全く同じ原則がサーバーノード間の空間的ハンドオフにも当てはまります。

ここでプラットフォームソリューションが真価を発揮します。horizOnを利用すれば、これらの高コンカレンシーなバックエンドサービス、リアルタイムのデータベース同期、Dedicated Serverのオーケストレーションが事前に設定された状態で提供されます。Kubernetesのネットワーキングルールの構築やデバッグに貴重な時間を費やす代わりに、都市のゲームプレイループやクライアントの最適化に専念できます。

モバイル都市ワールドビルディングのベストプラクティス

低スペック端末でも高いフレームレートを維持しつつ、数百万人のユーザー規模に対応させるために、以下のアーキテクチャ・ルールを厳守してください。

  1. 徹底的な Instance Pooling: 車両、歩行者、プロジェクタイル(弾丸など)のように一時的に存在するオブジェクトに対し、プレイ中にInstantiate()SpawnActorを決して使用しないでください。モバイルCPUはメモリ確保とGarbage Collectionに非常に弱いです。ロード画面中にオブジェクトプールをプリウォーム(事前生成)し、継続的に再利用してください。
  2. 街区単位の Texture Atlasing: ドローコールは、Tile-Based Deferred Renderingを採用するモバイルGPUの最大の敵です。ゴミ箱、ベンチ、街灯といった汎用的なストリートプロップのテクスチャを1つの大きなTexture Atlasにまとめます。これにより、エンジンは何百ものプロップのレンダリングを1つのドローコールにバッチ処理できます。
  3. チャンクごとの厳格なポリゴン予算: 限界を設けてください。モバイル都市の1つのチャンク(例:100x100メートル四方)は、表示される三角形の数を理想的には300,000以下に抑えるべきです。建築物のディテールを表現するには、生のジオメトリではなくNormal Mapを多用してください。
  4. サーバー側のスリープ(Hibernation)実装: マップの80%が空の状態である巨大都市のためにDedicated Serverを稼働させ続けるのは、スタジオを倒産させる近道です。Fortniteのサーバー最適化・スリープ案の分析を参考に、アイドル状態のグリッド座標を停止させ、プレイヤーが接近した瞬間に即座にウェイクアップさせる強力なインスタンス管理が必要です。
  5. 衝突判定とビジュアルメッシュの分離: サーバー側の衝突計算に複雑なビジュアルメッシュを使用しないでください。サーバーは都市をボックス、カプセル、球体といった低ポリゴンのプリミティブ形状の集合としてのみ認識すべきです。これにより、サーバーのメモリフットプリントを最小限に抑え、物理演算をサブミリ秒に保つことができます。

避けるべき一般的な落とし穴

  • RPCフラッディングの罠: 開発者はよく、車の衝突によるスパークなどの視覚効果のためにサーバーからクライアントへのRemote Procedure Call (RPC)をトリガーしてしまいます。これは避けてください。サーバーは車のステート(例:bIsCrashed = true)のみをリプリケートすべきです。クライアントはOnRepやProperty Hookを介してこのステート変化を独自に検知し、ローカルでスパークのVFXをトリガーします。これにより、膨大なネットワーク帯域幅を節約できます。
  • ゾーン遷移時のメモリリーク: モバイルで都市チャンクをストリーミングアウトする際は、明示的にGarbage Collectionを強制するか、アセットバンドルを手動でアンロードしてください。プレイヤーがゾーン間を移動するたびに数メガバイトのテクスチャがメモリに取り残されるだけで、20分後には確実にクラッシュします。

結論

真のモバイルゲームのスケーリング最適化は、絶妙なバランスの上に成り立っています。クライアントRAMの1メガバイトを巡る戦い、ネットワーク関連性の厳格な管理、そしてスケーラブルなバックエンドノードへのサーバー負荷分散が必要です。Spatial Hashing、動的な更新頻度、非同期のAsset Streamingを実装することで、数年前のモバイル端末でもスムーズに動作する、広大で活気に満ちた都市を構築できます。

しかし、数千の同時接続をルーティングし、シームレスなサーバーハンドオフを管理するためのスケーラブルなインフラを構築することは、往々にしてゲームそのものを作るよりも困難です。DevOpsの悪夢に悩まされることなくマルチプレイヤーバックエンドを拡張したい方は、horizOnを無料でお試しいただくか、APIドキュメントをご覧になり、高コンカレンシーなアーキテクチャがどのように標準提供されているかをご確認ください。


Source: Designing Large-Scale Mobile Game Cities: Production, Optimization, & Worldbuilding Expertise

このダッシュボードは以下のチームによって愛情を込めて作られています Projectmakers

© 2026 projectmakers.de

unknown-v1.91.1 / unknown-v--