GS2-MegaField

大規模3D空間におけるプレイヤー位置の効率的な同期機能

GS2-MegaField は、巨大な3D空間上に存在する大量のプレイヤーの位置情報を、効率的に共有・同期するための機能を提供します。

MMORPG やオープンワールド、メタバース型のサービスなど、同じワールドに大量のプレイヤーが同時にログインするタイトルでは、すべてのプレイヤーの位置情報を全プレイヤーに配信していると、ネットワーク帯域もCPUも破綻してしまいます。 GS2-MegaField は「自分の近傍にいるプレイヤーだけ」を効率的に取得できるようにすることで、この問題を解決します。

エリアとレイヤー

GS2-MegaField の空間は「エリア (Area)」と「レイヤー (Layer)」の2つの概念で構成されます。

  • AreaModel (エリアモデル): 1つの大きな3D空間を表すマスターデータ。たとえば「街の中」「ダンジョン」「フィールド」など、ワールド内の論理的な区画ごとに作成します。
  • LayerModel (レイヤーモデル): 1つのエリアの中に存在する、用途別のレイヤー。たとえば「プレイヤー用」「NPC用」「アイテム用」のように、同じ場所を異なる目的で重ねて利用できます。

エリアモデルは複数のレイヤーモデルを内包する形でマスターデータとして定義します。 プレイヤーの位置は「どのエリアの、どのレイヤーに、どの座標に居るか」という形で表現されます。

graph TD
  AreaModel["AreaModel: town"] --> LayerPlayer["LayerModel: player"]
  AreaModel --> LayerNpc["LayerModel: npc"]
  AreaModel --> LayerItem["LayerModel: item"]
  Player1["Player A"] -- Spot --> LayerPlayer
  Player2["Player B"] -- Spot --> LayerPlayer

空間インデックス

GS2-MegaField は、レイヤー内のプレイヤーの位置を空間インデックス (R-Tree ベースの構造) で管理します。 これによって「自分の周囲 N メートル以内に居るプレイヤーをすべて取得する」といった近傍探索を、プレイヤー数に依存せず効率的に処理できます。

レイヤー単位で空間インデックスは独立しており、「プレイヤー同士の近傍」と「NPC との近傍」のような検索を別々に取り扱えます。

プレイヤーの位置情報

各プレイヤーは「自分の現在位置」を MyPosition として GS2-MegaField に送信します。 MyPosition には以下の情報が含まれます。

  • position: 3D 座標 (x, y, z)
  • vector: 向きベクトル (x, y, z)
  • r: 視界・関心の半径

プレイヤーは位置を送信する際に、同時に「自分が興味のある範囲」を Scope として指定でき、その範囲に居る他のプレイヤーの情報を応答として受け取ることができます。 Scope には以下の情報が含まれます。

  • layerName: 検索対象のレイヤー名
  • r: 検索半径
  • limit: 返却する最大件数

複数の Scope を指定することで、同時に複数のレイヤーの近傍プレイヤーを取得することも可能です。

ゲームクライアント間の位置同期

GS2-MegaField は、プレイヤーの位置情報を「ある程度間引いた近傍プレイヤーのリスト」として提供します。 このリストには各プレイヤーの位置・向きベクトル・最終同期時刻などが含まれ、ゲームクライアントはこの情報を補間処理しながら描画することで、滑らかな位置同期を実現できます。

ただし、 GS2-MegaField 自体はリアルタイム性能を保証する常時接続のプロトコルではありません。 高頻度なアクション同期や、入力ベースの厳密な同期が必要なケースでは、 GS2-Realtime と組み合わせるのが効果的です。

トランザクションアクション

GS2-MegaField ではトランザクションアクションを提供していません。

マスターデータ管理

マスターデータを登録することでマイクロサービスで利用可能なデータや振る舞いを設定できます。

マスターデータの種類には以下があります。

  • AreaModel: ワールド内の論理的な区画 (エリア) の定義
  • LayerModel: エリアの中の用途別レイヤーの定義 (AreaModel の子として定義)

以下は AreaModel / LayerModel を設定するマスターデータの JSON 例です。

{
  "version": "2019-09-09",
  "areaModels": [
    {
      "name": "town",
      "metadata": "starter town",
      "layerModels": [
        { "name": "player", "metadata": "players" },
        { "name": "npc", "metadata": "npcs" }
      ]
    },
    {
      "name": "dungeon",
      "layerModels": [
        { "name": "player" },
        { "name": "enemy" }
      ]
    }
  ]
}

マスターデータの登録はマネージメントコンソールから登録する他、GitHubからデータを反映したり、GS2-Deployを使ってCIから登録するようなワークフローを組むことが可能です。

実装例

エリアモデル一覧の取得

ワールドにどのようなエリアが存在するかを取得します。

    var items = await gs2.MegaField.Namespace(
        namespaceName: "namespace-0001"
    ).AreaModelsAsync(
    ).ToListAsync();
    const auto It = Gs2->MegaField->Namespace(
        "namespace-0001" // namespaceName
    )->AreaModels(
    );
    TArray<Gs2::UE5::MegaField::Model::FEzAreaModelPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

自分の位置を更新し、近傍プレイヤーを取得

自分の現在位置を GS2-MegaField に登録するとともに、周囲に居る他プレイヤーのリストを取得します。 位置の更新は一定間隔で繰り返し呼び出すのが基本的な使い方です。

    var spatials = await gs2.MegaField.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Spatial(
        areaModelName: "town",
        layerModelName: "player"
    ).UpdateAsync(
        position: new Gs2.Unity.Gs2MegaField.Model.EzMyPosition
        {
            Position = new Gs2.Unity.Gs2MegaField.Model.EzPosition
            {
                X = 10.0f,
                Y = 0.0f,
                Z = 20.0f,
            },
            Vector = new Gs2.Unity.Gs2MegaField.Model.EzVector
            {
                X = 1.0f,
                Y = 0.0f,
                Z = 0.0f,
            },
            R = 1.0f,
        },
        scopes: new [] {
            new Gs2.Unity.Gs2MegaField.Model.EzScope
            {
                LayerName = "player",
                R = 50.0f,
                Limit = 100,
            },
        }
    );
    const auto Future = Gs2->MegaField->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Spatial(
        "town",   // areaModelName
        "player"  // layerModelName
    )->Update(
        MakeShared<Gs2::UE5::MegaField::Model::FEzMyPosition>(
            MakeShared<Gs2::UE5::MegaField::Model::FEzPosition>(10.0f, 0.0f, 20.0f),
            MakeShared<Gs2::UE5::MegaField::Model::FEzVector>(1.0f, 0.0f, 0.0f),
            1.0f
        ),
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::UE5::MegaField::Model::FEzScope>>>();
            v->Add(MakeShared<Gs2::UE5::MegaField::Model::FEzScope>(TEXT("player"), 50.0f, 100));
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Spatials = Future->GetTask().Result();

特定プレイヤーの位置情報の参照

ユーザーIDが分かっているプレイヤーの位置情報を直接取得することもできます。

    var spatial = await gs2.MegaField.Namespace(
        namespaceName: "namespace-0001"
    ).User(
        userId: "user-0001"
    ).Spatial(
        areaModelName: "town",
        layerModelName: "player"
    ).ModelAsync();
    const auto Future = Gs2->MegaField->Namespace(
        "namespace-0001" // namespaceName
    )->User(
        "user-0001" // userId
    )->Spatial(
        "town",   // areaModelName
        "player"  // layerModelName
    )->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Spatial = Future->GetTask().Result();

より実践的な情報

GS2-Realtime との組み合わせ

GS2-MegaField は「広いワールド内における大局的なプレイヤーの配置」を扱うのに向いています。 一方、近接戦闘やフレンドとの協力プレイのような、ミリ秒単位の同期が必要なシーンには適していません。

このようなケースでは、 GS2-MegaField で取得した近傍プレイヤーをトリガーとして GS2-Realtime のルームに集約し、近接プレイヤー同士のあいだだけリアルタイム通信を行う、といったハイブリッドな構成が有効です。

graph LR
  Player["プレイヤー"] -- 大局的な位置同期 --> MegaField["GS2-MegaField"]
  MegaField -- 近傍プレイヤー --> Player
  Player -- 近接戦闘などの同期 --> Realtime["GS2-Realtime"]
  Realtime -- 同部屋プレイヤー --> Player

更新頻度の設計

位置情報の更新は呼び出し頻度がそのままサーバー負荷とネットワーク帯域に直結します。 一般的には、画面表示のフレームレートよりも低い頻度 (例えば 5〜10Hz 程度) で更新を行い、クライアント側で補間処理を行う設計がバランスが取りやすいです。

Scope の指定の最適化

Scope で指定する半径と件数の上限は、ゲームの視界範囲や演出に応じて調整します。 すべてのプレイヤーが広い半径で大量に検索を行うと負荷が高くなるため、画面外の判定では狭い半径と少ない件数で問い合わせ、フォーカス時のみ広く取得する、といった工夫が有効です。

詳細なリファレンス